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 the exact same success (or error) shape a synchronous 200 would have returned.
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"}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.
const job = await txtfetch.extractAsync({ url: "https://example.com/report.pdf" });
const result = await txtfetch.waitFor(job, { maxWaitMs: 5 * 60_000 });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 and is validated at accept time and again at delivery time — private/internal/link-local addresses are rejected (the same guard ?url= fetches go through).
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=<hex> and X-Txtfetch-Timestamp: <ms-epoch>. The signature is an HMAC-SHA256 (hex) of the string "<timestamp>.<body>", 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.
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
});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
...import { createHmac, timingSafeEqual } from "node:crypto";
// Reproduces exactly what api/extract/src/webhook.js signs: HMAC-SHA256 of
// "<timestamp>.<body>", 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 so a retried submit never double-runs the extraction.
Stop parsing. Start shipping.
Create an account and get an API key in minutes — the free Hobby plan needs no card.
Get started →