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.

app.finalapproval.ai/dashboard/channels/content/webhook

apr_4f9c · content-publishing

Delivery timeline

Delivered
  1. #114:17:33upstream connect error503
  2. #214:18:03upstream connect error503
  3. #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
POST https://your-app.com/webhook
{
  "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
backoff curve · 8 attempts~24h total
#1
immediate
#2
30s
#3
2m
#4
10m
#5
30m
#6
2h
#7
6h
#8
12h

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
request headers · signed
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.