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.
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.
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.
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.
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.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.
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.
validation_error
HTTP 422. The request body or a parameter failed validation. param names the
offending field; message explains the constraint.
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.
"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.
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.
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.
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.
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.

