How it works
One skill call.
A human in every loop that matters.
FinalApproval is the checkpoint you wire into your agent once. After that, every gated action your agent performs pauses for a human decision, and your own handler runs on the way back.
The mental model
One-time setup. Per-decision calls.
The skill provisions a channel
Your agent runs the skill and describes what it wants approved. The skill creates the hosted review page, issues an API key, and writes the signed webhook endpoint and continuation handler directly into your codebase.
- Hosted review UI at finalapproval.ai/a/<channel>
- HMAC-signed webhook endpoint in your own app
- Approve / deny handler wired into your agent
Your app POSTs approvals
At every gate point, your code calls POST /api/v1/approvals. A human reviews the HTML body, clicks Approve or Deny, and your handler branch runs on the webhook callback.
- Rich HTML body describes the decision to the reviewer
- Structured JSON data travels alongside for the handler
- Decision fires back in seconds, not days
Setup
Your agent runs the skill
The skill is a one-time action. You describe what needs approval — in plain English — and the skill provisions the hosted channel, the signed endpoint, and the continuation code in your repo.
Hosted review UI
Built from your description. Rendered server-side.
One-time API key
Shown once. Stored as SHA-256 hash server-side.
Signed webhook endpoint
HMAC-SHA256 with replay timestamp. Drop-in verifier.
Continuation handler
Approve / deny branches in your own code.
$ /approval-create-channelApprove customer refunds above $1,000 before they post
Runtime
Your agent asks for approval
At every gate point, your code calls POST /api/v1/approvals with an HTML body the reviewer sees and a structured data payload your handler will use. The call blocks only long enough to log the request — the actual wait happens out of band.
body (HTML, for humans)
Sanitized, Tailwind-styled, renders in the dashboard.
data (JSON, for machines)
Travels through the webhook. Use it in your handler.
await fetch('https://www.finalapproval.ai/api/v1/approvals', {method: 'POST',headers: {Authorization: `Bearer ${process.env.FA_API_KEY}`,'Content-Type': 'application/json',},body: JSON.stringify({title: 'Refund $1,248 to Acme, Inc.',body: `<p>Invoice #INV-9831 · support ticket #4421</p>`,data: { invoice_id: 'INV-9831', amount: 1248 },}),});
The reviewer's seconds
A human reviews and decides
The reviewer opens the channel, reads the rendered HTML, and clicks Approve or Deny. Optional denial reasons are captured in the audit trail. The UI is keyboard-first — Enter approves, Esc cancels.
Decision takes seconds
Most approvals resolve in under 90 seconds — faster than Slack, let alone email.
Sanitized HTML rendering
DOMPurify allowlist strips scripts, iframes, and unsafe attributes before display.
Acme, Inc. · #INV-9831
Refund requested by support ticket #4421. Original charge placed 2026-04-02; customer reports duplicate billing.
Continuation
Your handler runs on the way back
The moment a decision is recorded, we sign and POST it to the webhook endpoint the skill wired into your app. Your handler verifies the HMAC, reads the decision, and runs the matching branch — executing the action or closing it out.
HMAC-SHA256 signed
X-FinalApproval-Signature-256: sha256=<hex> over timestamp + body.
Replay-resistant
X-FinalApproval-Timestamp header — verify it's within 5 minutes.
8 retries over 24 hours
Jittered backoff: 30s, 2m, 10m, 30m, 2h, 6h, 12h. Every attempt logged.
→ POST https://api.yourapp.com/hooks/refunds{"decision": "approved","approver": "sam@team.com","channel": "refunds","data": { "invoice_id": "INV-9831", "amount": 1248 }}
Under the hood
The approval lifecycle
- pending
The approval exists, the reviewer has been notified, and the webhook hasn't been called. No action has been taken on your end yet.
- approved
A human clicked Approve. The decision is written to the audit log, the webhook is queued, and the continuation branch runs when delivery succeeds.
- denied
A human clicked Deny. Any denial reason is attached to the record. The webhook delivers the decision so your handler can cancel cleanly.
- expired
Channels can auto-expire pending approvals after a configurable window (default 7 days). Expired requests are tombstoned, not deleted, so the history stays complete.
Put a human in the loop — in ninety seconds.
Install the skill, describe what you want approved, and ship.