DocsJobs & Queue

Jobs & Queue

The async job lifecycle: webhooks (recommended), polling, and the job object.

Every extraction is a job. Creating one returns instantly with the job already in pending, and the work happens in a queue behind the scenes. You get the result by receiving a webhook (recommended) or by polling the job. This design means a slow or large document never blocks your request, and nothing is silently dropped. A job always ends in completed or failed.

Lifecycle

text
create ──▶ pending ──▶ completed
                  └──▶ failed
StatusMeaning
StatuspendingMeaningThe job was accepted and is being processed. output and settled usage are not final yet.
StatuscompletedMeaningDone. output holds the result; usage.cost is the settled charge.
StatusfailedMeaningThe job could not be completed. The balance hold is refunded.

POST /jobs/create returns the full job object, the same shape as GET /jobs/:jobId/get, already in pending, with model, target, and the hold populated. output and the final token usage fill in once it finishes.

Use webhooks, not polling. If your service can receive an HTTPS request, register a webhook_url and let Veralens push the result the moment the job finishes. It is lower latency, costs you no extra requests, and takes load off both your app and the API. Reach for polling only when you genuinely can't receive a webhook: a local script, a notebook, a one-off.

Pass a webhook_url when creating the job and Veralens will POST the result to it as soon as the job finishes. Webhooks require API-key auth. From a dashboard session, webhook_url returns 400 invalid_input.

bash
curl -X POST https://veralens.ai/api/v1/jobs/create \
  -H "Authorization: Bearer vl_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [{ "type": "text", "data": "..." }],
    "target": "markdown",
    "webhook_url": "https://yourapp.com/hooks/veralens"
  }'

Delivery payload. The body is one of:

json
{ "status": "success", "job_id": "V1StGXR8_Z5jdHi6B", "data": { "merchant": "Acme", "total": 19.9 } }
json
{ "status": "error", "job_id": "V1StGXR8_Z5jdHi6B" }

For success, data is the parsed result (already an object for json targets, a string for markdown/plaintext). An error delivery means the job ended failed and its hold was refunded. It carries no reason.

Headers. Verify authenticity before trusting a delivery:

HeaderDescription
Headerx-veralens-signatureDescriptiont=<unix seconds>,v1=<hex>.
Headerx-veralens-timestampDescriptionThe same t, on its own.
Headerx-veralens-delivery-attemptDescriptionThe attempt number for this delivery.

Checking the signature. v1 is the hex HMAC SHA-256 of the string <t>.<raw body>, keyed with the secret of the API key that created the job. Compute it over the body exactly as received, before you parse it, and compare in constant time. Refuse a t older than a few minutes. If you revoke that key, its webhooks stop being delivered. A Node example is in the Quickstart.

Retries. A delivery fails when your endpoint does not reply with a 2xx within 10 seconds. Redirects are not followed and count as a failure. Veralens then retries up to 6 more times, waiting 20, 40, 80, 160, 320 and 320 seconds before each try. URLs that point to a private or local network address are never called.

Two rules for a robust handler:

  • Acknowledge fast, process after. Return 200 immediately, then do the work. A slow handler looks like a failure and triggers retries.
  • Be idempotent on job_id. A retry can deliver the same event twice; dedupe so you never double-process.

Polling (fallback)

When you can't receive a webhook, fetch the job until status is no longer pending. Poll every 2 to 3 seconds. GET /jobs/:jobId/get allows 30 requests a minute per key, so polling every second hits 429 after 30 seconds. See Rate limits.

js
async function waitForJob(jobId, key) {
  const BASE = "https://veralens.ai/api/v1"
  for (let attempt = 0; attempt < 60; attempt++) {
    const res = await fetch(`${BASE}/jobs/${jobId}/get`, {
      headers: { Authorization: `Bearer ${key}` },
    })
    const job = (await res.json()).data
    if (job.status === "completed") return JSON.parse(job.output ?? "null")
    if (job.status === "failed") throw new Error(`job ${jobId} failed`)
    await new Promise((r) => setTimeout(r, 2500))
  }
  throw new Error("timed out waiting for job")
}

The same in Python, with requests:

python
import json, time
import requests

def wait_for_job(job_id, key):
    base = "https://veralens.ai/api/v1"
    for _ in range(60):
        res = requests.get(f"{base}/jobs/{job_id}/get", headers={"Authorization": f"Bearer {key}"})
        job = res.json()["data"]
        if job["status"] == "completed":
            return json.loads(job["output"] or "null")
        if job["status"] == "failed":
            raise RuntimeError(f"job {job_id} failed")
        time.sleep(2.5)
    raise TimeoutError("timed out waiting for job")

Billing per job

Creating a job places an estimated hold on your balance. When the job completes, the hold settles to the actual token cost, which can be higher or lower than the hold. If it fails, the hold is refunded in full. The final usage.cost on the job is what you were charged. See Pricing & Funds.

Listing and traceability

Use GET /jobs/list to page through past jobs, filter by status or source, or find a job by id with q. Every job keeps its target, engine, token usage, and cost, so you can audit exactly what each extraction did and what it cost.