ActionBoxDOCS

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:

ClaimWhy it matters
action_id and environmentPrevents a receipt from being used for another Action or environment
version and action_versionIdentifies the exact material Action snapshot
fingerprintBinds the approval to the content that was reviewed
statusMust be resolved
response or option_idCarries the typed human result
reason, resolved_at, resolved_byPreserves 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.json

The 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:

  1. Split exactly three segments and decode their JSON/header values.
  2. Require alg=EdDSA, typ=actionbox-decision-receipt, and v=1.
  3. Select a trusted JWK by kid and verify the Ed25519 signature over the exact encoded header.payload bytes.
  4. Require iss=actionbox, receipt_version=1, status=resolved, and the required Action claims.
  5. Compare action_id, environment, action_version, and fingerprint to the Action snapshot your worker is about to execute.
  6. 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 payload

The 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:

ControlPurposeSecret or key
Ed25519 receiptOffline proof of the resolved decisionPublic JWK set
Webhook HMACAuthenticate the callback delivery and raw bodyPer-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.

On this page