Quickstart: Everything needed to make a first call and handle errors # Quickstart > Create an API key, send your first request to the helping.io API, and read the reply. Five minutes, any language. ## Before you begin [Section titled “Before you begin”](#before-you-begin) * **A payment card.** Starting your plan opens a checkout page that asks for one. * **The owner role in your organization.** Only the organization’s owner can start the plan. Signing up creates an organization with you as its owner; if you were invited to someone else’s organization, ask its owner to start the plan. ## Make your first request [Section titled “Make your first request”](#make-your-first-request) 1. **Create an account and start your plan.** Sign up at [app.helping.io](https://app.helping.io/signup), then open the link in the verification email. The app then asks you to start your plan. Your first plan begins with a 14-day free trial: you enter a card on Stripe’s checkout page, and the plan is charged when the trial ends unless you cancel first. Pricing is on [helping.io](https://helping.io). 2. **Add credits.** Requests are paid for in credits, and a request with no credits behind it is refused before it runs. Leave **Add credits now** checked when you start your plan to buy them at the same checkout, or add them later from **Settings → Billing**. See [Credits and billing](/billing/credits/). 3. **Create an API key.** Open **Settings → API Keys** in the app and create a key. Owners and admins of the organization can create keys. The full secret, which starts with `sk_`, is shown once and cannot be recovered afterwards. Store it somewhere safe; if you lose it, rotate the key. See [API keys](/getting-started/authentication/). ```bash export HELPINGIO_API_KEY="sk_…" ``` 4. **Send a request.** Point any Responses API client at `https://api.helping.io/v1`. * curl ```bash curl https://api.helping.io/v1/responses \ -H "Authorization: Bearer $HELPINGIO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-sonnet-5", "input": "Say hello in exactly 3 words." }' ``` * Node ```bash npm install openai ``` ```js import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.helping.io/v1", apiKey: process.env.HELPINGIO_API_KEY, }); const response = await client.responses.create({ model: "anthropic/claude-sonnet-5", input: "Say hello in exactly 3 words.", }); console.log(response.id); console.log(response.output_text); ``` * Python ```bash pip install openai ``` ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.helping.io/v1", api_key=os.environ["HELPINGIO_API_KEY"], ) response = client.responses.create( model="anthropic/claude-sonnet-5", input="Say hello in exactly 3 words.", ) print(response.id) print(response.output_text) ``` If the request is refused, see [If your first request fails](#if-your-first-request-fails). 5. **Read the reply.** The response object’s `output` holds the assistant’s message, and `usage` holds the tokens you were charged for. The SDKs also give you the reply’s text on its own as `output_text`. The model writes a new reply each time, so your text will differ from the one below. ```json { "background": false, "completed_at": 1767225600, "conversation": null, "created_at": 1767225600, "error": null, "frequency_penalty": 0.0, "id": "resp_ZXhhbXBsZS9yZXNwL2ZpcnN0LXR1cm4AAAAAAAAAAAAAAAAAAAAAAA", "incomplete_details": null, "instructions": null, "max_output_tokens": null, "max_tool_calls": null, "metadata": null, "model": "anthropic/claude-sonnet-5", "object": "response", "output": [ { "content": [ { "annotations": [], "logprobs": [], "text": "Hello there, friend.", "type": "output_text" } ], "id": "msg_00000000000000000000000000000001", "role": "assistant", "status": "completed", "type": "message" } ], "parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, "reasoning": null, "safety_identifier": null, "service_tier": "default", "status": "completed", "store": true, "temperature": 1.0, "text": { "format": { "type": "text" } }, "tool_choice": "auto", "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled", "usage": { "input_tokens": 100, "input_tokens_details": { "cache_write_tokens": 0, "cached_tokens": 0 }, "output_tokens": 20, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 120 } } ``` 6. **Continue the conversation.** Send the next turn with `previous_response_id` set to the `id` of the response you just got. You send only the new turn: the API keeps the conversation history. * curl Set `RESPONSE_ID` to the `id` from the reply in step 4. It starts with `resp_`. ```bash curl https://api.helping.io/v1/responses \ -H "Authorization: Bearer $HELPINGIO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "previous_response_id": "'"$RESPONSE_ID"'", "input": "And again." }' ``` * Node Add this to the end of the script from step 4: ```js const next = await client.responses.create({ previous_response_id: response.id, input: "And again.", }); console.log(next.output_text); ``` * Python Add this to the end of the script from step 4: ```python next_response = client.responses.create( previous_response_id=response.id, input="And again.", ) print(next_response.output_text) ``` ## If your first request fails [Section titled “If your first request fails”](#if-your-first-request-fails) These are the refusals a new account most often meets. None of them creates or charges anything, and retrying without the fix gets the same answer. | Status | `code` | Meaning | | ------ | ------------------------- | ------------------------------------------------------------------------------------------------------------- | | `401` | `invalid_api_key` | The Authorization header is missing, malformed, or names a key that is not usable; check the key. | | `402` | `payment_method_required` | The organization has no payment method on file, so no request can be served; add one in the billing settings. | | `429` | `insufficient_quota` | The organization has no active plan, or no credit balance to pay for the request; the message says which. | * **`invalid_api_key`**: check that `HELPINGIO_API_KEY` holds the whole secret you copied when you created the key, and that the key is not revoked. A lost secret cannot be shown again; rotate the key or create a new one. * **`payment_method_required`**: the organization’s owner adds a card in **Settings → Billing**: **Start a plan** if there is no plan yet, otherwise **Manage billing**. * **`insufficient_quota`**: the message says whether the organization has no active plan or no credit balance. The owner starts the plan, or buys credits, in **Settings → Billing**. Every other code is on [Errors](/gateway/errors/). ## Where next [Section titled “Where next”](#where-next) * [Create a response](/gateway/create-a-response/) covers every request field and how input becomes a turn. * [Streaming](/gateway/streaming/) for tokens as they arrive. * [Function calling](/gateway/function-calling/) to give the model tools your code executes. * [Errors](/gateway/errors/) and [Rate limits](/billing/rate-limits/), before you ship. # API keys > Create, check, rotate and revoke the sk_ API keys that authenticate every request, and what each failure looks like. Every `/v1` request authenticates with an API key sent as a bearer token: ```http Authorization: Bearer sk_… ``` Keys are managed in the app at [app.helping.io](https://app.helping.io), under **Settings → API Keys**, not through `/v1`. Owners and admins of the organization can create, rotate and revoke keys; members cannot open the page. ## Create a key [Section titled “Create a key”](#create-a-key) 1. Open **Settings → API Keys**. 2. Under **Create a key**, enter a **Name** of at least three characters that says which integration will use it, then select **Create key**. 3. Copy the secret with **Copy**. It starts with `sk_` and is shown once: it cannot be recovered after you select **Dismiss** or leave the page. 4. Store it where your code reads it, for example: ```bash export HELPINGIO_API_KEY="sk_…" ``` The key is issued to you and belongs to your organization. Spend is attributed per key, so give each integration its own. ## Check a key [Section titled “Check a key”](#check-a-key) Listing models proves a key works without running a model, so it costs nothing and is not rate limited. ```bash curl https://api.helping.io/v1/models \ -H "Authorization: Bearer $HELPINGIO_API_KEY" ``` A working key answers `200` with the list of models. A key the API does not accept, because it is mistyped, incomplete or revoked, answers `401`: ```json { "error": { "message": "invalid API key", "type": "invalid_request_error", "param": null, "code": "invalid_api_key" } } ``` A request with no `Authorization` header, or one that is not `Bearer` followed by a key, gets the same `401` with the message `missing or malformed Authorization header`. ## Rotate a key [Section titled “Rotate a key”](#rotate-a-key) Rotating a key gives it a new secret. The key keeps its name, its spend history and its conversations. 1. Open **Settings → API Keys** and find the key in the list. 2. Open the key’s actions menu and select **Rotate**. 3. Under **Keep the current secret working for**, choose **Stop it now**, **1 hour**, **24 hours** or **7 days**. A grace period lets your clients switch to the new secret without downtime. 4. Select **Rotate key**, then copy the new secret. It is shown once. ## Revoke a key [Section titled “Revoke a key”](#revoke-a-key) Revoking a key stops it immediately and cannot be undone. Its spend history stays visible in the app, and conversations it started can no longer be continued by anyone. 1. Open **Settings → API Keys** and find the key in the list. 2. Open the key’s actions menu and select **Revoke**. 3. Select **Revoke key** to confirm. Keys do not expire on a schedule; revoke or rotate them instead. Keep keys out of clients you do not control A key is a long-lived bearer secret. Send it only from servers you operate. If a key is exposed, rotate it with **Stop it now** or revoke it. ## Conversation ownership [Section titled “Conversation ownership”](#conversation-ownership) A conversation is bound to the API key that started it. Only that key can continue, retrieve, or delete it, and a different key in the same organization gets the same `404` as an unknown id. Plan for this when you have several keys: the key that starts a conversation is the key that must finish it. ## Authentication failures [Section titled “Authentication failures”](#authentication-failures) * **`401` `invalid_api_key`**: the header is missing or malformed, or the key is unknown or revoked. Check the key as in [Check a key](#check-a-key). * **`403` `access_denied`**: the key is valid but is not entitled to what the request asked for. Ask an administrator of your organization. * **`503` `auth_unavailable`**: the key could not be validated for a moment. It comes with `Retry-After: 1`, never as a `401`, and the SDKs retry `503` automatically, so a short outage never looks like a bad key. # Clients and SDKs > Which clients work against the helping.io API, and how to point one at it. This API serves the Responses API, so an OpenAI Responses client works against it once you change the base URL and the key. If your app uses Chat Completions, migrate it to Responses first and it will work here unchanged. ## Clients that work [Section titled “Clients that work”](#clients-that-work) The API’s compatibility promise is that **the official OpenAI SDKs and Open Responses clients can call it and parse the result** without a custom integration: a non-streaming create, a next turn through `previous_response_id`, a stream read by the SDK’s own event reader, the function-calling loop, and the Conversations API all work the way they do against OpenAI. What is deliberately missing is listed under [Not implemented](/gateway/compatibility/#not-implemented), and nothing else is. ## Set up a client [Section titled “Set up a client”](#set-up-a-client) Point the client’s base URL at `https://api.helping.io/v1` and use your `sk_` key as the API key. The examples on these pages read the key from the `HELPINGIO_API_KEY` environment variable; see [API keys](/getting-started/authentication/) to create one. * curl Every request sends the key as a bearer token to the same base URL: ```bash curl https://api.helping.io/v1/models \ -H "Authorization: Bearer $HELPINGIO_API_KEY" ``` * Node ```bash npm install openai ``` ```js import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.helping.io/v1", apiKey: process.env.HELPINGIO_API_KEY, }); ``` * Python ```bash pip install openai ``` ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.helping.io/v1", api_key=os.environ["HELPINGIO_API_KEY"], ) ``` The Node and Python examples on the other pages use a `client` built this way. ## Chat Completions [Section titled “Chat Completions”](#chat-completions) **A Chat Completions app needs migrating first.** This API speaks the Responses API, so `/v1/chat/completions` answers a `404` today. OpenAI’s [guide on moving to Responses](https://developers.openai.com/api/docs/guides/migrate-to-responses) covers the changes, and the same code works here once migrated. ```http POST /v1/chat/completions ``` ```json { "error": { "code": "resource_not_found", "message": "Unknown request URL: /v1/chat/completions. This surface serves /v1/responses, /v1/conversations and /v1/models (Open Responses). Chat Completions is not supported.", "param": null, "type": "not_found_error" } } ``` # Create a response > POST /v1/responses creates one turn of a conversation. This page covers the request fields a turn needs, how input becomes a turn, and the turns that follow. `POST /v1/responses` creates a response, which is one turn of a conversation. To continue that conversation, send the next turn with `previous_response_id` set to the id you got back. ## A first turn [Section titled “A first turn”](#a-first-turn) * curl ```bash curl https://api.helping.io/v1/responses \ -H "Authorization: Bearer $HELPINGIO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-sonnet-5", "input": "Say hello in exactly 3 words." }' ``` * Node ```js import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.helping.io/v1", apiKey: process.env.HELPINGIO_API_KEY, }); const response = await client.responses.create({ model: "anthropic/claude-sonnet-5", input: "Say hello in exactly 3 words.", }); console.log(response.output_text); ``` * Python ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.helping.io/v1", api_key=os.environ["HELPINGIO_API_KEY"], ) response = client.responses.create( model="anthropic/claude-sonnet-5", input="Say hello in exactly 3 words.", ) print(response.output_text) ``` The reply is a response object. The fields you will read most are `id`, `status`, `output`, and `usage`: ```json { "background": false, "completed_at": 1767225600, "conversation": null, "created_at": 1767225600, "error": null, "frequency_penalty": 0.0, "id": "resp_ZXhhbXBsZS9yZXNwL2ZpcnN0LXR1cm4AAAAAAAAAAAAAAAAAAAAAAA", "incomplete_details": null, "instructions": null, "max_output_tokens": null, "max_tool_calls": null, "metadata": null, "model": "anthropic/claude-sonnet-5", "object": "response", "output": [ { "content": [ { "annotations": [], "logprobs": [], "text": "Hello there, friend.", "type": "output_text" } ], "id": "msg_00000000000000000000000000000001", "role": "assistant", "status": "completed", "type": "message" } ], "parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, "reasoning": null, "safety_identifier": null, "service_tier": "default", "status": "completed", "store": true, "temperature": 1.0, "text": { "format": { "type": "text" } }, "tool_choice": "auto", "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled", "usage": { "input_tokens": 100, "input_tokens_details": { "cache_write_tokens": 0, "cached_tokens": 0 }, "output_tokens": 20, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 120 } } ``` The full object also echoes the sampling and tool fields you sent, or their defaults. Fields are `snake_case` throughout. ## Continuing a conversation [Section titled “Continuing a conversation”](#continuing-a-conversation) Send the next user turn with `previous_response_id`. The API keeps the conversation history, so you send only the new turn, never the history. POST /v1/responses ```json { "input": "And again.", "previous_response_id": "resp_ZXhhbXBsZS9yZXNwL2ZpcnN0LXR1cm4AAAAAAAAAAAAAAAAAAAAAAA" } ``` Two rules apply to the turns after the first: * **A conversation is bound to the key that started it.** A next turn sent with a different API key answers the same `404` as an unknown id, even inside the same organization. A rotated key keeps its conversations; revoking a key ends them for everyone. * **`model` carries forward.** Omit it on a later turn and the conversation’s model is used. Supply it to switch models for that turn. See [Choose a model](/gateway/choose-a-model/). ## How input becomes a turn [Section titled “How input becomes a turn”](#how-input-becomes-a-turn) `input` carries exactly one thing: the current user turn. | You send | What happens | | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | A bare string | One user message | | `message` items with role `user` | Their text joins the user message. Several user items concatenate in wire order, separated by a blank line | | A `message` item with role `assistant` | `400`. Continue the conversation with `previous_response_id` instead | | A `message` item with role `system` or `developer` | `400`. Use `instructions` for per-request steering | | A `function_call_output` item | The tool-result path. See [Function calling](/gateway/function-calling/) | | A `function_call` item with its matching `function_call_output` | Carried onto the new conversation as a real tool exchange. The call’s arguments and the result’s text become context; the tool’s name is not sent | | A `function_call` with no matching output, or an output with no call | `400` naming which item and which half is missing | | A `reasoning` item | Stored and readable, never sent to the model | | An item-reference item | `400`. A first turn does not resolve item references, so send the item itself | Multi-part user content keeps its text parts (`input_text`, `output_text`, `text`), joined with newlines. Every other part fails loudly rather than vanishing: `input_image`, `input_file`, `input_video`, `summary_text`, `reasoning_text` and `refusal` parts are each a `400` naming the part. Only text reaches the model, so send what it needs as an `input_text` part: a file’s text, or a description of an image. A user item whose content is empty is skipped. An input array that yields no user text at all is a `400`. When that input holds a `function_call_output`, the message says so instead, because a new conversation has no tool call waiting for a result. Every refusal in this section sets `param` to `input`, and a refusal about one item names its index and type in the message. Three more refusals apply to the whole request body: * A string anywhere in the body that contains a NUL character (U+0000) is a `400` naming where it is. Remove or replace the character before sending. * `conversation` and `previous_response_id` together are a `400`: each names the conversation a response continues, so send one of them. * A malformed `previous_response_id` is a `400`, and one that names no response your key can see is a `404`. ## Request fields [Section titled “Request fields”](#request-fields) The fields a turn needs, and what each one does. Three things are true of this surface that are worth knowing before you read the tables: unknown fields are ignored rather than rejected, so a newer SDK never fails here; a field that parses but is not applied is listed as **accepted, ignored**, so nothing is silently half-supported; and the response object echoes back `model`, `previous_response_id`, `metadata`, `tools`, `tool_choice`, `truncation`, `instructions`, and the effective `max_output_tokens`. All of them survive a retrieve, with one difference worth knowing when you answer a tool call, described under [what a retrieve reports](/gateway/compatibility/#behavior-that-differs-from-what-you-might-expect). The request-only fields it does not echo report the specification’s default value regardless of what you sent, except the generation settings, which report the value the response was generated with. ### Required [Section titled “Required”](#required) | Field | Notes | | ------- | -------------------------------------------------------- | | `model` | Required on a first turn. Carries forward on later turns | | `input` | Required on a new turn. String or item array, as above | ### Conversation [Section titled “Conversation”](#conversation) | Field | Notes | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `previous_response_id` | Continue the conversation that produced this response | | `instructions` | Per-request steering, echoed, never inherited | | `metadata` | Stored with the response and returned when you retrieve it. Up to 16 pairs of key and value, keys up to 64 characters, values strings up to 512 characters | | `safety_identifier` | An opaque id for your end user, up to 64 characters. Sent where the model’s provider has a per-end-user field, as a keyed hash of your value, never the value itself, so one end user can be restricted without restricting your key. Echoed as you sent it | | `user` | Deprecated in favor of `safety_identifier`. Used as your end user’s id when `safety_identifier` is absent, and ignored when both are sent. It does not group requests for caching, and the response does not echo its value | ### Tools [Section titled “Tools”](#tools) | Field | Notes | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tools` | Your function tools. Echoed. See [Function calling](/gateway/function-calling/) | | `tool_choice` | `none`, `auto`, `required`, `{"type": "function", "name": …}`, or an `allowed_tools` object. Echoed. A function choice needs a `name`; `allowed_tools` takes mode `auto` or `required` and a `name` on each entry; `required` needs at least one tool; and naming a tool that is not in your `tools` is a `400` naming the tool | ### Output [Section titled “Output”](#output) | Field | Notes | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `stream` | `true` for server-sent events. Defaults to `false`. See [Streaming](/gateway/streaming/) | | `stream_options` | `include_obfuscation`, on by default, pads each streamed delta event so its size does not track the length of the text in it. Set it to `false` to turn the padding off | | `max_output_tokens` | Minimum 16. Echoed as the effective value, which can be lower than you asked for, and carried forward to a later turn that omits it | | `truncation` | `disabled` (the default) refuses a turn that does not fit the model’s context window, with the code `context_length_exceeded`; `auto` drops the oldest turns so it fits. See [How `truncation` differs](/gateway/compatibility/#how-truncation-differs) | ### Everything else [Section titled “Everything else”](#everything-else) Generation settings, the fields that are accepted but advisory, and the fields that are refused are listed on [Compatibility and versioning](/gateway/compatibility/#not-implemented). The [API reference](/reference/api/) carries every field of the request body. ## Headers [Section titled “Headers”](#headers) | Header | Notes | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Authorization: Bearer sk_…` | Required. See [API keys](/getting-started/authentication/) | | `Content-Type: application/json` | Recommended on `POST`. The body is read as JSON whatever this header says | | `Idempotency-Key` | Optional, and the official SDKs do not send it on their own: pass it in the request options. Visible ASCII, at most 255 bytes. A retry with the same key and body returns the first response, so the model produces output and bills once; the same key with a different body is a `409`. Remembered for 24 hours. See [Idempotency](/gateway/errors/#idempotency) | ## Waiting for the result [Section titled “Waiting for the result”](#waiting-for-the-result) A non-streaming request waits up to 90 seconds. A response that is not finished by then comes back in progress: the same `id`, a `status` of `queued` or `in_progress`, an empty `output`, and HTTP `200`. It is not an error, and it is not the end of the work. Poll `GET /v1/responses/{response_id}` for the result. ```json { "background": false, "completed_at": null, "conversation": null, "created_at": 1767225600, "error": null, "frequency_penalty": 0.0, "id": "resp_ZXhhbXBsZS9yZXNwL2luLXByb2dyZXNzLWF0LXRoZS13YWl0LWJvdQ", "incomplete_details": null, "instructions": null, "max_output_tokens": 1024, "max_tool_calls": null, "metadata": null, "model": "anthropic/claude-sonnet-5", "object": "response", "output": [], "parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, "reasoning": null, "safety_identifier": null, "service_tier": "default", "status": "in_progress", "store": true, "temperature": 1.0, "text": { "format": { "type": "text" } }, "tool_choice": "auto", "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled" } ``` For work that takes longer than a few seconds, or when you want tokens as they arrive, use [streaming](/gateway/streaming/), which is bounded at 300 seconds instead. Closing a non-streaming request cancels the response If your client disconnects while a non-streaming request is waiting, the response is canceled and you are billed only for what had already been produced. Streaming is the other way around: closing a stream does not cancel the response, and you can still retrieve the result by id. # Errors > The error envelope, every status and code the helping.io API returns, what each one asks you to do, and the timeouts and retry rules to design for. Every error on `/v1` is the standard Responses API envelope, including the ones raised before your request reaches the endpoint, such as a bad key. ```json { "error": { "message": "missing or malformed Authorization header", "type": "invalid_request_error", "param": null, "code": "invalid_api_key" } } ``` * `message` is human-readable and safe to log. * `type` follows from the HTTP status, so SDK retry logic can branch on it. The one exception is `insufficient_quota`: a `429` that carries type `insufficient_quota`, as OpenAI’s own quota refusal does, so a client that handles that refusal from OpenAI handles this one. * `code` is the specific condition, always lowercase, and is the field to switch on in your own code. * `param` names the request field a refusal is about, when it is about one: `model`, `input`, `metadata`, `tool_choice`, `max_output_tokens` and the rest are reported as you sent them, so your error handler can branch on the field instead of reading the message. It is `null` for everything else, including a body that will not parse, a bad key, and every rate limit. Read it when it is there; do not require it. ## Status to type [Section titled “Status to type”](#status-to-type) | HTTP status | `type` | | ----------- | ----------------------- | | `400` | `invalid_request_error` | | `401` | `invalid_request_error` | | `402` | `invalid_request_error` | | `403` | `permission_error` | | `404` | `not_found_error` | | `409` | `invalid_request_error` | | `429` | `insufficient_quota` | | `429` | `rate_limit_error` | | `500` | `api_error` | | `502` | `api_error` | | `503` | `overloaded_error` | | `504` | `api_error` | Any other `4xx` carries `invalid_request_error`. ## Codes [Section titled “Codes”](#codes) ### Authentication, authorization, and billing [Section titled “Authentication, authorization, and billing”](#authentication-authorization-and-billing) | Status | `code` | `Retry-After` | Meaning | | ------ | ------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------- | | `401` | `invalid_api_key` | No | The Authorization header is missing, malformed, or names a key that is not usable; check the key. | | `403` | `access_denied` | No | The key is valid but is not entitled to what the request asked for; ask your administrator. | | `429` | `insufficient_quota` | No | The organization has no active plan, or no credit balance to pay for the request; the message says which. | | `402` | `payment_method_required` | No | The organization has no payment method on file, so no request can be served; add one in the billing settings. | | `503` | `auth_unavailable` | Yes | Your credentials could not be checked this instant, which is not a verdict on the key; retry after the Retry-After header. | `insufficient_quota` means your organization cannot pay for the request: it has no active plan, or it has no credit balance. The message says which. It is not a rate limit, and retrying does not change the answer. ### Request validation [Section titled “Request validation”](#request-validation) Nothing is created and nothing is charged when one of these comes back. | Status | `code` | `param` | Meaning | | ------ | ------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `invalid_input` | The offending field | The request cannot be honored as written; the message names the field and the rule it broke. | | `400` | `context_length_exceeded` | Always null | The conversation plus this turn's input is larger than the model's context window; start a new conversation, send less, or set truncation to auto to let the oldest turns be dropped. | | `400` | `model_policy_violation` | The offending field | Your organization's policy does not allow the model this request selected; the message names the model and the rule that refused it, and param is \`model\`. | | `404` | `resource_not_found` | Always null | The path or the previous response named by this request is not available to you. | ### Conflicts [Section titled “Conflicts”](#conflicts) | Status | `code` | Meaning | | ------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `409` | `idempotency_conflict` | This Idempotency-Key was already used within the last 24 hours, and what it made cannot be returned for this request: the body differs, or this API key cannot read it. Nothing was created. | | `409` | `idempotency_in_progress` | The first request with this Idempotency-Key is still being processed. Nothing was created; send the request again shortly. | See [Idempotency](#idempotency). ### Capacity and limits [Section titled “Capacity and limits”](#capacity-and-limits) Entries marked `Retry-After` carry that header; honor it. See [Rate-limit headers](/billing/rate-limits/#rate-limit-headers) for the headers that let you avoid most of them. | Status | `code` | `Retry-After` | Meaning | | ------ | ----------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `429` | `rate_limit_exceeded` | Yes | This request exceeded a rate limit that applies to you; retry after the Retry-After header. | | `429` | `stream_limit_exceeded` | Yes | You are already holding the maximum number of open streams; nothing was started, so retry once one ends. | | `429` | `scope_budget_exceeded` | No | A spend budget set for your organization, a member, or an API key is exhausted; the message states which. | | `429` | `insufficient_credits` | No | The organization's credit balance is exhausted; top up in the billing settings. | | `503` | `gateway_overloaded` | Yes | The API could not accept this request and nothing was created; retry after the Retry-After header, and retrying with the same Idempotency-Key still deduplicates. | | `503` | `database_unavailable` | Yes | The request could not be served this instant and nothing was created; retry after the Retry-After header. | ### Model provider failures [Section titled “Model provider failures”](#model-provider-failures) | Status | `code` | Meaning | | ------ | ----------------------- | ---------------------------------------------------------------------------------------------- | | `429` | `provider_rate_limited` | The model provider rate limited this request; retry, honoring Retry-After when one is present. | | `502` | `provider_overloaded` | The model provider reported that it is overloaded; retry shortly. | | `502` | `provider_unavailable` | The model provider did not complete the request; retry shortly. | A provider that rate limits your request is refused with `429`, carrying the envelope the table above describes: Response ```json { "error": { "code": "provider_rate_limited", "message": "The upstream model provider rate limited this request; retry later.", "param": null, "type": "rate_limit_error" } } ``` Non-transient provider failures do not use the error envelope. They arrive as a `200` whose response object has `status: "failed"` and an `error` with a `code` and a message. See [Failed responses](#failed-responses). ### Server faults [Section titled “Server faults”](#server-faults) | Status | `code` | Meaning | | ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `504` | `request_timeout` | This request's wait window ended before a result was available and no response could be read back for it; quote the x-request-id response header when reporting it. | | `500` | `internal_server_error` | The request could not be completed; quote the x-request-id response header when reporting it. | | `500` | `database_error` | The request could not be completed; quote the x-request-id response header when reporting it. | | `500` | `item_unreadable` | The item is stored but could not be read, so it cannot be returned. A list of items names it in helpingio:unreadable and returns the others. | `item_unreadable` answers a request for one item that is stored but could not be read. A list of items does not fail because of it: the list names the item in `helpingio:unreadable` and returns the others. ## Failed responses [Section titled “Failed responses”](#failed-responses) A response that fails after it has started is not an HTTP error. It is a response object with `status: "failed"` and a populated `error`: ```json { "id": "resp_…", "object": "response", "status": "failed", "output": [], "error": { "code": "provider_client_error", "message": "The upstream model provider rejected the request." }, "usage": { "input_tokens": 100, "input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 }, "output_tokens": 0, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 100 } } ``` The same shape arrives inside a streaming `response.failed` event. `usage`, when present, shows what the failed response consumed; read it with a presence check. `error.code` on this channel is a closed set, and a failure this surface does not name separately arrives as `run_failed`. Switch on the code; the message is written for a person to read and may be reworded. | `code` | Meaning | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `budget_exceeded` | The spending limit for a single response was reached. The message states what was spent against the limit, in credits. | | `canceled` | The request was canceled before it finished. | | `end_user_blocked` | The model provider refuses requests for the end user this request was sent for, named by its safety\_identifier or user. Sending it again will not succeed for that end user. | | `insufficient_credits` | The organization ran out of credits while the request was running. Add credits and send it again. The message states the balance, in credits. | | `org_daily_budget_exceeded` | The organization's daily spending limit was reached. The message states what was spent against the limit, in credits. | | `provider_client_error` | The upstream model provider rejected the request. The request reached the provider and came back refused. | | `provider_error` | The upstream model provider returned an unexpected error. | | `provider_overloaded` | The upstream model provider is overloaded. Retry later. | | `provider_rate_limited` | The upstream model provider rate limited the request. Retry later. | | `provider_unavailable` | The upstream model provider failed to complete the request. Retry later. | | `run_failed` | The request did not complete. Send it again, and contact support with the response id if it keeps failing. | | `scope_budget_exceeded` | A spending limit set for this period was reached. The message states what was spent against the limit, in credits. | Five of these codes are in the HTTP tables above as well, because where a failure reaches you depends on how you asked: * A non-streaming `POST /v1/responses` answers an upstream provider failure with an HTTP error, so `provider_rate_limited`, `provider_overloaded` and `provider_unavailable` arrive there with a status. * A streaming request answers `200` before it can know, so the same failure arrives in the response object instead. So does a `GET /v1/responses/{response_id}`, which answers what happened rather than refusing. * `scope_budget_exceeded` is an HTTP refusal when the limit is already spent as the request arrives, and a failed response when it runs out while the request is running. * Out of credits as the request arrives is the HTTP refusal `insufficient_quota`. `insufficient_credits` is the later, rarer case: an HTTP refusal when the request was accepted but the balance could not pay to start it, and a failed response when credits run out while the request is running. Handle both codes the same way. A spending failure in a failed response states your own figures in credits, to one decimal place: what was spent against the limit, or the balance. Switch on the code; the figures are for the person reading the message. ## Errors during a stream [Section titled “Errors during a stream”](#errors-during-a-stream) Once a stream is open the HTTP status has already been sent, so every later failure is in-band: * `response.failed` carries the failed response object above. * `response.incomplete` means the response stopped early, for example at `max_output_tokens`. Read `incomplete_details.reason`. * An `error` event carries a failure that belongs to neither: the stream itself could not deliver a result. The codes an `error` event can name: | `code` | Meaning | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `response_unresolved` | The stream ended before a result was ready. The response may still finish: retrieve it by the id response.created carried. Retrying without the same Idempotency-Key starts a second response. | ## Timeouts and retries [Section titled “Timeouts and retries”](#timeouts-and-retries) The rate limits, and the headers that report them, are on [Rate limits](/billing/rate-limits/). ### Timeouts [Section titled “Timeouts”](#timeouts) | Bound | Value | What happens | | ------------------ | ---------------- | ------------------------------------------------------------------------ | | Non-streaming wait | 90 seconds | The in-progress response object is returned with `200`. Poll by id | | Stream ceiling | 300 seconds | The stream closes with its terminal event. Retrieve by id | | Stream keep-alive | every 15 seconds | An SSE comment frame keeps intermediaries from timing out a quiet stream | ### Idempotency [Section titled “Idempotency”](#idempotency) `POST /v1/responses` honors the `Idempotency-Key` header, and so do the requests that create a conversation, update one and add items to one. The official SDKs do not send it on their own, so pass it yourself in the request options and reuse it when you retry: * A retried request with the same key and the same body returns the first request’s response, so the model produces output and bills once. That includes a request that sends `function_call_output` items. * The same key with a different body is a `409` with the code `idempotency_conflict`, and nothing is created. * A retry that arrives while the first request is still being processed is a `409` with the code `idempotency_in_progress`. Nothing is created; send it again shortly. * A request that is refused, for example with a `400`, does not use up its key, so you can correct it and send it again with the same key. * A key is remembered for 24 hours. After that, the same key starts a new request. * Keys belong to your organization, and each endpoint keeps its own keys. A retry from a different API key than the first request’s is a `409` with the code `idempotency_conflict` when that key cannot read what the first request made. On a conversation’s own endpoints, the same key and body sent to a different conversation is a `409` with the code `idempotency_conflict`. * Keys are printable ASCII, spaces included, at most 255 bytes. A longer key, or one with any other character, is a `400`. Empty keys are ignored. * Without a key, a retry after a delivery failure creates a second response and a second charge. ### Backing off well [Section titled “Backing off well”](#backing-off-well) * Honor `Retry-After` on every `429` and `503`. Use exponential backoff with jitter when the header is absent. * Watch the remaining-requests header above and slow down before it reaches zero. * Send an `Idempotency-Key` on every create so retries are safe. * Prefer streaming for long work. A stream is bounded at 300 seconds, a non-streaming wait at 90.