Docs
View as Markdown

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.

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

POST /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.

json
{
  "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.refunded is reserved − charged.
  • credits.charged is what the items actually cost. The ledger shows the hold, the per-item releases and the refund (job_hold, job_release, job_refund in GET /v1/credits/transactions).

Read a job#

shell
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#

statusMeaning
queuedHeld and stored; no worker has picked it up yet.
runningA worker is running its items (up to 4 at a time). completed counts items that finished, succeeded and failed split them.
completedEvery item ran. Individual items may still be error: that is a result, not a failed job.
cancelledYou cancelled it. Items that had not started are skipped with JOB_CANCELLED and refunded.
failedThe 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.

json
{
  "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#

LimitValue
Items per job5,000 (INVALID_JOB beyond)
Request body2,000,000 bytes
Unfinished jobs10 per account and 3 per key (JOB_LIMIT_REACHED beyond)
Items in flight per job4
Results kept7 days after the job finishes, then deleted (JOB_NOT_FOUND)
Stored result per item256 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#

Statuserror.typeWhen
402INSUFFICIENT_CREDITS / KEY_BUDGET_EXCEEDEDThe worst case does not fit. Nothing was held.
404JOB_NOT_FOUNDUnknown id, another account's job, or results past their 7 days.
409JOB_LIMIT_REACHEDToo many unfinished jobs.
409JOB_FINISHEDCancelling a finished job.
422INVALID_JOBMalformed body, both or neither of requests and crawl, an unknown field, too many items, an AI parameter.
422WEBHOOK_URL_REJECTEDThe webhook URL failed the egress rules.