Docs
View as Markdown

Batch

POST /v1/batch runs up to 100 ordinary endpoint calls in one HTTP request. Each item runs through the same pipeline as a single call, so caching, refunds and prices are identical; the batch only adds one request in and one answer out, with a row per item that says what happened to it.

shell
POST https://api.onekey.dev/v1/batch
x-api-key: YOUR_API_KEY
content-type: application/json

When to use it#

  • You need several records at once (a list of profiles, the repos in a report, a page of links) and would rather make one round trip than a hundred.
  • You want mixed platforms in one call: a batch can hold any endpoints in any order.
  • You want per-item outcomes without the batch failing because one item did: a bad item becomes a row with an error, the others still run.

For the same query on several platforms, multi-platform search is the shorter form.

The request#

The body is {"requests": [...]} where each item names an endpoint and its parameters:

curl -X POST "https://api.onekey.dev/v1/batch" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "content-type: application/json" \
  -d '{"requests": [
    {"platform": "github", "endpoint": "profile", "params": {"username": "torvalds"}},
    {"platform": "hackernews", "endpoint": "item", "params": {"id": 8863}},
    {"platform": "github", "endpoint": "repo", "params": {"owner": "facebook", "repo": "no-such-repo-xyz"}},
    {"platform": "nope", "endpoint": "thing"}
  ]}'
Item fieldRequiredWhat it is
platform, endpointyesThe two path words of the endpoint, as in GET /v1/{platform}/{endpoint}.
paramsnoThe endpoint's query parameters. Values may be strings, numbers or booleans; each is sent as its string form (8863 becomes "8863", true becomes "true"). Objects, arrays and null are refused.

Limits: at most 100 items and a body of at most 1 MB (1,000,000 bytes). A body that is not JSON, has no requests list, is empty, or breaks either limit is a free 400 INVALID_REQUEST that names the problem (At most 100 requests per batch, Body larger than 1000000 bytes, requests[3] needs string platform and endpoint).

The response#

One row per item, in request order, whatever happened to each. This is the request above as recorded from the live API:

json
{
  "success": true,
  "data": [
    {
      "index": 0, "platform": "github", "endpoint": "profile", "params": { "username": "torvalds" },
      "status": 200,
      "data": { "author": { "id": "1024025", "username": "torvalds", "displayName": "Linus Torvalds", "followers": 325709, "...": "..." }, "computed": { "language": null } },
      "error": null, "credits_charged": 1, "cached": false
    },
    {
      "index": 1, "platform": "hackernews", "endpoint": "item", "params": { "id": "8863" },
      "status": 200,
      "data": { "content": { "id": "8863", "title": "My YC app: Dropbox - Throw away your USB drive", "...": "..." }, "author": { "username": "dhouston" }, "engagement": { "likes": 104 }, "computed": { "engagement_rate": null, "language": null } },
      "error": null, "credits_charged": 1, "cached": false
    },
    {
      "index": 2, "platform": "github", "endpoint": "repo", "params": { "owner": "facebook", "repo": "no-such-repo-xyz" },
      "status": 404, "data": null,
      "error": { "code": "not_found", "type": "NOT_FOUND", "message": "Not found", "doc_url": "https://onekey.dev/errors/NOT_FOUND" },
      "credits_charged": 0, "cached": false
    },
    {
      "index": 3, "platform": "nope", "endpoint": "thing", "params": {},
      "status": 404, "data": null,
      "error": { "code": "not_found", "type": "NOT_FOUND", "message": "Unknown endpoint", "doc_url": "https://onekey.dev/errors/NOT_FOUND" },
      "credits_charged": 0, "cached": false
    }
  ],
  "metadata": { "legs": 4, "succeeded": 2, "failed": 2, "credits_charged": 2, "request_id": "d38c75aa0add4bac8faa5b6273c6bc7f", "idempotent_replay": false },
  "error": null
}

A row#

FieldTypeMeaning
indexnumberPosition of this item in your request, from 0.
platform, endpointstringThe endpoint that ran.
paramsobjectThe parameters it ran with, every value as a string.
statusnumberThe HTTP status this call would have had on its own: 200, or 404, 422, 502...
dataobject, array or nullThe unified result when status is 200, exactly as the endpoint returns it (including its computed block where it has one). null otherwise.
errorobject or nullnull on success. Otherwise code, type, message, doc_url, as in any error.
credits_chargednumberWhat this item cost. 0 for a cache hit or a failure.
cachedbooleantrue when the answer came from cache.
paginationobjectOnly when the endpoint pages: its next_cursor, has_more, page_size. To get the next page, call the endpoint directly with that cursor (see Pagination).

Batch metadata#

FieldMeaning
legsHow many items ran (one per request item).
succeeded, failedHow many rows have status: 200, and how many do not.
credits_chargedThe sum of the rows' credits_charged.
request_idOne id for the whole batch, the same as the X-Request-Id header. Quote it if you contact support.
idempotent_replaytrue when the rows were replayed for an Idempotency-Key you sent before.

