Reading and proofs#
Everything you write can be read back, and every sealed event can be proven to belong to its stream. Reads use the same API key as writes.
Listing events#
GET /api/v1/streams/{slug}/events
curl -s "https://app.sigilbase.io/api/v1/streams/admin-actions/events?limit=50" \
-H "Authorization: Bearer sgb_example.xxxx"
Two query parameters:
| Parameter | Default | Rules |
|---|---|---|
limit |
50 | 1 to 200 |
after |
0 | Return events with a sequence greater than this |
The response is a page of events in sequence order plus a cursor:
{
"data": [
{
"id": "0197c9a4-1b2f-7c3d-9e4a-5f6b7c8d9e0f",
"sequence": 1,
"occurred_at": "2026-07-09T09:15:00.000000Z",
"received_at": "2026-07-09T09:15:03.412876Z",
"actor": "user:42",
"action": "role.granted",
"resource": "user:9f31",
"payload": {"role": "admin", "granted_by": "user:42"},
"payload_hash": "11294bc41a363046cae292e51e14a892de8f459f498fd9dd4381f98a4170550b",
"prev_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"entry_hash": "9b89974c651b484589b61d710f1bc0284e7788bf3ace3e833a46f5729d694c01",
"checkpoint_id": null
}
],
"next_cursor": null
}
next_cursor is the last sequence on the page when the page is full, and null when you have reached the end. Pass it back as after to continue. Because the cursor is the sequence number itself, pagination is stable: appends never shift earlier pages.
A single event is available at GET /api/v1/streams/{slug}/events/{sequence}, wrapped in a data key.
Checkpoints#
GET /api/v1/streams/{slug}/checkpoints
Returns every checkpoint for the stream in order, each with its range (sequence_from, sequence_to), merkle_root, prev_checkpoint_hash, checkpoint_hash, signature, and the public_key that signed it.
The proof endpoint#
GET /api/v1/streams/{slug}/events/{sequence}/proof
Once an event has been sealed, this returns everything needed to prove its inclusion without trusting Sigilbase. Before sealing, it returns 409 with {"message": "This event has not been sealed into a checkpoint yet; try again after the next sealing run."}.
A worked example#
Every value below is real and recomputable; work along with any SHA-256 tool. The stream's id is 0197c8e2-4a51-7000-8000-2f9d3b6a1c44 and it holds four sealed events. We prove event 3.
GET /api/v1/streams/admin-actions/events/3/proof
{
"event": {
"id": "0197c9a6-88e1-7d02-b1c9-3a7e5d0f2b61",
"sequence": 3,
"occurred_at": "2026-07-09T09:17:30.000000Z",
"received_at": "2026-07-09T09:17:31.008212Z",
"actor": "service:billing",
"action": "invoice.voided",
"resource": "invoice:1042",
"payload": {"amount_pence": 12500, "reason": "duplicate"},
"payload_hash": "b5283aeb255ecf4911a420ffc236fc6f9870fe1d8f9b9342192254ed18a02c4e",
"prev_hash": "73ead8c1b8c931829b4725d220ec5d9cf53a382e457e4717838fcd12eb37e1d8",
"entry_hash": "150ad25255a16073ce6079a53d522ce490094cbde8cab17ec0f94c0171502afe",
"checkpoint_id": "0197c9b0-1234-7abc-8def-0123456789ab"
},
"proof": {
"leaf_index": 2,
"audit_path": [
{"hash": "f7006eb55a8407c1b2007e1628d4d7689cf6b3cd59a5c34c36bb27630cfcd345", "side": "right"},
{"hash": "caf1b8b503b9d726c61e2e29e0eb63025f172974c9f27808dfbf2f06c8bb732b", "side": "left"}
],
"merkle_root": "4a157629882340d739370041b824b38334a98777cfb805b89de4a7551c27b0d2"
},
"checkpoint": {
"id": "0197c9b0-1234-7abc-8def-0123456789ab",
"sequence_from": 1,
"sequence_to": 4,
"merkle_root": "4a157629882340d739370041b824b38334a98777cfb805b89de4a7551c27b0d2",
"prev_checkpoint_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"checkpoint_hash": "1d78a3d9d7fbe21e18cdeaf6acec0eca75ad647a66b057e1e96873b37abbdec9",
"signature": "fbaeacd76624ef225988cb7b6c9112a25f57b8bab2e081659fa7ff17ca42a00828f5ebf77a613fbd31377e1fd10695e77cf7e63c3585bfd222c7eb1dd6770208",
"public_key": "2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"
}
}
leaf_index is the event's zero-based position inside the checkpoint range: sequence - sequence_from, here 3 - 1 = 2.
Step 1: recompute the payload hash#
Encode the payload as canonical JSON (RFC 8785: keys sorted, no whitespace) and hash it:
{"amount_pence":12500,"reason":"duplicate"}
SHA-256 of that exact string is b5283aeb...18a02c4e, matching payload_hash.
printf '%s' '{"amount_pence":12500,"reason":"duplicate"}' | sha256sum
Step 2: recompute the entry hash#
Build the entry preimage: a canonical JSON object of the event's fields, the payload hash, and the previous event's entry hash (prev). Sequence 1 uses sixty-four zeros as prev.
{"action":"invoice.voided","actor":"service:billing","occurred_at":"2026-07-09T09:17:30.000000Z","payload_hash":"b5283aeb255ecf4911a420ffc236fc6f9870fe1d8f9b9342192254ed18a02c4e","prev":"73ead8c1b8c931829b4725d220ec5d9cf53a382e457e4717838fcd12eb37e1d8","received_at":"2026-07-09T09:17:31.008212Z","resource":"invoice:1042","seq":3,"stream":"0197c8e2-4a51-7000-8000-2f9d3b6a1c44","v":1}
SHA-256 of that string is 150ad252...71502afe, matching entry_hash. Because prev is inside the preimage, changing any earlier event changes this hash too: that is the chain.
Step 3: climb the audit path to the root#
The Merkle tree follows RFC 6962: a leaf is SHA-256(0x00 || entry_hash_bytes) and an inner node is SHA-256(0x01 || left || right), all over raw bytes.
- Leaf:
SHA-256(0x00 || 150ad252...)=671c26a503582abcb2012cc908f1dfffbacff49df00b2ff208bd5acb4a7a9916 - First path step, sibling on the right:
SHA-256(0x01 || 671c26a5... || f7006eb5...)=1c4ea013dc4802b205feaad8b0542dc36ff27ac76145cf5a3bb5fa01688ee42c - Second path step, sibling on the left:
SHA-256(0x01 || caf1b8b5... || 1c4ea013...)=4a157629882340d739370041b824b38334a98777cfb805b89de4a7551c27b0d2
The result equals the checkpoint's merkle_root. The event is in the sealed set.
Step 4: check the seal#
The checkpoint hash commits to the root, the range, and the previous checkpoint:
{"created_at":"2026-07-09T09:20:00.000000Z","from":1,"prev_checkpoint":"0000000000000000000000000000000000000000000000000000000000000000","root":"4a157629882340d739370041b824b38334a98777cfb805b89de4a7551c27b0d2","stream":"0197c8e2-4a51-7000-8000-2f9d3b6a1c44","to":4,"v":1}
SHA-256 of that string is 1d78a3d9...7abbdec9, matching checkpoint_hash. Finally, verify the Ed25519 signature over the raw bytes of checkpoint_hash against public_key:
php -r '
var_dump(sodium_crypto_sign_verify_detached(
hex2bin("fbaeacd76624ef225988cb7b6c9112a25f57b8bab2e081659fa7ff17ca42a00828f5ebf77a613fbd31377e1fd10695e77cf7e63c3585bfd222c7eb1dd6770208"),
hex2bin("1d78a3d9d7fbe21e18cdeaf6acec0eca75ad647a66b057e1e96873b37abbdec9"),
hex2bin("2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12")
));
'
It prints bool(true). The published signing keys are available without authentication at GET /api/v1/keys, so a verifier never needs an API key of yours.
If any byte anywhere had changed (in the payload, the event fields, an earlier event, or the checkpoint) one of these four steps would have failed. For verifying whole ranges offline, use evidence bundles.
Consistency between exports#
An inclusion proof shows one event belongs to one sealed set. A consistency proof answers a different auditor question: is this quarter's export the same history I verified last quarter, plus new entries — or was something rewritten in between?
The object under proof is the cumulative tree: the RFC 6962 Merkle tree over entry hashes 1..n. Every checkpoint boundary defines such a tree state (tree_size = the checkpoint's sequence_to). A consistency proof between two states is a handful of node hashes showing the older tree is a prefix of the newer one — no re-download of history required.
Prove July's export extends June's#
Suppose June's bundle covered events 1..2 and July's covers 1..4. Run the bundled verifier over both at once:
php verify.php --consistency june.zip july.zip
It verifies each bundle fully, then proves consistency between them. Or, if you only kept the two values the verifier printed after June's run (tree_size=2 and the cumulative root), check July's bundle against the record:
php verify.php --consistency july.zip \
--root fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125 \
--size 2
Business and Enterprise accounts can also fetch the proof live, between any two checkpoint boundaries:
curl -H "Authorization: Bearer $SIGILBASE_KEY" \
"https://sigilbase.test/api/v1/streams/admin-actions/consistency?from=2&to=4"
{
"stream": "admin-actions",
"from": {"tree_size": 2, "root": "fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125", "checkpoint_hash": "..."},
"to": {"tree_size": 4, "root": "d37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7", "checkpoint_hash": "..."},
"proof": ["5f083f0a1a33ca076a95279832580db3e0ef4584bdff1f54c8a360f50de3031e"]
}
The single proof node here is the root of the subtree holding entries 3..4. Recombine by hand: SHA-256(0x01 || from.root || proof[0]) — the old root joined with the new subtree — equals to.root exactly (these are the RFC 6962 reference-vector values; recompute them yourself). Because the old root participates in rebuilding the new one, the new tree contains the old tree unchanged: nothing before sequence 2 was modified, deleted, or reordered. A rewritten history cannot produce such a proof against the root you recorded.