How Approvals Work
The complete lifecycle of an approval request — from creation to signed decision to verification.
An approval in SignedApproval follows a strict lifecycle: pending → approved or rejected → expired (if TTL elapses). Each transition produces cryptographic artifacts that form an audit trail.
There are three roles: the caller (the app or agent requesting approval), the approver (the human deciding), and the verifier (anyone confirming the decision). The caller and verifier can be the same entity or different ones.
Request Lifecycle
Here is the complete flow, step by step:
Caller creates a request
POST to /api/v1/approvals/request with the action description, TTL, optional metadata, and optional webhook URL. Requires a valid sa_live_ API key.
Request stored as pending
The request is inserted into signedapproval_approval_requestswith status "pending" and an expiry timestamp.
Notification sent to approver
If the approver has push notifications enabled (and is not in quiet hours), an APNs push is sent to their iOS device. The request also appears in the web dashboard.
Approver reviews and authenticates
The approver sees the action description and metadata. They choose to approve or reject, then authenticate with their registered method (passkey, TOTP, or biometric).
Decision signed with Ed25519
A canonical JSON payload is constructed and signed with the approver's Ed25519 private key. The decision is stored in signedapproval_approval_decisions.
Caller notified
If a webhook URL was provided, SignedApproval fires a POST with the signed decision. The webhook is HMAC-signed with X-SignedApproval-Signature. Callers can also poll the status endpoint.
Anyone can verify
The public verification endpoint returns the signature, payload, and public key. Verification requires no authentication — anyone with the request ID can confirm the decision is authentic.
Request Statuses
- pending — Waiting for the approver to decide. The request is live and visible.
- approved — The approver authenticated and approved. An Ed25519-signed decision exists.
- rejected — The approver authenticated and rejected. A signed rejection decision exists.
- expired — The TTL elapsed without a decision. No signed artifact is produced for expirations.
TTL (Time to Live)
Every request has a TTL that determines how long it remains actionable. If the TTL expires before the approver decides, the request moves to "expired" status. Callers specify TTL in seconds:
{
"action": "Transfer $500 to vendor",
"ttl_seconds": 3600
}If the caller omits ttl_seconds, the approver's configured default TTL is used (configurable in Settings → Preferences). If the approver has no default, a system default of 24 hours applies.
Quiet Hours
Approvers can configure quiet hours (e.g., 10 PM to 7 AM) in their preferences. During quiet hours, push notifications are suppressed, but requests are still created and visible in the dashboard. The request's TTL still counts down during quiet hours.