Event types
Four events are emitted. Subscribe to all or a subset in the dashboard.
| Event | When fired | data field |
|---|---|---|
| parse.completed | Single-page or multi-page parse finishes | ParseResponse |
| parse.failed | A parse request exhausts all retries | { request_id, error, status_code } |
| batch.completed | All documents in a batch are resolved | BatchStatusResponse |
| batch.failed | A batch is permanently failed | { batch_id, error, failed_documents } |
Envelope format
Every webhook is a POST with Content-Type: application/json. The body is a fixed envelope — the event-specific payload lives under data.
X-Docira-Event: parse.completed X-Docira-Request-ID: req_01hwzqj2k8fgp7e3n6mdct0yxv X-Docira-Signature: sha256=6a8f0c3d… Content-Type: application/json
{
"event": "parse.completed",
"request_id": "req_01hwzqj2k8fgp7e3n6mdct0yxv",
"timestamp": 1714000000.123,
"data": {
// ParseResponse or BatchStatusResponse depending on event
}
}timestamp is a Unix epoch float (seconds). Use it to reject events older than a reasonable window (5 minutes is typical) as a replay defence.
Verifying signatures
When a webhook secret is configured, Docira signs the raw request body with HMAC-SHA256 and sets X-Docira-Signature: sha256=<hex>. Always verify against the raw bytes before JSON-parsing, and use a constant-time comparison to prevent timing attacks.
Set a webhook secret. Endpoints without a secret receive unsigned requests. Anyone who discovers your URL can send forged events. Always configure a secret in the dashboard before accepting webhook data.
import hashlib
import hmac
def verify_signature(
raw_body: bytes,
signature_header: str,
secret: str,
) -> bool:
"""Return True if the X-Docira-Signature header is valid."""
if not signature_header.startswith("sha256="):
return False
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
received = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
# In a FastAPI/Flask handler:
# raw = await request.body()
# sig = request.headers.get("X-Docira-Signature", "")
# if not verify_signature(raw, sig, WEBHOOK_SECRET):
# raise HTTPException(status_code=401, detail="Invalid signature")import { createHmac, timingSafeEqual } from "node:crypto";
function verifySignature(
rawBody: Buffer,
signatureHeader: string,
secret: string,
): boolean {
if (!signatureHeader.startsWith("sha256=")) return false;
const expected = createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = signatureHeader.slice("sha256=".length);
// Use timingSafeEqual to prevent timing attacks
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
// Express handler:
// app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
// const sig = req.headers["x-docira-signature"] as string ?? "";
// if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET!)) {
// return res.sendStatus(401);
// }
// const event = JSON.parse(req.body.toString());
// // handle event.event …
// res.sendStatus(200);
// });# Compute a valid test signature (requires openssl)
SECRET="whsec_your_secret"
BODY='{"event":"parse.completed","request_id":"req_abc","timestamp":1700000000,"data":{}}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print "sha256="$2}')
curl -X POST https://your-server.example.com/webhook \
-H "Content-Type: application/json" \
-H "X-Docira-Signature: $SIG" \
-H "X-Docira-Event: parse.completed" \
-d "$BODY"The signature covers the exact bytes Docira sent. Middleware that re-encodes or compresses the body will break verification — make sure your framework exposes the raw body before any JSON parsing.
Idempotency
The same event may arrive more than once. Retries after a network timeout deliver identical envelopes with the same request_id. Your handler must be idempotent.
- Use
request_idas a deduplication key. Store processed IDs in your database or a Redis set with a TTL of 24 hours. Return200for duplicate events without re-processing them. batch.completedfires once — when all documents resolve. It does not fire per-document; listen toparse.completedif you need per-document notifications.- Events are not guaranteed to arrive in order. A
parse.completedfor page 3 may arrive before page 1. Sort bytimestampafter collecting all events for a job.
Failure and retries
Docira retries a failed delivery up to 3 times with exponential backoff (base 2 s, no jitter cap). A delivery is considered failed if your endpoint returns a 4xx or 5xx status, or does not respond within 30 seconds.
| Attempt | Delay before retry | Cumulative wait |
|---|---|---|
| 1 (initial) | — | 0 s |
| 2 | 2 s | ~2 s |
| 3 | 4 s | ~6 s |
| 4 (final) | 8 s | ~14 s |
After 3 retries Docira marks the delivery as permanently failed and moves on. Failed deliveries appear in the webhook log where you can manually replay them.
Auto-pause threshold
If your endpoint records 5 consecutive final failures (all retries exhausted), Docira pauses delivery for that endpoint and emails the account owner. Re-enable in the dashboard once your receiver is healthy. Events that arrived during the pause are not replayed automatically — replay each one manually from the webhook log if needed.
Test events
Send a synthetic event to any registered endpoint from the dashboard to confirm your handler is wired correctly before routing live traffic.
- Go to Dashboard → Webhooks.
- Select an endpoint and choose an event type from the dropdown.
- Click Send test event. The dashboard shows the raw response from your server within 5 seconds.
Test events carry a real signature computed against your secret, so your verification logic runs end-to-end. The data field contains a minimal fixture — not a real parse result.