Webhooks
Webhooks notify your system when something happens, so you don’t have to poll. Available on the Scale and Enterprise plans.
Events
Section titled “Events”| Event | Sent when |
|---|---|
invoice.analysis.completed |
An invoice has a decision. |
invoice.analysis.failed |
Analysis of a document failed. |
scan.completed |
A historical scan finished. |
finding.created |
A new finding was raised. |
finding.reviewed |
Someone recorded a disposition on a finding. |
Create an endpoint
Section titled “Create an endpoint”In Integrations → Webhooks, enter your endpoint URL (HTTPS only) and pick the events, or use the API:
curl https://api.invogi.com/v1/webhooks \ -H "Authorization: Bearer $INVOGI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/invogi/webhooks", "events": ["invoice.analysis.completed"]}'The response includes the endpoint’s signing secret. It is shown once, so
store it straight away; you need it to verify deliveries.
Payload
Section titled “Payload”{ "id": "evt_…", "type": "invoice.analysis.completed", "data": { "invoice_id": "3f2c…", "decision": "PASS", "risk_score": 4, "export_ready": true, "canonical_export_path": "/v1/invoices/3f2c…/export?format=json" }}export_ready is true only for PASS
decisions; use canonical_export_path to fetch the invoice for your payment
system.
Verify signatures
Section titled “Verify signatures”Every delivery carries two headers:
invogi-event-id: the event ID.invogi-signature:t=<unix seconds>,v1=<hex>.
The signature is HMAC-SHA256, keyed with your endpoint secret, over
<event id>.<t>.<raw request body>. Reject the request if no v1 value
matches or if t is more than a few minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyInvogiWebhook(rawBody: string, headers: Headers, secret: string): boolean { const eventId = headers.get("invogi-event-id") ?? ""; const header = headers.get("invogi-signature") ?? ""; const match = header.match(/^t=(\d+)((?:,v1=[a-f0-9]+)+)$/); if (!match) return false;
const timestamp = Number(match[1]); if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac("sha256", secret).update(`${eventId}.${timestamp}.${rawBody}`).digest(); return (match[2] ?? "") .split(",") .slice(1) .map((entry) => Buffer.from(entry.slice(3), "hex")) .some((candidate) => candidate.length === expected.length && timingSafeEqual(candidate, expected));}Always verify against the raw body, before parsing JSON.
Rotate a secret
Section titled “Rotate a secret”Rotate secret in the app (or POST /v1/webhooks/{id}/rotate-secret) returns a new
secret once. For 24 hours deliveries are signed with both secrets, so the
header carries two v1= values and the code above accepts either. Rotating
again within that window drops the older secret immediately.
Retries and replays
Section titled “Retries and replays”Respond with any 2xx status within 15 seconds. Failed deliveries are retried
with exponential backoff and stop after five failed attempts.
Integrations → Webhooks → Recent deliveries lists the latest 100 deliveries with status,
response code and attempt count, and a Replay button for failed ones. Use
invogi-event-id to ignore duplicates, since a replay re-sends the same event.