> Source: https://txtfetch.com/docs/async > Plain-text twin — every page on txtfetch.com has one. https://txtfetch.com/text --- # Async jobs & webhooks. Large inputs are routed off the request/response cycle automatically. Poll for the result, or have txtfetch push it to you. job-lifecycle `POST /v1/extract` returns `202` with a `job_id` whenever the input is too large or slow for a synchronous response, or whenever `?async=true` is set. Poll `GET /v1/extract/{job_id}`. It returns `{"status": "processing"}` while the job runs. Then it returns the exact same success (or error) shape a synchronous `200` would have returned. Submit (async) ```submit curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf&async=true" \ -H "Authorization: Bearer $TXTFETCH_KEY" # {"status": "processing", "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"} ``` Poll ```poll curl "https://api.txtfetch.com/v1/extract/3fa85f64-5717-4562-b3fc-2c963f66afa6" \ -H "Authorization: Bearer $TXTFETCH_KEY" # {"status": "processing", "job_id": "..."} while running, then the same # {"status": "success", "extracted_text": "...", "metadata": {...}} shape # POST /v1/extract would have returned synchronously. ``` sdk-async Both official SDKs wrap this in a job handle: `extractAsync`/`waitFor`(JS) and `extract_async`/`wait_for` (Python). A plain `extract()`/`extract(...)` call already does this transparently. Reach for these only when you want the job handle without blocking on it. JavaScript ```javascript const job = await txtfetch.extractAsync({ url: "https://example.com/report.pdf" }); const result = await txtfetch.waitFor(job, { maxWaitMs: 5 * 60_000 }); ``` Python ```python job = client.extract_async(file="huge-report.pdf") result = client.wait_for(job) # or: job.result() ``` webhook\_url Pass `webhook_url` on the same `POST /v1/extract` call to have txtfetch push the finished result to your endpoint instead of (or in addition to) polling. The target must be an `http://`/`https://` URL. It's validated at accept time and again at delivery time. Private, internal, and link-local addresses are rejected. This is the same guard that `?url=` fetches go through. curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf" \ -H "Authorization: Bearer $TXTFETCH_KEY" \ --data-urlencode "webhook_url=https://example.com/webhooks/txtfetch" ``` Delivery is a `POST` of the job's result body with two headers: `X-Txtfetch-Signature: sha256=` and `X-Txtfetch-Timestamp: `. The signature is an HMAC-SHA256 (hex) of the string `"."`, keyed by your webhook signing secret. Delivery is retried up to 3 times with a short backoff between attempts. A delivery failure never fails the underlying job. The result is always still available by polling. verify-the-signature Both SDKs ship a verifier that reproduces this exactly, including a constant-time comparison and a timestamp-tolerance replay check. Verify against the _raw_ request body. Don't re-serialize parsed JSON, since that can change byte-for-byte and break the signature. JavaScript ```javascript import { verifyWebhook } from "@txtfetch/sdk"; app.post("/webhooks/txtfetch", (req, res) => { verifyWebhook(req.rawBody, req.header("X-Txtfetch-Signature"), process.env.WEBHOOK_SECRET, { timestamp: req.header("X-Txtfetch-Timestamp"), toleranceSec: 300, }); // ... handle req.body }); ``` Python ```python from txtfetch import verify_webhook, WebhookVerificationError try: verify_webhook( payload=request_body, # raw request body, str or bytes signature=request.headers["X-Txtfetch-Signature"], secret=WEBHOOK_SIGNING_SECRET, timestamp=request.headers["X-Txtfetch-Timestamp"], ) except WebhookVerificationError: # reject the delivery ... ``` Raw HMAC ```raw-hmac import { createHmac, timingSafeEqual } from "node:crypto"; // Reproduces exactly what api/extract/src/webhook.js signs: HMAC-SHA256 of // ".", hex-encoded, "sha256=" prefixed. function verify(rawBody, signatureHeader, timestampHeader, secret) { const expectedHex = createHmac("sha256", secret) .update(`${timestampHeader}.${rawBody}`) .digest("hex"); const provided = Buffer.from(signatureHeader.replace(/^sha256=/, ""), "hex"); const expected = Buffer.from(expectedHex, "hex"); if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) { throw new Error("webhook signature mismatch"); } } ``` see-also Retrying a `POST /v1/extract` call (including one that also carries `webhook_url`)? Pair it with an [Idempotency-Key](https://txtfetch.com/docs/idempotency) so a retried submit never double-runs the extraction. ## Watch it run, then take a key. The playground replays a real recorded response for every sample document. [Open the playground →](https://txtfetch.com/playground) [Get an API key →](https://app.txtfetch.com/signup)