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.

POST https://your-app.com/webhook200 OK
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" }
}
Within seconds

The webhook fires the moment a human clicks. Your branch runs while the reviewer is still on the page.

Same payload, every event

Approved or denied, you get the same shape with a status field. One handler, two branches.

Signed by default

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.

AttemptDelayCumulative
  1. #1immediate0s
  2. #230s30s
  3. #32m2m 30s
  4. #410m12m 30s
  5. #530m43m
  6. #62h2h 43m
  7. #76h8h 43m
  8. #812h20h 43m
5s per-attempt timeout, ±20% jitter on every delay, stops on the first 2xx.

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.

channel / content-publishing / deliveries
TimeEndpointAttemptResult
14:20:12
api.your-app.com/webhook
1 / 8200
14:18:03
api.your-app.com/webhook
2 / 8200
14:17:33
api.your-app.com/webhookupstream connect error
1 / 8503
13:02:44
api.your-app.com/webhook
1 / 8200
One row per attempt

A 503 → 503 → 200 sequence reads top-to-bottom like a timeline, so you can see exactly when your endpoint came back.

Errors captured verbatim

Upstream status code plus the first line of the error body. Nothing swallowed, nothing summarised.

Replay any row

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.