Concepts
Async & webhooks
Long documents without held-open connections: pass async: true, get a job id in milliseconds, poll — or let a signed callback bring the result to you.
Enqueue
The document endpoints (compare, contract, invoice, parse, receipt, resume, split, statement, tables) accept async: true.
curl https://api.kynth.studio/v1/parse \
-H "Authorization: Bearer $KYNTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "data": "<base64 pdf>", "async": true,
"callbackUrl": "https://yourapp.com/hooks/kynth" }'
# 202 { "jobId": "6f1d2c8e-…", "status": "queued", "requestId": "req_…" }Poll
Status is one of queued, running, succeeded, failed — the last two are terminal. Billing is unchanged: a job is charged when it succeeds, never on enqueue and never when it fails. Your jobs are also visible in the console.
curl https://api.kynth.studio/v1/jobs/6f1d2c8e-… \
-H "Authorization: Bearer $KYNTH_API_KEY"
# { "status": "succeeded", "result": { …, "usage": { … } } }Webhooks
Pass callbackUrl (a public https URL) and we POST the finished job to it once, on completion:
| Header | Value |
|---|---|
| X-Kynth-Event | job.completed | job.failed |
| X-Kynth-Job-Id | the job's id |
| X-Kynth-Request-Id | the originating request id |
| X-Kynth-Signature | sha256=<hex HMAC-SHA256(raw body, your signing secret)> |
The signature is HMAC-SHA256 over the raw request body, keyed with your account's signing secret — find it under Webhooks in the console. Verify before you trust the body, and compare in constant time:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, header: string, secret: string) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const got = header.replace(/^sha256=/, "");
return got.length === expected.length &&
timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}What we guarantee
A job is durably recorded the moment you get the 202, and every job reaches a terminal state. If the instance running yours dies mid-flight, a sweeper marks it failed with an explanation and fires your callback — you're never left polling running forever, and you're never billed for it. We don't silently retry: resubmit and you stay in control of the spend. Callback delivery is best-effort and not retried — polling is the source of truth. Redirects are not followed, and a callbackUrl that resolves to a private address is refused.