How Approvals Work

The complete lifecycle of an approval request — from creation to signed decision to verification.

Key Concepts

An approval in SignedApproval follows a strict lifecycle: pending approved or rejectedexpired (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:

1

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.

2

Request stored as pending

The request is inserted into signedapproval_approval_requestswith status "pending" and an expiry timestamp.

3

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.

4

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).

5

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.

6

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.

7

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:

JSON
{
  "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.

Tip
For critical actions that must be approved quickly, use a short TTL (e.g., 300 seconds / 5 minutes) and combine it with webhook callbacks so your system knows immediately when the decision is made — or when the request expires.