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.