Webhooks
Register an https endpoint under Developers → Webhooks and InvoiceJet POSTs signed events when extraction finishes, whether the invoice arrived through the API, the app, or email forwarding.
Events
| Event | When |
|---|---|
invoice.completed | Extraction finished; the invoice resource is complete. |
invoice.failed | Extraction failed; the resource carries status failed. |
Payload
data.invoice is exactly what GET /v1/invoices/:id returns; there is no separate shape to parse.
{
"id": "evt_5f0c…",
"type": "invoice.completed",
"created_at": "2026-07-10T14:03:22.114Z",
"data": { "invoice": { "id": "9b2f…", "object": "invoice", "status": "complete", "…": "…" } }
}Signatures
Every delivery is signed with your endpoint's whsec_… secret (reveal it in Developers → Webhooks). Headers: webhook-id, webhook-timestamp (unix seconds), and webhook-signature: an HMAC-SHA256 of {id}.{timestamp}.{body} keyed with the base64-decoded secret, formatted v1,<base64>. Verify against the raw body bytes, not re-serialized JSON. Reject anything older than 5 minutes. webhook-id is the event id and stays the same across retries, so use it to drop duplicates.
import express from "express";
import { verifyWebhookSignature } from "@invoicejetai/node";
const app = express();
app.post("/webhooks/invoicejet", express.raw({ type: "*/*" }), async (req, res) => {
const ok = await verifyWebhookSignature({
payload: req.body.toString("utf8"),
headers: req.headers,
secret: process.env.INVOICEJET_WEBHOOK_SECRET!,
});
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "invoice.completed") {
// event.data.invoice
}
res.status(200).end();
});
app.listen(3000);Delivery & retries
Endpoint URLs must be https. Redirects are not followed: a 3xx counts as a failed delivery, so register the final URL. Respond with any 2xx within 10 seconds. Failed deliveries (network errors, 3xx, 5xx, and 4xx alike) retry automatically on a backoff schedule, re-signed with a fresh timestamp on each attempt. Every attempt is logged in Developers → Webhooks → Recent deliveries (status, response code, duration; response bodies are not stored), where failed deliveries also show their next scheduled retry and a Redeliver button for an immediate manual attempt. Endpoints can be paused with the active toggle without losing configuration; paused endpoints do not receive retries.
| Attempt | When |
|---|---|
| 1 and 2 | At dispatch (immediate, 2s apart). |
| 3 | 5 minutes later. |
| 4 | 30 minutes. |
| 5 | 2 hours. |
| 6 | 8 hours. |
| 7 | 24 hours, then delivery is abandoned. |
Reconciling: GET /v1/events
Webhooks are at-least-once but not guaranteed: if your endpoint is down past the last retry, the delivery is abandoned. The event itself is never lost. Every event is stored and listable at GET /v1/events (newest first, same payload shape as the webhook body), so a periodic sweep after downtime catches anything you missed. Filter with type, created_after, and created_before; page by passing next_cursor as starting_after until has_more is false.
// After downtime: replay everything since the last event you processed.
// iterate() follows next_cursor, so nothing past the first page is missed.
for await (const event of client.events.iterate({ created_after: lastSeenAt, limit: 100 })) {
if (event.type === "invoice.completed") handleCompleted(event.data.invoice);
}