Errors
Every failure carries a stable machine code. Branch on the code, not on the message.
The control-plane error envelope
Control-plane failures return a JSON body with a human message and a machine code:
{
"message": "This entitlement has no remaining token quota.",
"code": "insufficient_tokens"
} Validation failures add an errors map keyed by field name:
{
"message": "Please check the highlighted fields and try again.",
"code": "validation_failed",
"errors": {
"email": ["This email is already registered."]
}
}code is stable and safe to compare against. message is copy: it is written for a human reading a screen, it can be reworded at any time, and it may be localised. Never branch on it, and never regex it.
Control-plane codes
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
validation_failed | 422 | The request body failed validation. Per-field messages are in errors. | Fix the input. Show the field messages rather than the summary. |
unauthenticated | 401 | No credential was presented, or it is not valid. | Sign in again, or check the API key you are sending. |
session_expired | 401 / 419 | The browser session is no longer valid. | Re-authenticate. This dashboard does this for you and returns you to the page you were on. |
forbidden | 403 | Authenticated, but not permitted — often a key scoped to different models. | Check the key scope. Do not change the base URL; that is a different failure. |
account_suspended | 403 | The account is suspended. | Contact SP Cambo. Retrying will not clear it. |
not_found | 404 | The addressed record does not exist, or is not yours. | Check the identifier. |
rate_limit_exceeded Retryable | 429 | A per-key or per-package rate limit was hit. | Back off and retry. Honour Retry-After when it is present. |
insufficient_tokens | 402 | No remaining token quota on any usable entitlement. | Buy another package. Requests stop rather than becoming an overage bill. |
insufficient_credits | 402 | No remaining credit balance. | Top up. Retrying without a top-up returns the same error. |
payment_pending Retryable | 402 | The payment for this order has not been confirmed yet. | Wait for verification, or ask SP Cambo to re-check. Re-checking is always safe. |
payment_verification_failed | 402 / 422 | The payment could not be verified against the payment network. | Do not re-pay on the strength of this alone. Check the order, then contact support. |
server_error Retryable | 5xx | A fault on the SP Cambo side. | Retry with backoff. If a request was metered and then failed, the reservation is released. |
Classified by the client
These three are produced by SP Cambo's own clients — this dashboard included — when a failure arrives without a usable code. They are not sent by the server, but you will see them in the UI and it is useful to know what they mean.
| Code | Seen as | Meaning | What to do |
|---|---|---|---|
network_unreachable Retryable | — | The request never reached SP Cambo: offline, DNS, TLS or a blocked host. | Check connectivity and the base URL. Nothing was metered. |
endpoint_unavailable | 404 / 501 | The endpoint has not been published yet. | Nothing to fix on your side. Pages that depend on one say so plainly. |
unknown_error | any | A failure that did not carry a recognised code. | Treat as unexpected. Report it if it persists. |
Handling them
const res = await fetch(url, { headers })
if (!res.ok) {
const body = await res.json().catch(() => ({}))
switch (body.code) {
case 'insufficient_tokens':
case 'insufficient_credits':
return stopAndPromptForTopUp() // never retry: the answer will not change
case 'rate_limit_exceeded':
case 'concurrency_limit_exceeded':
return retryAfterBackoff(res.headers.get('retry-after'))
case 'session_expired':
case 'unauthenticated':
return reauthenticate()
default:
throw new Error(body.message ?? 'Request failed')
}
}The important distinction is between retry and stop. A quota error will return the same answer however many times you send it, so retrying only burns your own rate limit. A 429 or a 5xx is worth retrying with backoff.
That example reads body.code, which is the control-plane shape. Inference failures put the code somewhere else — see below before reusing this against the gateway.
Errors from the inference gateway
Inference failures do not use the envelope above. They are shaped like the API you are calling, so your existing SDK error handling keeps working — which also means the SP Cambo code is in a different field depending on the surface. On /v1/messages and /v1/messages/count_tokens:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "The model does not support this inference protocol.",
"sp_cambo_code": "model_unavailable"
}
} On /v1/responses, /v1/chat/completions and the key and model read endpoints:
{
"error": {
"message": "The model does not support this inference protocol.",
"type": "invalid_request_error",
"code": "model_unavailable"
}
} Read the code from error.sp_cambo_code on the first and error.code on the second. The type field is the upstream classification your SDK expects and is coarser than the code — several distinct SP Cambo failures share one type, so branch on the code.
Gateway codes
These are additional to the codes above; rate_limit_exceeded, insufficient_tokens, insufficient_credits, account_suspended and server_error also arrive here, meaning the same thing.
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
invalid_api_key | 401 | No key was sent, or it is malformed, or SP Cambo does not recognise it. | Check the key. An SP Cambo key starts with sk-. |
conflicting_api_keys | 401 | Authorization and x-api-key were both sent, with different values. | Send one credential. A leftover environment variable from another provider is the usual cause. |
api_key_disabled | 403 | The key exists but is disabled. Also api_key_revoked and api_key_expired. | Re-enable it, or use another. A revoked key never comes back. |
model_not_allowed | 403 | The alias is outside this key's scope, or is no longer published. | Call GET /v1/models to see what this key may use. |
model_unavailable | 400 | The alias does not support the protocol you called it through. | Use a protocol the alias supports. The catalogue lists them per alias. |
invalid_model | 400 | The model field is missing or is not a valid alias. | Send a public alias, not a provider model id. |
unsupported_parameter | 400 | A parameter outside the set this surface accepts. The message names it. | Remove it. It is rejected rather than dropped so it cannot silently not apply. |
invalid_max_output_tokens | 400 | The maximum output field is not a positive integer. | Send a positive integer, or omit it and take the default. |
max_output_tokens_exceeded | 400 | More output was requested than the key permits. | Lower the request. Your key's ceiling is on its card in the dashboard. |
request_too_large | 413 | The body exceeds the service limit or your key's max_request_bytes. | Split the request. It is refused before any upstream call, so it costs nothing. |
concurrency_limit_exceeded Retryable | 429 | Too many of your requests are in flight at once. | Wait for one to finish — Retry-After is short. Bound your own parallelism. |
rate_limiter_unavailable Retryable | 503 | The limiter could not be reached, so the request was refused rather than admitted unchecked. | Retry with backoff. Nothing was metered. |
billing_unavailable Retryable | 503 | The control plane could not be reached to reserve or settle the request. | Retry with backoff. No quota is spent on a request that was never reserved. |
upstream_rejected | 4xx | The provider refused the request itself. The status is passed through. | Fix the request. The reservation is released, so it is not charged. |
upstream_unavailable Retryable | 503 | The provider was unavailable, over capacity or too slow to answer. | Retry with backoff. |
upstream_invalid_response Retryable | 502 | The provider returned something that is not a valid response body. | Retry. The reservation is reconciled rather than charged as used. |
billing_settlement_pending Retryable | 502 | The response carried no usage figures, so it could not be settled. | Retry. The reservation is held for reconciliation, not billed as an estimate. |
client_disconnected | 499 | Your client closed the connection before the response finished. | Usually your own timeout or a cancelled request. The reservation is released. |
Every 4xx in the table is refused before any upstream call and is therefore not metered — a misconfigured client cannot quietly drain a package. The exception is upstream_rejected, which is the provider refusing the request itself; there the reservation is released, so it is not charged either. Where a request reached the provider and then failed part-way, the reservation is released or held for reconciliation rather than settled, so you are never charged an estimate for a response you did not receive.
For errors that appear mid-stream rather than in the status line, see streaming .
What errors never contain
Error bodies never include a stack trace, an internal hostname, an upstream provider identifier or a credential. If you are seeing a framework error page instead of one of these envelopes, you are not talking to SP Cambo — check the base URL and any proxy in between.