Background jobs
When a list is longer than one call can take, send it as a job: the API runs it in the background, you poll for the results (or get a signed webhook when it finishes), and you are charged only for what actually ran.
POST https://api.onekey.dev/v1/jobs
x-api-key: YOUR_API_KEY
content-type: application/jsonPOST /v1/batch/jobs is the same route under a second name.
Submit a job#
A job is either a batch (up to 5,000 items, each the same shape as an item of POST /v1/batch) or a crawl (a list of URLs for one web endpoint, up to 5,000):
curl -X POST "https://api.onekey.dev/v1/jobs" \
-H "x-api-key: YOUR_API_KEY" \
-H "Idempotency-Key: 6f1c2d3e-4b5a-4c6d-8e7f-90a1b2c3d4e5" \
-H "content-type: application/json" \
-d '{
"name": "weekly profiles",
"requests": [
{"platform": "github", "endpoint": "profile", "params": {"username": "torvalds"}},
{"platform": "github", "endpoint": "profile", "params": {"username": "gvanrossum"}}
],
"webhook": {"url": "https://hooks.example.com/jobs"}
}'The answer is 202 with the job. Send exactly one of requests or crawl; name (120 characters) and webhook are optional. A crawl runs the web endpoint you name (article, markdown, metadata, any web endpoint with a url parameter) once per distinct URL; it fetches exactly the URLs you list and does not follow links. AI parameters (label, relevance, themes, include) are not supported in jobs.
{
"success": true,
"data": {
"id": "job_9f1c...", "name": "weekly profiles", "kind": "batch", "status": "queued",
"total": 2, "completed": 0, "succeeded": 0, "failed": 0, "progress_pct": 0.0, "cancel_requested": false,
"credits": {"reserved": 4.0, "charged": 0.0, "refunded": 0.0},
"webhook": {"url": "https://hooks.example.com/jobs", "secret": "whsec_..."},
"created_at": "2026-10-02T10:00:00+00:00", "started_at": null, "finished_at": null, "expires_at": null, "error": null
},
"metadata": {"request_id": "...", "idempotent_replay": false}
}webhook.secret is shown once, in this answer. Nothing is held and nothing runs if the request is refused.
Credits: held up front, unused refunded#
At submission the worst case for the whole job is held in one step: the sum of every item's price, or its dearest reachable source, the same upper bound a batch is checked against. If the balance (or the key's credit limit) cannot cover it, the job is refused with INSUFFICIENT_CREDITS or KEY_BUDGET_EXCEEDED and nothing is held.
- Each item then runs like a normal call: a cache hit is free, a failed, empty or not-found item is refunded, and it appears in the request log and the dashboard usage.
- When the job finishes (completed, cancelled or failed), whatever is still held is given back.
credits.refundedisreserved − charged. credits.chargedis what the items actually cost. The ledger shows the hold, the per-item releases and the refund (job_hold,job_release,job_refundinGET /v1/credits/transactions).
Read a job#
curl "https://api.onekey.dev/v1/jobs/job_9f1c..." -H "x-api-key: YOUR_API_KEY"
curl "https://api.onekey.dev/v1/jobs/job_9f1c...?after=99&limit=100" -H "x-api-key: YOUR_API_KEY"Free. The job object plus results, the items so far in order: index, platform, endpoint, params, state (pending, running, ok, error, skipped), status (the HTTP status the call would have had), data, error (same shape as everywhere, with doc_url), credits_charged, cached. limit is 0 to 500 (default 100, 0 returns no rows); after is the last index you have, and next_after is the value to send next (null on the last page). Poll while status is queued or running; completed, cancelled and failed are final.
A result larger than 256 KB is billed but not stored: the item has data: null and data_omitted: true. GET /v1/jobs lists the account's jobs, newest first (limit up to 100). Any key of the account can read any job of the account.
Statuses#
status | Meaning |
|---|---|
queued | Held and stored; no worker has picked it up yet. |
running | A worker is running its items (up to 4 at a time). completed counts items that finished, succeeded and failed split them. |
completed | Every item ran. Individual items may still be error: that is a result, not a failed job. |
cancelled | You cancelled it. Items that had not started are skipped with JOB_CANCELLED and refunded. |
failed | The key that created the job was revoked while it ran; the rest were skipped and refunded. |
A worker that dies mid-job loses its lease after two minutes and another worker resumes from the stored items; an item that was in flight runs again (usually a free cache hit).
Cancel#
DELETE /v1/jobs/{id} stops a job. Items that already ran keep their results and their charge; the rest are skipped and refunded. A job being run right now answers with cancel_requested: true and finishes cancelling within a batch of items. A finished job answers JOB_FINISHED.
Idempotency#
Send an Idempotency-Key (a UUID). The same key with the same body returns the same job (200, idempotent_replay: true, no secret, nothing held again). The same key with a different body is IDEMPOTENCY_KEY_REUSED. A key that is not a UUID is IDEMPOTENCY_KEY_INVALID.
Completion webhook#
With webhook.url the API sends one signed job.completed event when the job reaches a final status. The URL rules, the signature (Onekey-Signature: t=<unix>,v1=<hmac-sha256> over the timestamp and the raw body, with the secret from the submit answer), the retry schedule (1 m, 5 m, 30 m, 2 h, 12 h; at most 6 attempts) and the redirect policy are those of monitors; one verifier covers both. A URL that is not https on a public host is refused with WEBHOOK_URL_REJECTED before anything is held.
{
"id": "dlv_3c1a...", "type": "job.completed", "created_at": "2026-10-02T10:04:00+00:00", "api_version": "v1",
"job": {"id": "job_9f1c...", "status": "completed", "total": 2, "completed": 2, "succeeded": 2, "failed": 0,
"credits": {"reserved": 4.0, "charged": 4.0, "refunded": 0.0}, "...": "..."},
"results_url": "/v1/jobs/job_9f1c..."
}Headers: Onekey-Event: job.completed, Onekey-Job-Id, Onekey-Delivery-Id (the same on every retry: de-duplicate on it) and Onekey-Signature. The event carries no results; fetch them from results_url. Deliveries and their attempts are on GET /v1/jobs/{id} under webhook.deliveries.
Limits#
| Limit | Value |
|---|---|
| Items per job | 5,000 (INVALID_JOB beyond) |
| Request body | 2,000,000 bytes |
| Unfinished jobs | 10 per account and 3 per key (JOB_LIMIT_REACHED beyond) |
| Items in flight per job | 4 |
| Results kept | 7 days after the job finishes, then deleted (JOB_NOT_FOUND) |
| Stored result per item | 256 KB |
Submitting is one request for the rate limit; items run inside the job and are not counted against it. The dashboard has the same routes under /v1/dashboard/jobs (session cookie).
Errors#
| Status | error.type | When |
|---|---|---|
| 402 | INSUFFICIENT_CREDITS / KEY_BUDGET_EXCEEDED | The worst case does not fit. Nothing was held. |
| 404 | JOB_NOT_FOUND | Unknown id, another account's job, or results past their 7 days. |
| 409 | JOB_LIMIT_REACHED | Too many unfinished jobs. |
| 409 | JOB_FINISHED | Cancelling a finished job. |
| 422 | INVALID_JOB | Malformed body, both or neither of requests and crawl, an unknown field, too many items, an AI parameter. |
| 422 | WEBHOOK_URL_REJECTED | The webhook URL failed the egress rules. |
Related#
- Batch requests: the synchronous form for up to 100 items.
- Monitors: the signature and delivery rules.
- Billing webhooks: tell your own systems when credits run low.