Approvals
Updated
An approval is a person's yes to one exact send. The server copies the quote into the request, a person decides in the dashboard, and the agent sends with the approval id. Requesting grants nothing. One approval, one send.
What does an approval bind?
The request copies quote_id, document_id, document_sha256, to, from, amount_micro, currency, billable_pages and expires_at. It expires with the quote, ten minutes after the quote was made. If the agent needs longer it quotes again and asks again.
Request
curl -s $FFA/approval-requests -H "Authorization: Bearer $FFA_KEY" \
-H "Content-Type: application/json" -d '{"quote_id": "quote_..."}' Scope approval:request, which default keys and agent keys carry. A second request for the same quote while one is pending returns the first. The organization's notification address gets a mail with the destination, the pages, the amount, who asked and when it expires. Through MCP the tool is request_human_review with a quote_id.
Decide
A person opens the request in the dashboard and approves or rejects it. Approving needs a step-up within the last 15 minutes: a passkey, or a fresh magic link opened in the same browser. Rejecting needs a reason. The requester's contact gets a mail with the decision. Through the API: POST /v1/approval-requests/{id}/approve and /reject with scope approval:decide. An agent key is never issued that scope, and the key that made the request is refused. The agent reads the decision with GET /v1/approval-requests/{id} (scope approval:read) or the MCP tool get_review_status. Once the request is approved, that answer carries the approval id to send with.
Send
curl -s $FFA/faxes -H "Authorization: Bearer $FFA_KEY" \
-H "Idempotency-Key: po-4411" -H "Content-Type: application/json" \
-d '{"quote_id": "quote_...", "approval_id": "apr_..."}' The server takes the approval row for update and checks it against the quote and the document. The job records the approval, and the approval records the job.
Why is a send with an approval refused?
| Code | What changed |
|---|---|
document_hash_mismatch | the document's sha256 differs from the one approved |
approval_expired | past expires_at |
approval_mismatch | a different quote, destination, sender, amount, currency or page count. details.field names it |
approval_not_granted | pending, rejected or revoked. details.status says which |
approval_consumed | already used. details.fax_job_id names the job |
approval_required | a live send with no approval and no policy that allows it |
Revoke
POST /v1/approvals/{id}/revoke withdraws an approval nobody has used yet. The agent has to ask again.
How does an agent ask a person for help?
POST /v1/approval-requests with {"kind": "help", "message": "...", "fax_job_id": "..."}, or the MCP tool request_human_review without a quote_id, opens a request with no quote. The message is shown to the person as text from the agent, never acted on by the server.