Skip to content

Webhooks & signatures

Add a webhook_url to any /v1/render or /v1/image request and DocJet delivers the result asynchronously:

Terminal window
curl -X POST https://api.docjet.dev/v1/render \
-H "Authorization: Bearer $DOCJET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template_id":"invoice-en","data":{"client":"Acme"},"webhook_url":"https://example.com/webhooks/docjet"}'

The API accepts the job immediately; when the render completes, DocJet POSTs a JSON payload to your webhook_url containing the job status and a signed download URL. Webhook URLs must be HTTPS on the default port.

Render outputs are served via expiring, HMAC-signed URLs (.../v1/outputs/<id>?exp=...&sig=...). Anyone with the URL can download until it expires — treat URLs like the documents themselves and pass them point-to-point, not into public logs.

Every webhook callback is signed so you can authenticate that it really came from DocJet before trusting it. The header:

X-DocJet-Signature: t=<unix-seconds>,v1=<hmac-sha256-hex>

where:

v1 = HMAC-SHA256( "<t>" + "." + <raw request body>, webhook_secret )

Your per-key webhook secret comes from:

Terminal window
curl https://api.docjet.dev/v1/keys/webhook-secret \
-H "Authorization: Bearer $DOCJET_API_KEY"
  1. Parse t and v1 from the header.
  2. Reject if |now - t| > 300 seconds (replay protection).
  3. Recompute the HMAC over "<t>.<rawBody>" — the raw body string, before any JSON parsing or re-serialization.
  4. Compare with a timing-safe comparison (crypto.timingSafeEqual / hmac.compare_digest) — never == on hex strings.

Both official SDKs implement the exact scheme above:

// JavaScript — npm install docjet
import { verifyWebhookSignature } from 'docjet';
if (!verifyWebhookSignature(rawBody, signatureHeader, secret)) {
// reject with 401
}
# Python — pip install docjet
from docjet import verify_webhook_signature
if not verify_webhook_signature(raw_body, header, secret):
... # reject with 400/401

Both default to a 300-second tolerance window (configurable) and return false/False for tampered, expired, or malformed signatures.

  • Parsing JSON before verifying — frameworks that re-serialize the body produce different bytes; always verify the raw body string.
  • Skipping the timestamp check — without it, a captured callback can be replayed forever.
  • Hex comparison with == — leaks timing information; use the SDK helpers or a constant-time compare.