Webhook + audit trail
Every decision,
on the wire and on the record.
When a human resolves an approval, your app hears about it within seconds. Every attempt we make lands in a delivery log you can open any time, including the ones that failed.
Delivery
A clean JSON POST to your endpoint.
When someone approves or denies, we build the payload, POST it to your URL, and hand your code everything it needs to run the next step. The request is signed so you can verify it came from us.
X-FinalApproval-Signature-256: sha256=8f3b…e7a2
X-FinalApproval-Timestamp: 1712770812
{
"event": "approval.resolved",
"status": "approved",
"approval": {
"id": "apr_4f9c…",
"title": "Publish blog post",
"data": { "post_id": "p_28" },
"resolved_by": "pm@finalapproval.ai",
"resolved_at": "2026-04-10T14:20:12Z"
},
"channel": { "id": "chn_b12a…", "name": "Content publishing" }
}The webhook fires the moment a human clicks. Your branch runs while the reviewer is still on the page.
Approved or denied, you get the same shape with a status field. One handler, two branches.
Each delivery carries an HMAC signature and a timestamp. The install guide has the verifier; the security page covers the threat model.
Durability
We retry for about 24 hours.
Things break. Your endpoint times out during a rolling deploy and the delivery fails. That's fine — we'll keep trying for roughly a day across eight attempts with jittered backoff, and we stop the moment you return a 2xx.
- #1immediate0s
- #230s30s
- #32m2m 30s
- #410m12m 30s
- #530m43m
- #62h2h 43m
- #76h8h 43m
- #812h20h 43m
Audit trail
Every attempt, written to an append-only log.
This is the log you reach for when something's weird in production. Who approved, when, what your endpoint returned, and whether the delivery actually went through. Open it from the channel page and filter down to the row you care about.
A 503 → 503 → 200 sequence reads top-to-bottom like a timeline, so you can see exactly when your endpoint came back.
Upstream status code plus the first line of the error body. Nothing swallowed, nothing summarised.
Fixed the bug on your side? Pick the row and resend. The decision flows through without asking the human again.
Wire it in once
Drop a URL in. Done.
Add a webhook URL to the channel and you're finished. The signing, the retries, the log: all of it happens without you touching it again.