The approval flow
What happens between your API call and the webhook — the approver's experience, payload editing, and how requests end.
Magic links, not accounts
Approvers are busy people who did not sign up for your tooling. So they never log in: each request generates a single-purpose link like confirm.dev/a/<token> that we email to the address you passed in notify. Possession of the link is the credential — the token is 256 bits of entropy, stored only as a hash, bound to exactly one request, and dead the moment the request resolves or expires.
The review page shows the request summary, the agent's name, the workspace it came from, the full JSON payload, and the expiry time. Three actions are possible:
- Approve — the action proceeds as proposed.
- Reject — the action never runs; an optional note explains why.
- Edit payload first — fix the JSON inline, then approve the corrected version.
Payload editing
Editing is the feature that separates "approval" from "supervision." When the human corrects the payload — fixing a hallucinated detail, adjusting an amount, removing a recipient — we store their version as modifiedPayload alongside the untouched original, and every read surface exposes:
| Field | Contents |
|---|---|
| payload | What the agent proposed, verbatim. Never mutated. |
| modifiedPayload | The approver's edit, or null if they approved as-is. |
| effectivePayload | modifiedPayload ?? payload — the one your code should execute. |
Keeping both versions is deliberate: the diff between what the agent wanted and what the human allowed is often the most valuable line in your audit log.
Resolution semantics
Requests resolve exactly once. The state transition is guarded at the database level, so a double-click, a forwarded email opened by two people, or a race between an approval and the expiry sweep can't produce two verdicts — the first decision wins and every later attempt sees "already resolved."
Each resolution records:
- the decision and the approver's email identity
- timestamp, IP address, and user agent
- an optional free-text note for the audit log
- the payload edit, if any
Expiry
Every request carries a TTL — ttlMinutes on create, default 24 hours, maximum 7 days. An unanswered request moves to EXPIRED and fires request.expired. Expiry is enforced both by a scheduled sweep and lazily on every read, so a stale PENDING is never returned even if the sweep lags.
DESIGN
EXPIRED exactly like a rejection in your agent code. If the action was worth doing, the agent can ask again — a fresh request with a fresh link.The developer's view
Everything the approver does surfaces in your dashboard: the audit log lists every request with status and resolver, and each request's detail page shows the original payload, the edited payload when one exists, the resolution metadata, and each webhook delivery attempt with its HTTP status.