Build an approver app
Any app can carry approvals: a phone app, a chat bot, a relay. The app moves messages; the person signs the exact action hash with their own enrolled key, at the assurance the route demands. The contract is docs/APPROVAL_API.md.
- Registration, and who signs it
- Authentication
- Poll, stream, webhook
- The challenge per key type
- Publishing a decision
- Enrolling keys at each assurance level
- examples/approver_bot.py, walked through
- How the receipt records it
- Every approval route the authority serves
Registration, and who signs it
A device registers with a one-time enrollment code that the principal issues (POST /approvers/{approver}/enrollment-codes, principal-signed); one call to POST /approvals/enroll registers its app key and enrolls the person's signing key. A server app (relay, bot) is registered by the principal's signature (POST /approvals/apps) for named approvers.
Authentication
Every later call carries Authorization: Situs-App kid=<app kid>, ts=<unix>, sig=<hex>, the app key's signature over the hex sha256 of METHOD\nPATH_WITH_QUERY\nts\nsha256_hex(body). Stale or replayed requests are refused; keep ts strictly increasing.
Poll, stream, webhook
- Poll
GET /approvals/pending?approver=&since=with the cursor it returns. - Stream
GET /approvals/stream(server-sent events; resume withLast-Event-ID). - Webhook: a server app's URL receives each event with a
Situs-Signatureheader.
Events and webhooks are signed by the authority; pin its key from https://situs-vm.o11r.com/api/authority/.well-known/keys.json.
The challenge per key type
| alg | what is signed |
|---|---|
| ed25519 | the 64 ASCII bytes of the hex action hash |
| es256 | the same 64 ASCII bytes, ECDSA P-256 / SHA-256, DER hex |
| webauthn-es256 | a passkey assertion; challenge = base64url(the 64 ASCII bytes) |
| appattest-es256 | an App Attest assertion over sha256(authenticator_data || sha256(JCS(client_data))) |
Publishing a decision
POST /approvals/{grant_id} with the approver, approve or reject, the action hash and the signature. Repeating the same decision returns the current state; a different one on a decided grant is refused. Poll the item until resolution.receipt_id fills.
Enrolling keys at each assurance level
| level | what it is | how the server knows | receipt strength |
|---|---|---|---|
| software | a key file or keychain key, ed25519 | verified by construction | 2 |
| hardware | a secure-element key, P-256 (es256) | declared by the app at enrollment, recorded as declared | 3 |
| hardware_attested | a hardware key a vendor attests (App Attest, WebAuthn packed or apple) | verified: the attestation chains to a trusted root | 4 |
A route in a mandate demands a level (software by default). A key below it gets assurance_insufficient and the grant stays pending: no app, channel or console can lower it.
examples/approver_bot.py, walked through
enroll: generates an app key and the person's ed25519 key, and enrolls both with a code (software assurance).call: signs each request as §5 says, with a strictly increasingts.run_once: polls pending items, prints the card exactly, asks y/n, and posts the person's signature overchallenge.digest.
uv run python examples/approver_bot.py --authority https://situs-vm.o11r.com/api/authority --code SITUS-XXXX-XXXX --approver approver_jdoeHow the receipt records it
who.approval carries the approver, kid, alg, assurance, assurance_basis (verified or declared), app_id and approved_at; signatures.approver is the signature over the action hash. The proof report grades it 4 for hardware_attested, 3 for hardware, 2 for software.
Every approval route the authority serves
| method | path | who | what |
|---|---|---|---|
| GET | /approvals/config | anyone | Discovery: rp_id, origins, algs, assurance levels, server time, windows. |
| POST | /approvers/{approver}/enrollment-codes | principal's signature | A one-time enrollment code for an approver. |
| POST | /approvals/enroll | the code | Register a device's app key and enroll the person's signing key in one call. |
| POST | /approvers/{approver}/keys | app + an existing key's signature | Add a key to an approver. |
| GET | /approvers/{approver}/keys | app | The approver's keys with assurance and basis. |
| DELETE | /approvers/{approver}/keys/{kid} | a key of the approver, or the principal | Revoke a key. |
| POST | /approvals/apps | principal's signature | Register a server app (a relay or a bot) for named approvers. |
| GET | /approvals/apps | app | The calling app's own record. |
| DELETE | /approvals/apps/{app_id} | app, or the principal | Revoke an app. |
| GET | /approvals/apps/{app_id}/deliveries | app | Webhook deliveries and their status. |
| PUT | /approvals/apps/{app_id}/push-token | app | Register or replace the app's push token; the relay only ever says an approval is waiting. |
| DELETE | /approvals/apps/{app_id}/push-token | app | Remove the app's push token. |
| GET | /approvals/pending | app | Pending items for an approver, with a cursor (poll). |
| GET | /approvals/stream | app | Server-sent events, signed by the authority; resume with Last-Event-ID. |
| GET | /approvals/{grant_id} | app | One item as it is now; poll it for the receipt id after deciding. |
| POST | /approvals/{grant_id} | app, carrying the approver's signature | Approve or reject the exact action hash. |