Webhook delivery
Signed, retried,
and delivered.
The moment a human resolves an approval, we POST a signed JSON payload to your endpoint. If your endpoint is down, we keep trying for about a day across eight attempts.
apr_4f9c · content-publishing
Delivery timeline
- #114:17:33upstream connect error503
- #214:18:03upstream connect error503
- #314:20:12delivered · acknowledged200
Attempts
3 / 8
Total time
2m 39s
Final status
Delivered
The payload
One shape. Approved or denied, it's the same handler.
Your endpoint receives the full approval object and the channel context. Status tells you which branch to run. The denial_reason is populated when the reviewer denied.
- status = 'approved' | 'denied'
- chosen_action tells you which custom button was clicked
- resolved_by and resolved_at pin the decision to a human and a moment
{
"event": "approval.resolved",
"status": "approved",
"approval": {
"id": "apr_4f9c1a…",
"title": "Publish blog post",
"data": { "post_id": "p_28" },
"chosen_action": "approve_and_publish",
"resolved_by": "pm@finalapproval.ai",
"resolved_at": "2026-04-10T14:20:12Z"
},
"channel": { "id": "chn_b12a…", "name": "Content" }
}Retries
Your bad afternoon doesn't become ours.
Your endpoint times out during a deploy. The VPC misbehaves. A 500 slips through. We retry with jittered exponential backoff and stop the moment you return a 2xx.
- Eight attempts over roughly 24 hours
- Jittered backoff so simultaneous failures don't thunder-herd your endpoint
- 5-second per-attempt timeout
Signed by default
Every request carries an HMAC and a timestamp.
The X-FinalApproval-Signature-256 header lets you prove the payload came from us and hasn't been replayed. Our install guide has a Node and a Python verifier you can paste in.
- HMAC-SHA256 of `timestamp.body` using your channel secret
- Timestamp header for a five-minute replay window
- SSRF-guarded at submit time and again at delivery
POST /webhook HTTP/1.1 Host: api.your-app.com Content-Type: application/json X-FinalApproval-Signature-256: sha256=8f3b…e7a2 X-FinalApproval-Timestamp: 1712770812 X-FinalApproval-Event: approval.resolved X-FinalApproval-Delivery: dlv_a7e2…
Wire it in once. Forget it exists.
Set a webhook URL on a channel and every future decision ships signed, retried, and logged — no further configuration.