Approval UI

A card the reviewer can decide fromin under five seconds.

Every approval lands in a full-width card with the rendered HTML body your agent sent, the actions it's asking for, and nothing else. Skim, decide, move on.

app.finalapproval.ai/dashboard/channels/content

Channel · Content publishing

3 pending

Live

apr_4f9c1a · 2m ago · content-bot

Publish blog post: "A quieter way to ship agents"

Pending

I drafted the launch post you asked for. Ready to publish to the blog and schedule the social posts.

Word count

1,284

Reading time

6 min

Category

Engineering

Show first 200 words

There's a moment in every AI-agent project where the thing goes from toy to tool — and it's almost always the moment the team realises it can't ship without a human on the rail…

Responds to webhook · api.contentapp.com/webhook

Rich HTML bodies

Your agent writes the story. The reviewer reads it.

Submit any sanitised HTML as the body. Tables, images, code blocks, collapsible sections. The reviewer sees exactly what the agent is asking to do, in the shape the agent thought would be clearest.

  • Sanitised by DOMPurify on render — scripts, iframes, and inline handlers stripped
  • Images must be https; links open in a new tab by default
  • Works with prose, tables, code, diffs, details/summary, and small media
approval body · rendered

Migrate 2,341 rows from staging → prod

The migration adds a team_id column and backfills it from the legacy ownership table.

TableRows
projects1,204
proj_members987
activities150
ALTER TABLE projects ADD COLUMN team_id uuid;
UPDATE projects p SET team_id = (
  SELECT team_id FROM legacy_owner o WHERE o.project_id = p.id
);

Custom actions

Two buttons aren't always enough.

Approve and Deny ship by default. Add up to three extra actions per channel — 'Publish as draft', 'Send to editor', 'Reject with reason' — and your webhook receives the exact action the reviewer chose.

  • Configure per channel, not per approval
  • Labels, colours, and order are yours
  • The chosen action ships to your webhook as chosen_action
action bar · per channel
Approve & publishprimary
Publish as draftcustom
Send back to editorcustom
Denydeny

Denial reasons

A reviewer can't say 'no' without a why.

Deny prompts for a reason before it commits. The text is stored with the approval, flows to your webhook, and shows up in the history view for anyone auditing the decision later.

  • Required by default, configurable per channel
  • Appears on the webhook payload as denial_reason
  • Rendered into the history view so the audit trail is self-explaining
deny · reason required
Tone feels off for the audience — rewrite the intro to lead with the outcome, not the backstory. Happy to approve the second draft.
Shipped to your webhook as denial_reason

Give your humans a review loop they'll actually use.

Spin up a channel, submit your first approval with curl, and watch the card render.