Skip to content
Sigilbase

Receipts#

Every append through the events API is answered with a receipt: Sigilbase's signed statement of what it decided about your request, made the moment it decided, before the event is sealed. A receipt is evidence you hold. If the ledger ever disagreed with what you were told, the receipt would show it, because only Sigilbase's receipt key could have signed it.

Keep every receipt you are given. The PHP SDK can store them for you, and can export the directory an auditor reconciles a registry against.

What a receipt says#

{
  "body": {
    "v": "sigilbase-receipt/1",
    "tenant": "0f1e2d3c4b5a69788796a5b4c3d2e1f0",
    "stream": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "outcome": "accepted",
    "request_digest": "sha256:4e07…",
    "digest_kind": "canonical",
    "credential": "sgb_filer000000001",
    "accepted": {"sequence": 12, "entry_hash": "8c1f…", "request_digest": "sha256:4e07…"},
    "received_at": "2026-07-09T09:15:03.412876Z",
    "seal_by": "2026-07-09T09:30:03.412876Z",
    "key_id": "7a19…",
    "signed_at": "2026-07-09T09:15:03.415102Z"
  },
  "signature": "base64…"
}
Field Meaning
v sigilbase-receipt/1
tenant, stream The public ids of the workspace and of the stream the request was sent to
outcome What Sigilbase decided: accepted, replay or conflict, and on a registry also invalid, refused_early, refused_late, refused_scope, refused_paused, refused_archived or throttled
request_digest SHA-256 of the canonical form of your request's occurred_at, actor, action, resource, the payload's hash and, on a registry, the commitment_key. Two requests are the same exactly when their digests match
digest_kind canonical; on a registry raw when the body was not a valid submission, and then the digest is SHA-256 of the exact bytes you sent
credential The public id of the API key that sent the request, never its secret
accepted The event your request is accounted to, with the digest it was accepted under; null when nothing was accepted
received_at When Sigilbase decided
seal_by The latest time a signed checkpoint sealing the record will exist: fifteen minutes after received_at (on a standard stream a replay carries the original event's deadline; on a registry a replay's deadline is its own decision record's)
decision_seq When the decision was recorded in the stream's decision log: its sequence there
idempotency_key, commitment_key, policy_hash, reasons The key you sent; on a registry, the commitment key, the hash of the policy in force, and reason codes for a refusal
key_id, signed_at The receipt key that signed (the SHA-256 of its public key), and when

The signature is Ed25519 over the bytes sigilbase-receipt/1, a newline, and the RFC 8785 canonical form of body. Receipt keys are published at GET /api/v1/receipt-keys, retired ones included, and are never used to sign checkpoints, so no signature can be passed off as the other kind.

If the receipt signer is briefly unavailable, an append to a standard stream still succeeds: the response says "receipt": null, "receipt_pending": true, and the receipt is signed moments later and served at GET /api/v1/streams/{slug}/events/{sequence}/receipt. A registry never accepts anything it cannot receipt: it answers 503 and records nothing.

Checking a receipt#

The open-source verifier (1.8 and later) checks a receipt on its own, or against bundles:

php verify.php --receipt receipt.json
php verify.php --receipt receipt.json stream.zip decision-log.zip

On its own, it checks the signature against the receipt keys it carries, or against --receipt-keys <file> fetched from /api/v1/receipt-keys, and that the key was active when it signed. Given the stream's evidence bundle, it checks the event the receipt names carries the stated entry hash and request digest, and that it was sealed by a checkpoint signed no later than seal_by; a later seal fails with reason MSD_BREACH. Given the decision log's bundle, it checks the decision record says what the receipt says. A receipt signed by a key the verifier does not trust is UNCONFIRMED, not a pass.