Webhooks

Webhooks

Receive async callbacks when document processing completes — no polling required.

Last updated: April 2026

How it works

When a job reaches a terminal state (done or failed), we POST a signed payload to your registered callback URL.

Register your webhook URL in the dashboard under Settings → Webhooks. One URL per environment is recommended.

Payload shape

JSON
{
  "event":        "job.completed",
  "jobId":        "job_abc123",
  "fileStatus":   "done",
  "documentType": "invoice",
  "timestamp":    "2026-04-22T10:00:00.000Z"
}

For failed events, an additional error field describes the failure reason.

Signature verification

Every webhook request includes an x-number7ai-signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret.

Always verify the signature before trusting the payload. Reject requests with a missing or invalid signature.
TypeScript
import crypto from "crypto";

// Express example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["x-number7ai-signature"] as string;
  const expected = crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET!)
    .update(req.body)          // raw Buffer — do NOT parse JSON first
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body.toString());
  // handle event.event === "job.completed" …
  res.sendStatus(200);
});
Python
import hmac, hashlib

def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask example
@app.route("/webhook", methods=["POST"])
def webhook():
    sig = request.headers.get("x-number7ai-signature", "")
    if not verify_signature(request.data, sig, WEBHOOK_SECRET):
        abort(401)
    event = request.get_json(force=True)
    # handle event ...
    return "", 200

Retry policy

  • We retry delivery up to 3 times with exponential backoff on non-2xx responses.
  • Return HTTP 200 as fast as possible — do heavy processing asynchronously.
  • Idempotency: use jobId to deduplicate; retries may deliver the same event more than once.