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.

Submit (async)
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
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
const job = await txtfetch.extractAsync({ url: "https://example.com/report.pdf" });
const result = await txtfetch.waitFor(job, { maxWaitMs: 5 * 60_000 });
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 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
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.

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
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
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 →