Skip to main content
Every error the Samsa API returns — for any endpoint, at any status — uses one consistent JSON envelope. Parse the machine-readable code, not the human message (messages may change; codes are stable).

The error envelope

Successful responses never contain an error object. Branch on the HTTP status first, then on error.code.

Error codes


invalid_api_key

HTTP 401. The Authorization header is missing or malformed, or the key is unknown, expired, or revoked — or the key creator is no longer a member of the organization. Expired and unknown keys are intentionally indistinguishable.
How to handle. Check the Authorization: Bearer samsa_sk_… header. If the key was revoked or expired, create a new one. Do not retry — the outcome will not change.

missing_scope

HTTP 403. The key is valid but lacks the scope the endpoint requires. param is "scope" and the message names the missing scope.
How to handle. An admin edits the key’s scopes (or issues a new key) to grant the named scope. Do not retry without changing the key.

insufficient_credits

HTTP 402. The organization’s spendable credit pool (subscription plus valid top-ups) is below the action’s cost. See Pricing. If the organization has at least one active team, the API returns the team-aware codes insufficient_team_credits or insufficient_unallocated_credits instead — with two exceptions. A budget-exempt system organization stays under the org regime and keeps this generic code even with active teams. And an operational failure inside the deduction itself (for example a degraded balance lookup) falls back to this generic code whatever the budget regime, so a plain insufficient_credits under a team setup can be transient; retry before treating it as an exhausted balance.
How to handle. If a subsequent GET /credits shows available covering the cost, you may have hit the operational-failure fallback — retry once. That read is a point-in-time snapshot, though: a state change since the rejection (a top-up, refund, budget edit, or period reset) can also explain it, so a genuine shortfall is not ruled out. Otherwise top up or upgrade the organization’s plan in the app, then retry. Nothing was charged and no job was created.

subscription_inactive

HTTP 402. The organization has no usable subscription (none active, in grace, or holding spendable top-ups). Distinct from insufficient_credits, where a subscription exists but the pool is too low.
How to handle. Reactivate billing for the organization in the app, then retry.

insufficient_team_credits

HTTP 402. Organizations can split their monthly subscription credits into per-team budgets, and every API key can be assigned to a team. A key assigned to a team with a budget spends from that team’s monthly bucket; whatever is not allocated to any team budget forms the organization’s unallocated pool. Purchased top-up credits are budget-exempt and remain available to cover the remainder. This code fires when the balance the key can spend — the smaller of the organization’s remaining plan pool and the team’s remaining budget, plus top-ups — is below the action’s cost. That is exactly the available reported by GET /credits.
The code names the budget regime, not the balance that ran out. A key assigned to a budgeted team gets this code even when the organization’s plan pool, and not the team budget, is what was exhausted — so a team can still show budget headroom. Compare plan_credits with scope.remaining on GET /credits to see which one is binding.
How to handle. An organization admin can raise the team’s monthly budget, assign the key to a different team, or buy top-up credits (top-ups are not limited by team budgets). Budget usage also resets with the next billing period. Nothing was charged and no job was created.

insufficient_unallocated_credits

HTTP 402. The key is not assigned to a budgeted team (unassigned, or its team has no budget), so it spends from the organization’s unallocated pool — the monthly subscription credits left after subtracting the budgets of all active teams. This code fires when the balance the key can spend — the smaller of the organization’s remaining plan pool and that unallocated pool’s headroom, plus budget-exempt top-ups — is below the action’s cost, the same caveat about which balance ran out as above. Organizations without any active team never see the two team-aware codes — they receive plain insufficient_credits instead.
See it coming. GET /credits reports available — what the calling key can spend right now — and a scope block naming its budget regime. A job whose estimated_credits fits inside available is not refused with any of these three codes, as long as the credit and budget/team state does not change in between — barring the operational-failure fallback: a degraded deduction can return the generic insufficient_credits even then, so retry first.
How to handle. An organization admin can free unallocated credits by lowering team budgets, assign the key to a team with available budget, upgrade the plan, or buy top-up credits. Nothing was charged and no job was created.

not_found

HTTP 404. The id is unknown, or it belongs to another organization. The two cases are indistinguishable by design, so existence is never disclosed across organizations.
How to handle. Verify the id, and that the key’s organization owns the resource. Do not retry.

validation_error

HTTP 422. The request body or a parameter failed validation. param names the offending field; message explains the constraint.
How to handle. Fix the request per message and param, then resubmit. This is a client error — retrying the same request will fail identically.

svg_acceptance_required

HTTP 422. Only POST /images/vectorizations returns this. SVG is a documented EU AI Act Art. 50(2) scope-out: an SVG cannot carry a C2PA manifest or an embedded watermark, so vector outputs are delivered unsigned. svg_acceptance must therefore be the literal boolean true — a missing, false, or any other value is rejected with this distinct code, never the generic validation_error. The acknowledgment is disclosure / audit evidence, not a compliance waiver. Nothing is charged and no job is created.
How to handle. Resubmit with "svg_acceptance": true once your integration surfaces the unsigned-output disclosure to whoever acts on the result.

svg_phase1_scope_out_required

HTTP 403. Also vectorization-only, and distinct from missing_scope — the key’s scope is fine. Delivery additionally requires a current, server-verified ToS/AUP acceptance; the svg_acceptance request flag is never trusted as that fact. At submit time a missing or stale acceptance is refused before anything is charged. The check runs again at delivery, so a job that was accepted can still be refused by GET /images/vectorizations/{id} or on webhook emission if the acceptance lapses in between — a delivery-time refusal adds no charge, but it does not refund the credits the completed job already consumed.
How to handle. Accept the current ToS/AUP in the Samsa app, then retry. For a job refused at delivery, re-request its GET status — the webhook is not replayed automatically. Accepting is disclosure, not a waiver.

rate_limited

HTTP 429. The key exceeded its per-key request-rate window. The response carries Retry-After and X-RateLimit-* headers. See Rate limits.
How to handle. Back off and retry after the Retry-After interval. Use exponential backoff for repeated 429s.

too_many_active_jobs

HTTP 429. The organization has reached its cap on concurrent in-flight jobs. The message includes the current count and the limit, and a Retry-After header is set.
How to handle. Wait for in-flight jobs to reach a terminal status (poll their GET endpoint or use a webhook) before submitting more, then retry after Retry-After.

internal_error

HTTP 500. An unexpected error on Samsa’s side. The response never leaks internal details — the request_id is your handle for support.
How to handle. Retry with backoff — 500s are often transient. If it persists, contact support@samsa.ai with the request_id.

Using request_id for support

Every error (and every success) carries a request_id, also returned as the X-Request-ID response header. It ties your client-side error, the response header, and Samsa’s server logs to one request. Log it, and quote it when you contact support@samsa.ai — it is the fastest way for us to find exactly what happened.
New error codes may be added over time (for example, additive per-endpoint codes). Treat an unrecognized code the same as its HTTP status class, and always branch on code rather than matching message text.