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

EventWhen
invoice.completedExtraction finished; the invoice resource is complete.
invoice.failedExtraction failed; the resource carries status failed.

Payload

data.invoice is exactly what GET /v1/invoices/:id returns; there is no separate shape to parse.

Payload
{
  "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.

AttemptWhen
1 and 2At dispatch (immediate, 2s apart).
35 minutes later.
430 minutes.
52 hours.
68 hours.
724 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);
}