Decision Receipts
Verify an immutable human decision locally before allowing a consequential operation to continue.
A decision receipt is a compact Ed25519-signed proof of a resolved Action. It is separate from the webhook transport signature: a webhook uses a per-Source HMAC secret, while a receipt can be verified offline with ActionBox's public key set.
What a receipt binds
Every newly resolved Action includes a receipt token. Its signed payload
contains the decision binding and resolution metadata:
| Claim | Why it matters |
|---|---|
action_id and environment | Prevents a receipt from being used for another Action or environment |
version and action_version | Identifies the exact material Action snapshot |
fingerprint | Binds the approval to the content that was reviewed |
status | Must be resolved |
response or option_id | Carries the typed human result |
reason, resolved_at, resolved_by | Preserves resolution context |
Older terminal records created before receipt support may have a null receipt.
Cancellation and normal expiration do not create a decision receipt. A safe
on_expire.resolve fallback does create one, with resolved_by: "system".
Fetch trusted public keys
Fetch the public JWK set over HTTPS and cache it according to your trust policy:
curl -fsS https://api.actionbox.cloud/.well-known/actionbox-receipt-keys.jsonThe equivalent versioned route is GET /v1/receipts/keys. Keys use the Ed25519
JWK shape (kty: "OKP", crv: "Ed25519", alg: "EdDSA", kid, and x).
The set can contain the active key and configured previous keys so old receipts
remain verifiable across rotation. Select the key by the receipt header's
kid; do not trust an unrecognized key ID.
Verify before execution
The token has three base64url segments: signed header, signed payload, and signature. A verifier should:
- Split exactly three segments and decode their JSON/header values.
- Require
alg=EdDSA,typ=actionbox-decision-receipt, andv=1. - Select a trusted JWK by
kidand verify the Ed25519 signature over the exact encodedheader.payloadbytes. - Require
iss=actionbox,receipt_version=1,status=resolved, and the required Action claims. - Compare
action_id,environment,action_version, andfingerprintto the Action snapshot your worker is about to execute. - Reject the operation if any check fails, the key is unknown, or the receipt is missing.
For example, the cryptographic part can be implemented with a standard Python Ed25519 library:
import base64
import json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def b64url(value: str) -> bytes:
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
def verify_receipt(token: str, jwk: dict) -> dict:
parts = token.split(".")
if len(parts) != 3:
raise ValueError("receipt must contain three compact segments")
encoded_header, encoded_payload, encoded_signature = parts
header = json.loads(b64url(encoded_header))
payload = json.loads(b64url(encoded_payload))
if (
not isinstance(header, dict)
or not isinstance(payload, dict)
or header.get("alg") != "EdDSA"
or header.get("typ") != "actionbox-decision-receipt"
or header.get("v") != 1
or not isinstance(header.get("kid"), str)
or header["kid"] != jwk.get("kid")
):
raise ValueError("unsupported receipt header")
if (
payload.get("iss") != "actionbox"
or payload.get("receipt_version") != 1
or payload.get("status") != "resolved"
):
raise ValueError("receipt is not a resolved Action")
Ed25519PublicKey.from_public_bytes(b64url(jwk["x"])).verify(
b64url(encoded_signature),
f"{encoded_header}.{encoded_payload}".encode("ascii"),
)
return payloadThe example assumes jwk was selected from a trusted, freshly fetched JWK
set by matching header["kid"]. Production code should also enforce the full
required-claim and key-shape checks above, reject non-canonical input, and
apply its own replay/age policy.
A valid receipt is not execution success
A receipt proves that the Action was resolved with a particular decision. It
does not prove that the downstream deployment, migration, or job succeeded.
After execution, report one immutable success or failed outcome with the
same action_version and fingerprint.
Receipt versus webhook signature
Use both controls for different boundaries:
| Control | Purpose | Secret or key |
|---|---|---|
| Ed25519 receipt | Offline proof of the resolved decision | Public JWK set |
| Webhook HMAC | Authenticate the callback delivery and raw body | Per-Source webhook secret |
Verify the HMAC before parsing a callback and deduplicate its event id; then
verify the receipt before authorizing any consequential work. See Webhooks and
callbacks, Decisions and outcomes,
and Security.
Next: Expiration and timeouts, Errors and safe retries, or Testing with sandbox keys.