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.
POST https://api.onekey.dev/v1/batch
x-api-key: YOUR_API_KEY
content-type: application/jsonWhen 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 field | Required | What it is |
|---|---|---|
platform, endpoint | yes | The two path words of the endpoint, as in GET /v1/{platform}/{endpoint}. |
params | no | The 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:
{
"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#
| Field | Type | Meaning |
|---|---|---|
index | number | Position of this item in your request, from 0. |
platform, endpoint | string | The endpoint that ran. |
params | object | The parameters it ran with, every value as a string. |
status | number | The HTTP status this call would have had on its own: 200, or 404, 422, 502... |
data | object, array or null | The unified result when status is 200, exactly as the endpoint returns it (including its computed block where it has one). null otherwise. |
error | object or null | null on success. Otherwise code, type, message, doc_url, as in any error. |
credits_charged | number | What this item cost. 0 for a cache hit or a failure. |
cached | boolean | true when the answer came from cache. |
pagination | object | Only 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#
| Field | Meaning |
|---|---|
legs | How many items ran (one per request item). |
succeeded, failed | How many rows have status: 200, and how many do not. |
credits_charged | The sum of the rows' credits_charged. |
request_id | One id for the whole batch, the same as the X-Request-Id header. Quote it if you contact support. |
idempotent_replay | true 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:
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
paginationblock. Fetch the next page from that endpoint directly; a cursor is bound to the endpoint and its query, not to the batch. - An empty
paramsis fine for endpoints without required parameters; leavingparamsout 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):
| Status | Type | When |
|---|---|---|
| 400 | INVALID_REQUEST | The 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. |
| 402 | INSUFFICIENT_CREDITS, KEY_BUDGET_EXCEEDED | The worst-case total is above your balance or the key's limit. |
| 401 | UNAUTHORIZED | Bad or missing key. |
| 429 | RATE_LIMITED | Wait 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.