Registries#
A registry is a stream that accepts exactly one commitment per key, such as one quarterly filing per company, and accounts for every request it receives. Every authenticated request is decided under a published policy and recorded in the registry's decision log, and the submitter gets a signed receipt for each one, refusals included. An auditor can then account for every request that reached Sigilbase, not only the ones that were accepted.
Registries are available on the Business and Enterprise plans, because a registry's decisions are anchored externally and published on its transparency feed. The feed stays on for the registry's whole life. Whether a stream is a registry is chosen when the stream is created, and can never be changed afterwards in either direction.
The policy#
A registry accepts nothing until its owner commits a policy. The policy is canonical JSON, and its SHA-256 hash appears on every receipt:
{
"v": 1,
"registry": "4f1c0e2a9b7d4c3e8a6f5b1d2c3e4f50",
"commitment_key_pattern": "[a-z0-9-]+-2026-q[1-4]",
"payload_schema": {
"type": "object",
"required": ["filing_hash"],
"properties": {
"filing_hash": {"type": "string", "pattern": "sha256:[0-9a-f]{64}"},
"pages": {"type": "integer"}
},
"additionalProperties": false
},
"windows": [
{"key": "acme-2026-q3", "open_at": "2026-10-01T00:00:00Z", "close_at": "2026-10-31T00:00:00Z"}
],
"throttle_record_cap": 60,
"msd_seconds": 900,
"receipt_key_id": "…",
"amendment": "frozen"
}
| Field | Meaning |
|---|---|
registry |
The stream's public id. |
commitment_key_pattern |
A regular expression, at most 512 characters, that every key must match in full. The pattern must compile on its own. Keys are at most 128 characters. |
payload_schema |
A restricted JSON Schema: a top-level object with required, up to 32 properties typed string (optionally with pattern and maxLength) or integer, and additionalProperties: false. A submission with any field the schema does not name is refused, which keeps data nobody agreed to collect out of the registry. |
windows |
The expected keys, up to 100,000, each with the period it accepts submissions: from open_at (inclusive) to close_at (exclusive). Times are RFC 3339 with a time zone; a date that does not exist, such as 30 February, is refused. |
throttle_record_cap |
60: the number of throttled requests per key per hour that are recorded individually. |
msd_seconds |
900: the maximum seal delay every receipt promises. |
receipt_key_id |
The receipt key active when the policy is committed. |
amendment |
frozen (the default) means the policy can never change. notice, together with notice_seconds (from 3600, an hour, to 3155760000, a hundred years), allows amendments that take effect only after that notice period. |
Committing appends policy.committed as event 1 of the registry's decision log, carrying the policy without its windows. The windows follow in policy.windows records, numbered part 1 to parts, because a single record holds at most 32 KiB. Join the parts' windows in order and put them back into the policy, and the SHA-256 of that policy's canonical form is policy_hash. Under notice, an amendment appends policy.amended the same way, with its windows following, and an effective_at. Each receipt carries the hash of the policy that was in force at the receipt's received_at. Every version is published:
GET /api/v1/streams/{slug}/policy versions, hashes, effective_at, the version in force
GET /api/v1/streams/{slug}/policy/{version} one version's canonical JSON, exactly as hashed
Submitting#
A registry takes single submissions to the ordinary events endpoint. The body carries commitment_key beside the usual event fields, and nothing else: idempotency_key, pii and subject are not accepted, because the commitment key plays the role of the idempotency key.
curl -s -X POST https://app.sigilbase.io/api/v1/streams/filings-2026/events \
-H "Authorization: Bearer sgb_example.xxxx" \
-H "Content-Type: application/json" \
-d '{
"commitment_key": "acme-2026-q3",
"occurred_at": "2026-10-24T09:00:00Z",
"actor": "filer:acme",
"action": "filing.submitted",
"payload": {"filing_hash": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"}
}'
The batch endpoint is refused on a registry with 422 (batch_not_allowed), because a batch would have to succeed or fail as a whole, while a registry decides each key separately.
Outcomes#
Each request is checked in the order below, and the first check that matches decides the outcome. Every outcome is recorded in the decision log at decision_seq and answered with its signed receipt, except for throttles beyond the hourly cap, which are only counted.
| Outcome | HTTP | When |
|---|---|---|
refused_scope |
403 | The API key is not scoped to this registry. |
throttled |
429 | The key or the workspace is over its rate limit. The first 60 throttled requests per key per hour are recorded; after that they are counted, and each hour's count is recorded as throttled.summary once the hour is over. A request the rate limiter has already refused is a throttle even from a key not scoped to the registry. |
refused_paused |
423 | The registry is paused. |
refused_archived |
410 | The registry is archived. |
invalid |
422 | The body, the key pattern or the payload schema refused the request; reasons lists the codes, for example missing:filing_hash, pattern:filing_hash, unexpected_field, commitment_key_pattern or unknown_key. Reasons never quote what was sent. |
refused_early |
422 window_not_open |
The key's window has not opened yet. |
refused_late |
422 window_closed |
The key's window has closed. |
accepted |
201 | This is the key's first valid commitment; the event is appended. |
replay |
200 | The key already holds this exact content. |
conflict |
409 commitment_conflict |
The key already holds different content. |
Every answer has the same shape:
{
"outcome": "accepted",
"id": "9b0f…",
"sequence": 1,
"entry_hash": "8c1f…",
"received_at": "2026-10-24T09:00:01.204311Z",
"decision_seq": 2,
"recorded": true,
"receipt": {"body": {"outcome": "accepted", "commitment_key": "acme-2026-q3", "policy_hash": "sha256:…", "…": "…"}, "signature": "base64…"},
"receipt_pending": false
}
A suspended workspace is refused with 403 (tenant_suspended) before anything is decided, and nothing is recorded. A registry never accepts anything it cannot receipt. If the receipt signer or the database fails, the whole decision is rolled back and the answer is 503 (registry_unavailable) with nothing recorded, so retry. Before a policy is committed, every request is answered 409 (registry_not_ready) and nothing is recorded.
received_at, and every comparison against a window, use a single reading of the database clock taken while the decision is made. The time a receipt says a request arrived is therefore the same time its window was judged against. A body that is not a valid submission is still receipted: its digest_kind is raw, and its request_digest is the SHA-256 of the exact bytes you sent.
A registry's events are exactly its accepted commitments: imports and every other way of writing to a stream are refused for a registry. If a payload is ever redacted, the payload.redacted declaration goes to the workspace's system stream and names the registry, as it does for an archived stream, so the registry's events stay its commitments.
Window closes#
Each minute, every key whose window has closed gets a window.closed record in the decision log, saying committed (naming the accepted event) or missing. Closing a window never changes an outcome, because each refusal was already decided by received_at.
Reading the log#
The decision log is an ordinary stream, hash-chained, sealed, anchored and published:
GET /api/v1/streams/{slug}/decisions?page=N
GET /api/v1/streams/{slug}/decisions/{decision_seq}/receipt
GET /api/v1/streams/{slug}/events/{sequence}/receipt
An accepted decision's receipt is the same receipt as the one for the event it accepted.
Besides each request's decision, the log records:
| Record | When |
|---|---|
policy.committed, policy.windows, credentials.snapshot |
The first policy commit: the policy, its windows in parts, and every API key able to submit at that moment. |
policy.amended (with its policy.windows) |
An amendment under a notice policy. |
credential.issued, credential.rotated, credential.revoked, credential.expired |
An API key that can reach the registry (scoped to all streams or to this one) is issued, rotated, revoked, or lapses. |
stream.paused, stream.resumed, stream.archived |
The registry's state changes. |
receipt_key.rotated |
The key that signs receipts changes. |
auditor.granted, auditor.revoked |
An auditor grant covering the registry is created or revoked. |
throttled.summary |
Each hour's throttles beyond the record cap, per credential, once the hour is over. |
window.closed |
A key's window closes. |
People appear only as pseudonymous references (user: and an id), and auditors only by their grant's id; keys appear by their public key id. The set of credentials able to submit at any moment can be rebuilt from the log alone. To let an auditor account for every request, grant them the registry and its decision log.