Webhooks
Get a POST the moment your books move: a document finishing processing, a new proposal ready for review. Org owners create webhooks in the app under Organisation → Webhooks; each one receives every event for your org.
Events
document.ingested means something you sent in (a CLI push, an app upload, or an email) has finished processing. Its state tells you what to do next: proposed means proposals are ready to review, needs_attention means it needs a human look.
proposal.created means the AI has a new reconciliation proposal waiting for your review.
test is what the Send test button fires, so you can check your endpoint end to end. Same shape and signature as the real thing, but delivered once: never retried, and never counted toward auto-disable.
Every webhook gets every event for your org; there is nothing to subscribe to per event.
Payload shapes
Payloads say what happened, never what it’s worth: IDs only, no amounts, no descriptions. Fetch the details through the API with your key.
// document.ingested
{
"event": "document.ingested",
"orgId": "...",
"occurredAt": "2026-07-17T...Z",
"jobId": "...",
"documentId": "...",
"state": "proposed" | "needs_attention"
}
// proposal.created
{
"event": "proposal.created",
"orgId": "...",
"occurredAt": "2026-07-17T...Z",
"proposalId": "..."
}Verifying a delivery
Every delivery is signed, so you can prove it really came from acc. The X-Acc0-Signature header carries t=<unix timestamp>,v1=<hex HMAC-SHA256>, computed over ${timestamp}.${rawBody} with the signing secret shown when you created the webhook:
const crypto = require('node:crypto');
function verify(header, rawBody, secret) {
const [tPart, v1Part] = header.split(',');
const t = tPart.split('=')[1];
const v1 = v1Part.split('=')[1];
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return expected === v1;
}Delivery and retries
You don’t need your own retry logic. Deliveries queue and send in the background; your endpoint has 10 seconds to respond, and a failed delivery retries up to 6 attempts, waiting twice as long each time (30s, 60s, 120s, and so on) before that delivery is given up as failed.
If ten deliveries in a row exhaust their retries, the webhook switches itself off, so a dead endpoint doesn’t pile up failures forever; re-enable it in the app once your endpoint is back and the counter resets. The destination URL must stay public: it is re-checked before every attempt and can’t resolve to localhost or a private address.
Local testing
Run acc0 listen --secret <secret> (part of the acc0 CLI) and every delivery prints on your machine, labelled VERIFIED or INVALID SIGNATURE. Because acc won’t deliver to localhostor private addresses, put a tunnel (cloudflared, ngrok) in front and register the tunnel URL as your webhook. The CLI docs’ local webhook testing section walks the whole setup.