Errors

The error envelope, every error code Jettson emits, and how to react.

Every Jettson API error comes back in a stable envelope. Parse the code field to branch on category — message is for humans, code is for code.

Error envelope

json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many spawn requests. Try again in 12s.",
    "details": {
      "plan": "free",
      "window": "minute",
      "limitPerMinute": 5,
      "limitPerHour": 30
    }
  }
}

| Field | Always present | | --- | --- | | error.code | Yes — stable string identifier | | error.message | Yes — human-readable, can change | | error.details | Optional — structured context for UI rendering |

Some errors also include a Retry-After HTTP header (always for rate_limited).

Code reference

Authentication

| Code | HTTP | Meaning | | --- | --- | --- | | invalid_api_key | 401 | Missing, malformed, or revoked key | | missing_id_token | 401 | Only on Console endpoints — missing Firebase ID token | | invalid_id_token | 401 | Firebase token expired or signature invalid |

Validation

| Code | HTTP | Meaning | | --- | --- | --- | | invalid_body | 400 | Body wasn't JSON or wasn't an object | | invalid_task | 400 | Agent spawn — missing or oversized task | | invalid_key | 400 | Memory write — missing/oversized key | | invalid_value | 400 | Memory write — wrong type or over 5000 chars | | invalid_query | 400 | Bad query string parameters | | invalid_plan | 400 | Checkout — unknown plan tier | | invalid_price_id | 400 | Checkout — unknown Stripe price |

Quotas (402 — Payment Required)

| Code | Meaning | Recommended action | | --- | --- | --- | | monthly_quota_exceeded | Plan's agent-hour budget used | Upgrade or wait for the first of the month | | memory_quota_exceeded | Memory pool full | POST /api/v1/memory/dedupe first; otherwise upgrade |

Rate / concurrency (429 — Too Many Requests)

| Code | Meaning | Recommended action | | --- | --- | --- | | rate_limited | Per-key spawn rate hit | Honor Retry-After header; back off | | concurrent_limit_reached | Plan's concurrent agents at cap | Wait for one to finish, or stop one |

Resources

| Code | HTTP | Meaning | | --- | --- | --- | | agent_not_found | 404 | Wrong agent ID, or it belongs to a different account | | not_found | 404 | Memory not found by (key, namespace) | | user_not_found | 404 | Internal billing routes when the Firebase user doc is missing |

Provider

| Code | HTTP | Meaning | | --- | --- | --- | | temporarily_unavailable | 503 | Container provider transient failure — retry with backoff | | billing_unavailable | 502 | Stripe transient failure during checkout / portal mint | | mind_unavailable | 503 | Internal reasoning proxy errored — retry with backoff |

Generic

| Code | HTTP | Meaning | | --- | --- | --- | | internal_error | 500 | Unexpected — log it, retry once with backoff, then bail |

Retry guidance

| Code | Retry? | How | | --- | --- | --- | | rate_limited | Yes | Wait Retry-After seconds, then retry. Don't ignore the header. | | temporarily_unavailable / mind_unavailable | Yes | Exponential backoff, 1s → 2s → 4s, max 3 tries. | | internal_error | Once | If the second call also fails, surface the error — don't loop. | | concurrent_limit_reached | Yes (delayed) | Poll until concurrent count drops, then retry. Or upgrade. | | monthly_quota_exceeded | No | Reset is on the 1st of next month. | | invalid_* | No | Fix the request before retrying. | | agent_not_found / not_found | No | The resource doesn't exist. |

Example

A spawn that hits the concurrent limit:

bash
curl -X POST https://jettson.dev/api/v1/agents \
  -H "Authorization: Bearer $JETTSON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"task": "..."}'
text
HTTP/2 429
Content-Type: application/json

{
  "error": {
    "code": "concurrent_limit_reached",
    "message": "You've reached your concurrent agent limit (3). Stop an existing agent or upgrade your plan.",
    "details": {
      "plan": "pro",
      "currentConcurrent": 3,
      "maxConcurrent": 3
    }
  }
}

The right move here is to either stop a running agent (DELETE /api/v1/agents/{id}) or wait for one to finish, then retry.