Skip to content
Heimdallm
Marketplace/Documentation
Documentation · API reference

Batch API

POST https://api-staging.impossiblecarrot.cc/api/v1/batches
GET  https://api-staging.impossiblecarrot.cc/api/v1/batches
GET  https://api-staging.impossiblecarrot.cc/api/v1/batches/{id}
POST https://api-staging.impossiblecarrot.cc/api/v1/batches/{id}/cancel

Requires a bearer API key. A batch is a set of requests to one endpoint, submitted together and run in the background, for work that does not need an answer within the request that asked for it.

A batch changes how requests are submitted and nothing about how they are served. Every request in it is routed, held against your balance, settled and recorded as the same body posted to the endpoint would be, under the key that submitted the batch and its spend limit, and it carries a generation of its own. A batch earns no discount: it costs what its requests cost one by one.

Submit a batch

endpoint names the request shape every item is written in: /v1/chat/completions, /v1/responses or /v1/messages. Each item is a custom_id of your own, unique within the batch, and the body that endpoint takes. The route answers 202 once the batch is stored, with status: "validating"; nothing has run or been charged yet.

curl ${API_BASE_URL}/batches \
  -H "Authorization: Bearer $HEIMDALL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "/v1/chat/completions",
    "model": "acme/alpha-v1",
    "completion_window": "24h",
    "requests": [
      { "custom_id": "req-0001", "body": { "messages": [{ "role": "user", "content": "Summarize the first report." }] } },
      { "custom_id": "req-0002", "body": { "messages": [{ "role": "user", "content": "Summarize the second report." }] } }
    ]
  }'
{
  "id": "batch_5d1f0c9e2a7b4e36b8a4c1d27e90f3a5",
  "object": "batch",
  "endpoint": "/v1/chat/completions",
  "model": "acme/alpha-v1",
  "completion_window": "24h",
  "status": "validating",
  "created_at": 1786881571,
  "expires_at": 1786967971,
  "finalized_at": null,
  "request_counts": { "total": 2, "completed": 0, "failed": 0 },
  "results_file_id": null,
  "results": null,
  "error": null
}
MemberNotes
endpointRequired. One of /v1/chat/completions, /v1/responses, /v1/messages.
requestsRequired. 1 through 1000 items of { custom_id, body }. A custom_id is up to 128 printable characters and appears once in the batch.
modelOptional. A model for every request. A body that names none takes it; a body that names a different one fails on its own.
providerOptional. only and allow_fallbacks, applied to every request whose body states no provider of its own.
completion_windowOptional. 24h, which is also the default.

A submission may be up to 32 MiB, which is more than an ordinary request body. stream is not accepted in a body, because every request is answered in full, and a chat body cannot ask for store: true.

Bodies are checked one at a time

The envelope is checked as a whole: a repeated custom_id, an endpoint the API does not batch or a missing requests refuses the submission with 400. A body is checked against the chosen endpoint's schema as the batch is accepted, and one that does not parse fails on its own. It is reported in the results with the error the live route would have answered, and counted in request_counts.failed; the rest of the batch runs. A batch in which every body fails ends failed with the reason in error.message.

How the requests run

Batches of an account run oldest first, and at most eight of an account's requests are in flight at once, across every gateway. Requests are sent in submission order, and what each does is what the same body posted to its endpoint does:

  • It is routed over the live catalogue and the workspace's routing defaults as they stand when the request is sent, and held against the balance of the key's account.
  • A request the account cannot fund, or that no endpoint serves, fails on its own with that answer, and charges nothing.
  • A key that was disabled, expired or moved out of its workspace after it submitted the batch authorizes nothing: its remaining requests fail with 401.
  • The spend limit of the submitting key applies to each request as it is sent.
  • The X-Title and HTTP-Referer headers of the submission name the application every request is attributed to.

A gateway that stops while a batch runs does not lose it. Another gateway takes the account over once the first one's claim runs out and settles whatever the first left. A request that was never charged is sent again; a request that was charged and whose reply the first gateway never recorded is not sent again, since that would charge twice. It fails with type: "server" and names the charge in error.request_id.

Statuses

StatusWhat it means
validatingStored and waiting for a gateway to take it.
in_progressBeing sent.
cancellingA cancellation was asked for; the requests in flight are being settled.
completedEvery request has an outcome, an error included.
failedNo request was runnable: every body failed validation.
expiredThe completion window closed first. Requests not yet sent were not sent.
cancelledThe batch was cancelled. Requests that had not finished were not charged for.

The last four are terminal. When the window closes, a request already in flight is allowed to finish, and the ones not yet sent are listed in the results as batch_expired.

Read a batch

GET /batches/{id} returns the batch with its counts. request_counts.completed and .failed are the requests that ended in each way; a request stopped or expired before it was sent is in neither, so total less the two is the number the batch did not run.

curl https://api-staging.impossiblecarrot.cc/api/v1/batches/batch_5d1f0c9e2a7b4e36b8a4c1d27e90f3a5 \
-H "Authorization: Bearer $HEIMDALL_API_KEY"