The top-level success is about the batch itself: a batch whose rows all failed is still a 200 with success: true, succeeded: 0, and each row saying why. Read the rows, not the envelope, to know how an item did.

Per-item isolation#

Every item is its own call. A row with an unknown endpoint (status: 404, Unknown endpoint), invalid parameters (status: 422), a record that does not exist (404) or a source failure (502) is just that row; the others run and are charged normally. Items run concurrently, at most 8 at a time, and share nothing but the request.

What it costs#

Only rows with status: 200 are charged, each at its endpoint's own price (endpoints cost 1 to 8 credits). A row served from cache is free (a cached response over 100 KB costs 0.2 credit, never more than the endpoint price); failed and not-found rows are refunded like single calls. metadata.credits_charged is the total, and the X-Credits-Remaining header carries your balance after the batch when anything was charged.

Before anything runs, Onekey adds up the most every item could cost (each endpoint's price, or for an endpoint with more than one data route the price of its dearest one) and refuses the whole call if that sum exceeds your balance (402 INSUFFICIENT_CREDITS) or the key's remaining credit limit (402 KEY_BUDGET_EXCEEDED). Nothing is run or charged, and the message states both numbers, for example This call could cost up to 15 credits; the account balance is 10 credits. Nothing was run or charged (for a key limit: ... this key has 4 of its 500-credit budget left ...). The check does not look at the cache, so items that would have been free cache hits still count at full price here. Keep enough balance for the worst case even when you expect most items to be cached.

Rate limit: a batch is one request against your per-minute allowance, however many items it holds (see Rate limits & headers). Rows served from cache each count against the separate cache-hit allowance.

Retrying safely#

Send an Idempotency-Key header and a repeat of the same batch within 24 hours is replayed for free: the same rows, metadata.credits_charged: 0, idempotent_replay: true, and the headers X-Idempotent-Replay: true and X-Cache: HIT. The key is bound to the exact body bytes, so reformatting the JSON makes it a different request (422 IDEMPOTENCY_KEY_REUSED). A batch that returns 200 is stored even when every row failed; a batch refused up front (400, 402) releases the key. Responses over 64 KB are returned but not stored, so a large batch's replay answers 409 IDEMPOTENCY_REPLAY_UNAVAILABLE; keep the rows on your side when a batch comes close to that size.

Streaming#

Send Accept: text/event-stream and rows arrive one by one as they settle instead of all at once at the end:

shell
curl -N -X POST "https://api.onekey.dev/v1/batch" \
  -H "x-api-key: YOUR_API_KEY" -H "accept: text/event-stream" -H "content-type: application/json" \
  -d '{"requests": [{"platform": "github", "endpoint": "profile", "params": {"username": "torvalds"}}, {"platform": "hackernews", "endpoint": "item", "params": {"id": 8863}}]}'
# event: leg
# data: {"index":0,"platform":"github","endpoint":"profile","params":{"username":"torvalds"},"status":200,"data":{...},"error":null,"credits_charged":1.0,"cached":false}
#
# event: leg
# data: {"index":1,"platform":"hackernews","endpoint":"item","params":{"id":"8863"},"status":200,"data":{...},"error":null,"credits_charged":1.0,"cached":false}
#
# event: done
# data: {"legs":2,"succeeded":2,"failed":0,"credits_charged":2.0,"request_id":"b7087abd2e124dbb998381d8dce15927","idempotent_replay":false,"credits_remaining":771.8}

Rows come in the order they finish, not request order; use index to place them. See Streaming for the frame types, disconnects and idempotency on streams.

Edge cases#

  • Duplicates are separate items. The same endpoint and parameters twice in one batch are two rows; the second is a cache hit of the first (or both are merged into one fetch when they run at the same moment), so it is free either way.
  • Cursors work per row. A row from a paging endpoint carries its pagination block. Fetch the next page from that endpoint directly; a cursor is bound to the endpoint and its query, not to the batch.
  • An empty params is fine for endpoints without required parameters; leaving params out means the same.
  • Order is guaranteed in the JSON form (rows are sorted by index) and not in the streamed form.
  • Items never see each other. A batch cannot feed one row's output into another item.

Errors it can return#

For the batch as a whole (nothing is run or charged):

StatusTypeWhen
400INVALID_REQUESTThe body is not JSON, has no requests list, is empty, has more than 100 items or is over 1 MB, or an item is malformed.
402INSUFFICIENT_CREDITS, KEY_BUDGET_EXCEEDEDThe worst-case total is above your balance or the key's limit.
401UNAUTHORIZEDBad or missing key.
429RATE_LIMITEDWait Retry-After seconds.

Inside a row, status and error.type can be anything a single call returns: NOT_FOUND (unknown endpoint, or no such record), INVALID_PARAMS, UPSTREAM_ERROR and the other UPSTREAM_* types, INVALID_CURSOR. Plus the idempotency answers when you send an Idempotency-Key. See Errors for the full list.