Errors
Every failure is a JSON body with an HTTP status. This is the answer to a request sent without a key:
{
"error": {
"message": "Missing API key. Send it as: Authorization: Bearer <your OceanToken key>.",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}
Branch on code. It is a fixed vocabulary that we keep stable; message is for people and its wording may change. type groups codes into families that are handled the same way. The HTTP status tells you the same thing at a coarser grain, so existing clients that branch on it keep working.
Codes
| HTTP | type | code | What happened | What to do |
|---|---|---|---|---|
| 400 | invalid_request_error | invalid_request | The request was malformed: a missing field, a value the model does not accept, a bad image. | Fix the request. Do not retry as-is. |
| 400 | not_found_error | model_not_found | The model name does not exist. | Pick a name from GET /v1/models. |
| 400 | insufficient_credits | insufficient_credits | Your available credits are exhausted. Nothing ran and nothing was charged. | Add credits, then retry. |
| 400 | insufficient_credits | key_budget_exceeded | This key's own credit limit is used up (your account may still have credits). | Raise the key's limit, wait for its reset, or use another key. |
| 400 | content_policy_violation | content_filtered | A safety check refused the request before the model ran. Not charged. | Change the input. Retrying the same input gives the same answer. |
| 401 | authentication_error | invalid_api_key | No key, an unknown key, or a deleted key. | Check the Authorization: Bearer sk-... header. |
| 401 | authentication_error | key_expired | The key's expiration date has passed. | Create a new key. |
| 403 | permission_error | model_not_allowed | The key is valid but this model is outside your account's allowed set. | Use a model from GET /v1/models. |
| 403 | permission_error | forbidden | The key may not perform this operation. Managing API keys is one: an API key cannot create, change, rotate or delete keys. | Manage keys in the console; otherwise use a key with the right scope. |
| 404 | not_found_error | not_found | Unknown path, or a video / job id that does not exist. | Check the URL and the id. |
| 409 | idempotency_error | idempotency_key_reused | The Idempotency-Key was already used with a different body. | Use a fresh key for a new request. See Retries. |
| 409 | idempotency_error | idempotency_key_in_progress | The first request with this key is still running. | Wait for Retry-After seconds, then retry the same request. |
| 429 | rate_limit_error | rate_limited | You exceeded your key's requests- or tokens-per-minute limit. | Back off until Retry-After; see Limits. |
| 429 | rate_limit_error | provider_overloaded | Every channel of the model is at capacity right now. See Retries and failover. | Retry with backoff, or switch models. |
| 5xx | api_error | provider_error | The model failed, after the gateway's own retry and failover. Not charged. | Retry with backoff; if it persists, switch models. |
| 408 | api_error | timeout | A stream received nothing from the model for 120 seconds after it started. | Retry with backoff. |
| 504 | api_error | timeout | The request ran past the 240-second window. | Stream the response, shorten the request, or use an asynchronous endpoint. |
Anything not in this table is an api_error. New codes may be added; existing ones are not renamed or removed.
The request ID
Responses to model requests carry the header x-oceantoken-request-id. Log it with your errors and quote it when you write to us. A request refused for its key (401) never reached a model and has none.
A successful response also has an id in its body, and every chunk of a stream carries the same id. That is the Request ID the console shows under Logs, with the request's status, latency and cost, and the id that GET /v1/usage/{request_id} looks up.
Retries
Before an error reaches you, the gateway has already retried it and, where the model has another channel, moved the request there. Retries and failover explains what it handles and what it leaves to you.
Retry only on 429, 5xx, a timeout error and network failures (a dropped connection, a client-side timeout). Everything in the 400 range will fail the same way again until the request or the account changes.
Use exponential backoff with jitter, starting around one second and capping at about thirty; when the response carries Retry-After (a number of seconds), wait at least that long.
# Retry only what can succeed on a second try: 429, 5xx and network failures,
# with exponential backoff and jitter, honouring Retry-After when it is sent.
import os
import random
import time
from openai import APIConnectionError, APIStatusError, OpenAI
client = OpenAI(
base_url=os.getenv("OCEANTOKEN_BASE_URL", "https://api.oceantoken.ai/v1"),
api_key=os.environ["OCEANTOKEN_API_KEY"],
max_retries=0, # the loop below does the retrying
)
def with_retries(call, attempts=5):
for attempt in range(attempts):
try:
return call()
except APIConnectionError:
if attempt == attempts - 1:
raise
delay = None # network failure: back off and try again
except APIStatusError as e:
if e.status_code not in (429, 500, 502, 503, 504) or attempt == attempts - 1:
raise
retry_after = e.response.headers.get("retry-after")
delay = float(retry_after) if retry_after else None
time.sleep(delay if delay is not None else min(30, 2**attempt) + random.random())
models = with_retries(lambda: client.models.list())
print(len(models.data), "models")
Retrying a request that costs money
Image, video and speech requests are charged when they run. A retry after a network timeout would otherwise run them a second time. Send an Idempotency-Key header (any unique string, up to 255 characters, for example a UUID) on POST /v1/images/generations, /v1/images/edits, /v1/videos and /v1/audio/speech:
- The first request runs normally and its response is kept for 24 hours.
- A retry with the same key and the same body gets that response back, marked with the header
Idempotency-Replayed: true. Nothing runs and nothing is charged. - The same key with a different body is refused with
409 idempotency_key_reused. - A retry while the first request is still running gets
409 idempotency_key_in_progresswithRetry-After. - A
5xxanswer is not kept, so a retry after a provider failure runs again.
Keys are scoped to your API key. The header is ignored on every other endpoint, and on the endpoints above a request without it behaves exactly as before. Responses above 8 MB are not kept; the request still runs, only the replay does not apply.
Errors while streaming
With "stream": true, an error that occurs before the first token is an ordinary JSON error response with the status above. An error after the stream has started arrives as a final data: chunk whose payload is the same error object, and then the stream ends.
A stream that ends early, because the connection broke, you disconnected, or an error cut it off, is billed at the model's usual prices for:
- its prompt, in full;
- the output streamed up to that point. A reasoning model's streamed reasoning counts as output, even when no answer text had arrived yet.
Output the model may have gone on generating after the stream stopped is not billed. A stream that ended before producing any output is not billed at all.
Balance exhausted
When your balance is at or below $0, every request is refused with 400 insufficient_credits, free models included. Nothing is run and nothing is charged. Add credits and retry.
Content policy
Requests that trip a safety check are refused with 400 content_filtered. The check runs before the model is called, so the request is not charged.
Finding a failed request
Every failure is recorded in the console under Logs with its status and the response body. Find it by its time and model, and quote its x-oceantoken-request-id when you write to us.