GET /batches lists the account's batches, newest first. status may be repeated to include several, created_after and created_before take unix seconds, a date or an ISO 8601 instant and exclude it, and limit is 1 through 100 and defaults to 20. To read the next page, pass last_id as after; there is no offset. A list carries results: null on every batch.

curl "https://api-staging.impossiblecarrot.cc/api/v1/batches?status=completed&status=failed&limit=20" \
-H "Authorization: Bearer $HEIMDALL_API_KEY"

Results

When a batch ends it writes its results to a file in your file store, named by results_file_id, with one JSON line per request in submission order, and clears the requests and replies it held. Read the file from GET /files/{id}/content. A retrieved batch carries the same lines inline as results for as long as the file exists.

{"id":"breq_7e0c1a94d3b84f2a9c56e81d0b3f2a47","custom_id":"req-0001","response":{"status_code":200,"request_id":"gen-1786881609-aBcDeFgHiJkLmNoPqRsT","body":{"id":"gen-1786881609-aBcDeFgHiJkLmNoPqRsT","object":"chat.completion"}},"error":null}
{"id":"breq_a3c4e5f6071829304a5b6c7d8e9f0a1b","custom_id":"req-0002","response":null,"error":{"type":"invalid_request","message":"Invalid request: messages: Too small: expected array to have >=1 items","param":null,"code":400,"request_id":null}}

Exactly one of response and error is set on a line.

  • response.body is what the endpoint answered, in the shape its route answers in.
  • response.request_id is the id the request was billed under, the value X-Generation-Id carries on a live request, so a line can be matched to its generation and its ledger entry.
  • error.type and error.code are those of the answer the live route would have given. batch_cancelled and batch_expired mark a request the batch ended without sending, with a code of null.
  • A request whose reply would not fit is failed rather than cut short. A single reply may be up to 1 MiB and all of a batch's replies together fit one stored file; a request the file has no room left for is failed with results_full and is not sent, so it is never charged for a reply with nowhere to go.

The results file is a stored file like any other: it expires 48 hours after the batch ends, and from then on results is null, the file answers 404 and the batch still reports how it ended and its counts. Keep your own copy of anything you need for longer.

Cancel a batch

curl -X POST https://api-staging.impossiblecarrot.cc/api/v1/batches/batch_5d1f0c9e2a7b4e36b8a4c1d27e90f3a5/cancel \
-H "Authorization: Bearer $HEIMDALL_API_KEY"

Dispatch stops at once and the requests in flight are cut. The batch is cancelling until what ran has settled, then cancelled. A request that completed keeps its reply and its charge; one that was cut or had not been sent is listed as batch_cancelled and is not charged, unless it was charged before it was cut, in which case error.request_id names the charge. Asking again while the batch is stopping, or after it has stopped, returns it unchanged; a batch that ended another way answers 409.

Refusals

StatusWhen
400The envelope did not parse, a custom_id repeats, or a list parameter is outside its range.
401The key is missing, unknown, revoked or expired.
404No such batch on your account. An unknown id, a malformed one and another account's give the same answer.
409The batch already ended as completed, failed or expired, so it cannot be cancelled.
413The submission is larger than 32 MiB.
503The keys the batch's requests are stored encrypted under could not be reached. Nothing was stored; repeat the submission shortly.
429The account already has 10 batches waiting or running, or the key is over its request rate. metadata carries limit and active for the first.

Your batches are your data

The requests a batch holds, and the replies it collects, are kept only while the batch needs them. They are cleared in the same step that writes the results file, and the file expires 48 hours later. What remains of a batch is a record of what ran: its status, its counts, the custom_id of each request and the id it was billed under. The account export carries every batch and its requests, with the request bodies for as long as the batch runs, and every batch is erased with the account when a deletion runs.

While the batch runs, each request and each reply is stored encrypted, under a key that is kept outside the database and destroyed no more than 50 hours after the value was written, so a backup of the database taken in that time cannot give them back afterwards. If the key for a request is lost before its turn, the request is not sent and is not charged: its result is a server error saying the stored request could not be read. If the key for a reply is lost after the request was charged and answered, its result is a server error whose request_id names the charge, and the request is not run again.

DiscoverFind your next modelModel libraryAll models, versions, prices and capabilitiesBenchmarksMeasured quality, cost and time by serving configurationShowcaseImages, video and 3D made on the marketplaceProvidersCompare independent inference providersActivityMarketplace traffic and usageNetwork statusAvailability, latency and endpoint healthChatConversations, model settings and filesImage, video & 3DGenerate images, video and 3D assetsScene studioStage a shot in 3D, direct it and render it as a videoCoding playgroundBuild software with an AI agent on a machine of your ownOverview & routingIdentity, workspaces and routing preferencesCredits & billingBalance, deposits, ledger and reserved fundsAPI keysKeys scoped to workspaces, limits and expirationOrganizationsTeams, members and shared workspacesRequest historyRequests, receipts, sandbox sessions and account historyReferralsInvitations and referral earningsPrivacyData settings, exports and deletionDocumentationQuickstart, SDKs, pricing and guidesAPI referenceInteractive endpoint reference