Receipts

A receipt is a signed, immutable record of a single authorization decision or event. Every /check call, authorization lifecycle event (create, revoke), and escalation resolution produces one.

Receipts are signed with an Ed25519 workspace signing key managed by Allowly. Anyone with your workspace's public key can verify a receipt offline, permanently, without contacting Allowly.

---

GET /v1/receipts/{receipt_id}

Fetch a single receipt by ID. Returns a discriminated union on status.

GET /v1/receipts/rcp_01HXZ2B...
Authorization: Bearer allowly_l1_s001_...

Pending — signing not yet complete

{
  "status": "pending",
  "receipt_id": "rcp_01HXZ2B...",
  "ready_at_estimate": "2026-04-21T14:32:18.482Z",
  "url": "https://api.allowly.ai/v1/receipts/rcp_01HXZ2B..."
}

Poll the same URL. Signing completes asynchronously after the decision.

Signed — full receipt, ready to verify

{
  "status": "signed",
  "receipt": {
    "version": "1.0",
    "receipt_id": "rcp_01HXZ2B...",
    "workspace_id": "ws_01HXA1...",
    "issued_at": "2026-04-21T14:32:17.482Z",
    "decision": "allow",
    "reason": "authorization_granted_scope_active",
    "user_id": "emp_8821",
    "agent_id": "referral_outreach",
    "scope": "outreach.send",
    "resource": "edge:emp_8821:conn_9f2a",
    "context": {
      "initiated_by": "user",
      "origin": "chat",
      "session_id": "sess_7f2"
    },
    "authorization_id": "auth_01HXZ2A...",
    "policy_version": "2026-04-17.1",
    "signature": {
      "alg": "Ed25519",
      "key_id": "workspace-signing-key-version",
      "value": "base64url..."
    }
  }
}

Cache this locally. The receipt is immutable — it never changes after signing.

---

GET /v1/receipts

List receipts with filters. Returns summaries; fetch individual receipts for the full signed artifact.

GET /v1/receipts?authorization_id=auth_01HXZ2A...
Authorization: Bearer allowly_l1_s001_...

Supported query parameters:

ParameterDescription
authorization_idFull authorization chain: creation + scope receipts + escalation resolutions + revocation
user_idAll receipts for a user across all their authorizations
resourceAll receipts touching a specific resource
session_idAll receipts from a specific agent session
scopeFilter by scope name
eventFilter by event (authorization.create, authorization.revoke, escalation.resolve)
decisionFilter by outcome (allow, deny, confirm, escalate, authorization_granted, authorization_revoked, escalation_approved, escalation_rejected)
fromISO 8601 timestamp lower bound
toISO 8601 timestamp upper bound
cursorOpaque pagination cursor returned as next_cursor from a prior response
limitPage size, default 50, maximum 100
{
  "receipts": [
    {
      "receipt_id": "rcp_01HXZ2B...",
      "authorization_id": "auth_01HXZ2A...",
      "scope": "outreach.send",
      "event": null,
      "decision": "allow",
      "signed": true,
      "created_at": "2026-04-21T14:32:17.482Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

When has_more is true, pass next_cursor back on the next request:

GET /v1/receipts?authorization_id=auth_01HXZ2A...&cursor=eyJjcmVhdGVkX2F0Ijoi...
Authorization: Bearer allowly_l1_s001_...

---

Offline verification

Fetch your workspace's public keys — no API key required, so auditors can access this directly:

GET /v1/workspaces/`{workspace_id}`/keys
{
  "workspace_id": "ws_01HXA1...",
  "keys": [
    {
      "key_id": "workspace-signing-key-version",
      "alg": "Ed25519",
      "public_key": "base64url-32-bytes",
      "active_from": "2026-04-01T00:00:00Z",
      "active_until": null
    }
  ]
}

Then verify with the reference verifier:

from allowly.verify import verify_receipt, load_keys_from_json

keys = load_keys_from_json(keys_doc)
verify_receipt(signed_receipt, keys)  # raises VerificationError if invalid

The verifier checks: version, all required fields present, no unknown fields, scope/event decision pairing, Ed25519 signature over the canonical payload. See the receipt format spec for the full algorithm.

---

Receipt structure

All receipts share the same flat structure. Scope receipts have scope; event receipts have event. Exactly one of those fields is present.

FieldTypeDescription
versionstringAlways "1.0"
receipt_idstringULID
workspace_idstringIssuer identifier
issued_atstringRFC 3339, UTC, millisecond precision
decisionstringallow, deny, confirm, escalate, authorization_granted, authorization_revoked, escalation_approved, or escalation_rejected
reasonstringMachine-readable reason code
user_idstringOpaque user identifier
agent_idstringOpaque agent identifier
scopestring \absentScope name for /check receipts. Absent on event receipts.
eventstring \absentauthorization.create, authorization.revoke, or escalation.resolve. Absent on scope receipts.
resourcestring \nullTarget identifier, or null
contextobjectIssuer-defined metadata
authorization_idstring \nullRelated authorization ID
policy_versionstringDecision logic version at time of issue
signatureobject{ alg, key_id, value }