Errors
Every failure is a JSON body with an HTTP status:
{
"error": {
"message": "Budget has been exceeded! Current cost: 20.02, Max budget: 20.0",
"type": "insufficient_credits",
"param": null,
"code": "insufficient_credits"
}
}
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. | 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 deployment of the model is at capacity right now. | Retry with backoff, or switch models. |
| 5xx | api_error | provider_error | The model's provider failed. Not charged. | Retry with backoff; if it persists, switch models. |
| 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
Every response, success or failure, carries the header x-oceantoken-request-id. It is the Request ID shown in the console under Logs, where you can see the same request with its status, latency and cost. Log it with your errors and quote it when you write to us.
Retries
Retry only on 429, 5xx 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.
import random, time
from openai import OpenAI, APIStatusError, APIConnectionError
client = OpenAI(base_url="https://api.oceantoken.ai/v1", api_key=API_KEY, max_retries=0)
def with_retries(call, attempts=5):
for attempt in range(attempts):
try:
return call()
except APIConnectionError:
pass # network: retry
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")
if retry_after:
time.sleep(float(retry_after)); continue
time.sleep(min(30, 2 ** attempt) + random.random())
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. You are charged only for the tokens that were delivered.
Balance exhausted
When available credits reach zero, requests are refused with 400 insufficient_credits. Nothing is run and nothing is charged. Top up 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. Search by the Request ID (x-oceantoken-request-id).