Skip to content
Registry StackDocsDevelopment (unreleased)

Base Registry Engine API reference

View as Markdown

Look up here the HTTP API and events a compiled Base Registry Engine (BReg) package serves. Every registry serves the same route shapes over the Record types, fields, profiles, and Actions your project declares. API stability and versioning states what v1.0.0 promises.

For the request and response schemas of each operation, use the API operations viewer, rendered from the facility template’s compiled OpenAPI document. A running server publishes its own document at /openapi.json, filtered to the caller’s routes, with the exact schemas its package compiled.

For what Actions and Proposals are and how to declare them, use Actions and Proposals. For deployment and recovery, use deploy a registry. For receivers and replay, use bind webhook receivers. For every YAML key, use the configuration reference.

Every request except /health, /healthz, and /ready carries Authorization: Bearer <token>. The token is a JWT from the configured issuer that carries iss, aud, and exp. Its scopes come from the claim named by scopeClaim, split on scopeSeparator; the principal and purpose claims are the ones named under authorityClaims. A missing or rejected token answers 401 authentication.refused before any profile is selected.

Each route has one default access profile; select another with the accessProfile query option. A profile admits the caller when the token satisfies its requiredScopes, requiredPurposes, and principalClaim, and the profile grants the operation on that Record type.

An unknown route, an unsupported method, an unknown or ungranted profile, and an ungranted operation all answer the same 404 resource.not_found. Reads conceal records outside the caller’s boundary; see Read refusals. A mutation rechecks authority over its targets and prospective values and answers 412 precondition.failed when that fails, with no protected values and no effects.

Placeholders use the router’s snake_case form. {route} is the Record type’s configured route, {record_id} a record UUID, {read_path} a declared read path, {action} an Action ID, and {transition} one of the promoted Proposal transitions. Read routes answer GET only; only the probes also answer HEAD.

RouteOperationAvailable whenResponse
GET /v1/registrymetadataThe caller reaches at least one routeCaller-filtered Record types, operations, Action descriptors, and native Proposal capabilities. A caller with no visible route gets the concealed 404.
GET /v1/schemas/{record_type_id}metadataThe caller reaches the Record typeJSON Schema for one record of the Record type named by its ID, the target of the describedby link.
GET /openapi.jsonmetadataThe caller reaches at least one routeThe compiled OpenAPI document, filtered to the caller's routes.
GET /health and GET /healthzlivenessAlways{"status":"alive"}.
GET /readyreadinessAlways{"status":"ready"}, or 503 runtime.not_ready until the package and database are usable.
POST /v1/records/{route}createConfigured Record type with this granted operation201 with the record envelope, Location, and ETag.
GET /v1/records/{route}listConfigured Record type with this granted operationCollection.
GET /v1/records/{route}/{record_id}getConfigured Record type with this granted operationRecord envelope with ETag.
GET /v1/records/{route}/{record_id}/access-loggetRecord type declares accessLog; the authenticated caller reaches the record through get and is its current subjectA bounded page of the record's retained subject-facing access events. limit defaults to 50 and is at most 100; cursor continues from an event ID.
PATCH /v1/records/{route}/{record_id}patchmutationMode: mutableRecord envelope with data.snapshot. Requires If-Match.
DELETE /v1/records/{route}/{record_id}tombstonetombstone: trueEnvelope of the final revision with data.snapshot. Requires If-Match.
POST /v1/records/{route}:lookuplookupRecord type declares selectorProfilesRecord envelope, or 404 lookup.unresolved.
POST /v1/records/{route}:batchbatchRecord type declares batch{"snapshot", "results": [...]}.
POST /v1/records/{route}/ingestion-runsbatchRecord type declares batch201 {"run"}: the run document with the binding, the announced counts and bounds, and nextChunkIndex.
GET /v1/records/{route}/ingestion-runsbatchRecord type declares batch{"runs", "hasMore", "nextCursor"} over the runs the caller created. Accepts accessProfile, limit (1 to 100, default 25), the keyset cursor from nextCursor, status, and inputDigest. after answers 400 query.invalid; any other key answers the concealed 404.
GET /v1/records/{route}/ingestion-runs/{run_id}batchRecord type declares batch{"run"}. Another caller's run and an unknown run are the same 404.
POST /v1/records/{route}/ingestion-runs/{run_id}/chunksbatchRecord type declares batch{"run", "receipt"} after a commit or an exact replay. Takes no Idempotency-Key.
POST /v1/records/{route}/ingestion-runs/{run_id}/cancelbatchRecord type declares batch{"run"} after cancelling an open or blocked run. Empty body, no Content-Type, no Idempotency-Key.
GET /v1/records/{route}/ingestion-runs/{run_id}/chunks/{chunk_index}/receiptbatchRecord type declares batchThe chunk receipt, or 410 ingestion.receipt_erased once the record history it describes is erased.
GET /v1/records/{route}/{record_id}/revisionsrevisionsConfigured Record type with this granted operationCollection of the record's revisions, at most 100 per read.
GET /v1/records/{route}/{record_id}/revisions/{revision}revisionsConfigured Record type with this granted operationRecord envelope for one revision.
GET /v1/records/{route}:snapshotsnapshotConfigured Record type with this granted operationCollection as recorded at a saved snapshot.
GET /v1/records/{route}:currentlistRecord type declares temporalCollection of the records valid now.
GET /v1/records/{route}:as-oflistRecord type declares temporalCollection of the records valid at asOf.
GET /v1/records/{route}/{record_id}/{read_path}list through readPaths[]Record type declares readPathsCollection of related records.
POST /v1/actions/{action}/target-conditionsinvokeImmediate Action advertises required target conditions{"preconditions": {...}} for the targets the input names.
POST /v1/actions/{action}/availabilityinvokeImmediate Action has at least one reference inputAdvisory availability for one referenced target: true or false with its observed revision, or null when more input is required or the current profile cannot inspect the guard safely.
POST /v1/actions/{action}invokeAction mode is immediate and the caller holds Invoke{"action", "applicationId", "results"}. Requires Idempotency-Key.
POST /v1/review-completionsreview completion receiverA review authority configures a completion token and recipient binding204 after durable acceptance or an exact replay. Requires bearer authentication, Registry-Recipient-Binding, and an Idempotency-Key equal to the completion event ID.
GET /v1/gisGIS adapterlistener.publicOrigin is configured; the response includes only GIS collections visible to the callerLanding document with links to the service description, conformance document, and collection inventory.
GET /v1/gis/apiGIS adapterlistener.publicOrigin is configured; the response includes only GIS collections visible to the callerCaller-filtered OpenAPI 3.0 description for the visible collection item routes.
GET /v1/gis/conformanceGIS adapterlistener.publicOrigin is configuredAn empty conformsTo array; the bounded adapter makes no OGC conformance claim.
GET /v1/gis/collectionsGIS adapterlistener.publicOrigin is configured; the response includes only GIS collections visible to the callerCollection inventory for direct list profiles with readable primary geometry and an explicit bbox grant.
GET /v1/gis/collections/{collection}GIS adapterlistener.publicOrigin is configured and the caller reaches the governed GIS collectionCollection detail with the Record type, access profile, primary geometry, page bound, fields, and item link.
GET /v1/gis/collections/{collection}/itemslist through the GIS adapterlistener.publicOrigin is configured and the caller reaches the governed GIS collectionAuthorized GeoJSON FeatureCollection page. Accepts bbox, limit, cursor, and f=json.
POST /v1/actions/{action}/proposalsstartAction mode is proposal and the caller holds Start with target authorityDurable Proposal with its current revision and promoted links. Requires Idempotency-Key.
GET /v1/proposalslistCaller holds explicit Proposal List authority{"items", "nextCursor"} page of the Proposals the caller may see. Accepts only accessProfile, action, owner=caller, state, turn, target (a record UUID), cursor, and limit (1 to 100, default 50), each at most once. top answers 400 query.invalid; any other key, a repeated key, or a bad value answers the concealed 404. A page may hold fewer than limit items.
GET /v1/proposals/{proposal_id}getCaller holds Proposal Get and current ownership/target scopeProposal with its frozen binding, application status, authorized links, and ETag.
GET /v1/proposals/{proposal_id}/submissions/{submission_version}getCaller holds Proposal Get and current ownership/target scopeOne exact submission: its frozen effectDigest, its review result, its decision when one was made, and its application outcome as separate members, or 410 proposal.submission_expired once its result window has lapsed and no application receipt remains.
POST /v1/proposals/{proposal_id}/{transition}Promoted transitionProposal promotes the exact authorized transitionUpdated Proposal with a new ETag. Requires Content-Type: application/json, Idempotency-Key, and If-Match carrying the Proposal ETag.
GET /v1/proposals/{proposal_id}/review-statusget-review-statusCaller holds a matching Proposal review-status grantClosed review-status document; service grants are authority-scoped and value-free.
GET /v1/proposals/{proposal_id}/review-contextget-review-contextCaller holds Proposal context and current record disclosureOnly permitted review context fields.
GET /v1/proposals/review-contextget-review-contextEach item requires current Proposal context and Record disclosure authority under the selected profilePass 1 to 100 distinct canonical Proposal UUIDs in comma-separated ids and select accessProfile. Ordered items contain each ID with status: visible and its context, or status: concealed without a document. Operational failures refuse the whole batch; the response is bounded at 8 MiB.
GET /v1/proposals/{proposal_id}/attachments/{slot_id}Attachment downloadProposal declares the slot and promotes its caller-authorized downloadRetained bytes under current Proposal disclosure and retention.
PATCH /v1/proposals/{proposal_id}/attachments/{slot_id}Attachment uploadDraft Proposal declares the slot and promotes its caller-authorized uploadUpdated Proposal after bounded upload. Requires Idempotency-Key and If-Match carrying the Proposal ETag.
DELETE /v1/proposals/{proposal_id}/attachments/{slot_id}Attachment removalDraft Proposal declares the slot and promotes its caller-authorized removalUpdated Proposal after removal. Requires Idempotency-Key and If-Match carrying the Proposal ETag.

A successful GET /v1/registry carries a Registry-Engine-Version header with the engine release that served it, the value breg --version prints. The concealed 404 does not carry it.

Reads, creates, patches, tombstones, revisions, and snapshots return the Registry Record envelope:

{
"data": {
"recordIdentifier": "2d7e5e0c-5b2c-4b8e-9f1e-0d3c1a2b3c4d",
"revisionIdentifier": "3",
"domainData": {
"code": "SITE-001",
"label": "North depot",
"location": {"type": "Point", "coordinates": [30.5234, -1.9441]}
}
},
"meta": {
"registryIdentifier": "asset-registry",
"datasetIdentifier": "asset-sites",
"entityTypeIdentifier": "asset-site"
}
}
MemberTypeMeaning
data.recordIdentifierstring, UUIDStable identity of the record.
data.revisionIdentifierstring, ^[1-9][0-9]*$Revision counter, starting at 1. Compare as a string.
data.domainDataobjectThe record’s fields under their apiName, narrowed to the grant’s readableFields and to $select.
data.snapshotstringOn mutation responses only: an opaque token for :snapshot reads, at most 4096 bytes.
meta.registryIdentifier, meta.datasetIdentifier, meta.entityTypeIdentifierstringsThe registry, dataset, and Record type IDs from the compiled package.

Fields travel under their apiName: by default the camelCase form of the field ID, so household-code is householdCode. The field ID is never accepted in its place.

HeaderValue
Content-Typeapplication/json, or application/ld+json when the request sent Accept: application/ld+json. The JSON-LD form adds @context pointing at https://id.registrystack.org/contexts/registry-record/v1.
ETag"breg-hmac-sha256:<64 hex>", a strong validator of the current revision. Send it back in If-Match.
Link<https://id.registrystack.org/profiles/registry-record/v1>; rel="profile", then </v1/schemas/{record_type_id}>; rel="describedby".
LocationOn 201 Created: /v1/records/{route}/{record_id}.

Each Record type’s generated JSON Schema carries these encodings.

Field typeJSON encodingNotes
booleantrue or false
stringstringminimumLength and maximumLength from the field.
textstringmaximumLength from the field.
int64integerFull 64-bit range; clients that parse JSON numbers as doubles lose precision above 2^53.
decimalstringDecimal digits with the declared precision and scale, never a JSON number.
datestringRFC 3339 full date.
timestampstringRFC 3339 date-time.
uuidstringLower-case UUID.
vocabulary-codestringOne of the vocabulary values; the schema carries them as an enum with x-registry-vocabulary.
referencestringThe target record identifier as a UUID.
crs84-pointobjectGeoJSON Point, {"type": "Point", "coordinates": [longitude, latitude]}.
structuredobjectValidated against the declared schema, bounded by maximumBytes, advertised as x-registry-maxBytes.

A collection read returns items, pageInfo, and the same meta as a single record. Collection members carry recordIdentifier, revisionIdentifier, and domainData directly, so the field path is items[0].domainData, not items[0].data.domainData:

{
"items": [{"recordIdentifier": "…", "revisionIdentifier": "1", "domainData": {}}],
"pageInfo": {"nextCursor": "eyJ…"},
"meta": {"registryIdentifier": "asset-registry", "datasetIdentifier": "asset-sites", "entityTypeIdentifier": "asset-site"}
}

nextCursor is null on the last page. To continue, send it as $skiptoken with no other option except accessProfile. The cursor is signed with the runtime cursor.secretRef, binds the whole original query, and expires after cursor.maximumAgeSeconds; a cursor that fails any of those checks answers 400 query.cursor_invalid. With $count, the response carries the matching total.

Options apply only where the grant permits them: $select draws from readableFields, $filter from filterableFields, $orderby from sortableFields, and $count needs allowCount. Anything else answers 400 query.invalid with fieldPath naming the parameter. An unknown field and a withheld field return the same problem, and the field itself is never named.

OptionRoutesMeaningBound
accessProfileEvery record routeEvaluate the request under a profile other than the route default. The caller must satisfy that profile.One profile identifier.
$selectCollection readsComma-separated domain fields to return. Envelope members are never selectable.128 fields.
$filterCollection readsPredicate over filterableFields: eq, ne, lt, le, gt, ge, in, startswith(), contains(), combined with and, or, not.32 predicates, depth 16, 128 nodes, 100 in values, 1024-byte literals.
$orderbyCollection readsOne field from sortableFields, ascending. A desc direction is refused with query.invalid.One field.
$topCollection readsPage size.100.
$countCollection readsAdds the matching count. Refused unless the grant sets allowCount.Grant flag.
$skiptokenCollection readsContinuation from pageInfo.nextCursor. It carries the original query, so repeating snapshot or validAt beside it is refused.Expires after cursor.maximumAgeSeconds.
bboxlist on Record types with a crs84-point fieldwest,south,east,north in CRS84 degrees, permitted by spatialQueries.bbox on the grant.256 bytes, plus the span limits on the grant.
asOf:as-ofThe valid-time instant to evaluate, in the type of the Record type's valid-time fields.One value.
snapshot:snapshotA data.snapshot value returned by an earlier mutation.4096 bytes.
validAt:snapshotThe valid-time instant to evaluate inside the snapshot, for temporal Record types.One value.
Whole query stringEvery routeDecoded size of all options together.16 KiB.
$filter=status eq 'active' and (region in ('north','east') or startswith(label,'Depot'))
$filter=not contains(label,'closed') and validFrom ge 2026-01-01
bbox=30.0,-2.1,30.9,-1.8

Strings are single quoted, numbers and dates are bare, and null compares with eq and ne. Literals must fit the field type.

A read never reveals whether a record outside the caller’s boundary exists. Row boundaries, membership boundaries, and consent requirements all answer the same way, and each read re-evaluates them against current data.

ReadAnswer
get, lookup, or access-log of a record outside the row, membership, or consent boundary404 resource.not_found, or 404 lookup.unresolved for a lookup, identical to an absent record.
Collection, :snapshot, $count, or $skiptoken continuation over such records200 with those records left out and the count taken over the visible records only. A cursor replay re-evaluates the boundary.
Revisions of such a record404 resource.not_found, identical to an absent record.
GET /v1/records/{route}/{record_id}/{read_path} for a path the selected profile holds no read-path grant for404 resource.not_found, identical to an unknown path.
A read path rooted at a record outside the boundary200 with no items, identical to an absent root record. No related record is disclosed.
$select, $filter, or $orderby naming a field outside the read-path grant400 query.invalid; the field is not named.

Every write needs Content-Type: application/json, except a patch, which needs application/json-patch+json. A refused body answers 400 request.invalid; see Problem documents for its fieldPath.

POST /v1/records/{route} with the fields the grant lists in writableFields:

{"data": {"code": "SITE-001", "label": "North depot", "location": {"type": "Point", "coordinates": [30.5234, -1.9441]}}}

The response is 201 Created with the envelope, ETag, and Location. Missing required fields, values outside their type bounds, a reference to a missing or tombstoned target, and a violated unique constraint are refused before anything is written.

PATCH /v1/records/{route}/{record_id} takes a JSON Patch document whose paths are /data/ followed by the field’s apiName:

[
{"op": "replace", "path": "/data/label", "value": "North depot (annex)"},
{"op": "add", "path": "/data/closedOn", "value": "2026-12-31"}
]

If-Match must carry the ETag of the revision you read: a stale value answers 412 precondition.failed and a missing one 428 precondition.required. The response is the new revision’s envelope with data.snapshot. Reference values must name active targets, as on Create.

DELETE /v1/records/{route}/{record_id} with If-Match writes a final revision that marks the record tombstoned and returns that revision’s envelope. The record stays readable through its revisions and leaves live collection reads. The route exists only when the Record type sets tombstone: true.

An active Record that references the target prevents its tombstone, including when you cannot read that referencing Record. The response is the generic 409 mutation.conflict. Clear an optional reference, point it at an active replacement, or tombstone its source Record before tombstoning the target. Tombstoned Records and retained history keep their reference IDs.

POST /v1/records/{route}:batch applies several items to one Record type in one transaction, bounded by its batch.maximumItems and batch.maximumBytes:

{
"items": [
{"operation": "create", "data": {"code": "SITE-002", "label": "East depot"}},
{"operation": "patch", "recordId": "2d7e5e0c-…", "ifMatch": "\"breg-hmac-sha256:…\"", "patch": [{"op": "replace", "path": "/data/label", "value": "West depot"}]}
],
"changeContext": {
"kind": "correction",
"reasonCode": "effective-date-corrected",
"reasonText": "Site start date was recorded one month late.",
"sourceReferences": ["case-document:correction-001"]
}
}

The response is {"snapshot", "results"}. Each result carries operation, id, revision, etag, and data.

changeContext is optional and covers the whole batch; an item-level override is refused. A correction requires a nonempty reasonCode. reasonCode holds at most 64 UTF-8 bytes, reasonText at most 4 KiB, and sourceReferences at most 16 entries of 256 bytes each. Temporal non-overlap constraints are checked on the final state of the batch. One stale ifMatch or one violated constraint rolls back every item.

Send Idempotency-Key (1 to 256 visible ASCII bytes, without a comma or semicolon) on any create, patch, tombstone, or batch. Proposal starts and transitions, attachment changes, immediate Actions, and statistics publications require it.

  • Keys are scoped to the verified issuer and principal; two callers never share one.
  • A spent key answers 409 idempotency.conflict on any different request: the binding names the route, method, target, body, package activation, profile, and verified claims.
  • Only successes spend a key. Correct a refused request and retry with the same key.
  • An exact repeat returns the held response without writing, for retention.receiptRetentionDays after the commit (7 days by default), then answers 410 idempotency.expired and is never executed; read the record instead.

An ingestion run is the durable checkpoint of one bulk load that the client drives one chunk at a time. The client keeps the source; the server keeps the checkpoint and never recomputes the source. Only a chunk submission advances a run, and every operation rechecks the caller’s authority against the compiled batch route. A run is visible only to the issuer and principal that created it; another caller’s run and an unknown run answer the same 404. Run responses carry operational metadata only, never source rows or record values. Every ingestion response carries Cache-Control: no-store.

The ingestion routes refuse an Idempotency-Key header with 400 request.invalid. The server derives each chunk’s attempt key from the run ID, input digest, chunk index, and chunk digest, so resubmitting the exact chunk is always safe. For the procedure, see load records through an import window.

POST /v1/records/{route}/ingestion-runs, Content-Type: application/json, a body of at most 4 KiB with exactly these members:

{
"operation": "create",
"profileId": "operator",
"packageRevision": "sha256:9f2c…",
"schemaFingerprint": "sha256:1a7b…",
"inputDigest": "5e8d…",
"inputLength": 48211,
"itemCount": 12000,
"chunkCount": 13,
"chunkAlgorithmVersion": "greedy-canonical-http-batch-v1"
}
MemberRule
operationcreate or patch. A profile that holds import announces create.
profileId, packageRevision, schemaFingerprintNonempty strings that must match the selected profile and the active package.
inputDigestSHA-256 of the whole source, 64 lowercase hexadecimal characters.
inputLength, itemCount, chunkCountIntegers from 1 to 2^53 − 1.
chunkAlgorithmVersionExactly greedy-canonical-http-batch-v1.

The answer is 201 Created with {"run": <run document>}. An import run is created only while the Record type holds an open import authority for the profile whose remaining volume covers itemCount and, when the authority pins digests, lists this inputDigest; otherwise the answer is 412 precondition.failed and no run exists.

MemberMeaning
runIdThe run identifier, a lowercase UUID.
statusopen, complete, cancelled, or blocked.
blockedReasonnull, or activePackageChanged or importAuthorityClosed while blocked.
recordType, operation, profileIdThe bound Record type, operation, and profile.
packageRevision, schemaFingerprintThe binding the run was created under.
inputDigest, inputLength, itemCount, chunkCount, chunkAlgorithmVersionThe announced source.
maximumItems, maximumBytesThe batch bounds every chunk stays inside, fixed at creation.
nextChunkIndexThe chunk index the next submission must name.
committedItems, committedPrefixDigestItems committed so far and the prefix digest of the last committed chunk; the SHA-256 of empty input before the first.
lastAttemptnull, or {"outcome", "chunkIndex"}.
createdAt, updatedAtRFC 3339 timestamps.
completetrue when status is complete.

lastAttempt.outcome is committed, replayed, invalidItem, refused, bindingChanged, importAuthorityClosed, chunkMismatch, runNotOpen, or unavailable. invalidItem and refused leave the checkpoint where it was: continue the remainder in a successor run.

A run whose binding no longer matches the active package reports blocked with activePackageChanged. An import run whose authority closed, expired, or ran out of volume reports blocked with importAuthorityClosed at its next chunk, which commits nothing and answers 409 ingestion.run_blocked. A blocked run stays readable and never resumes; continue in a successor run.

POST /v1/records/{route}/ingestion-runs/{run_id}/chunks, Content-Type: application/json, with exactly these members:

MemberRule
chunkIndexNonnegative integer. A fresh chunk must equal nextChunkIndex.
items1 to maximumItems batch items, each the object the :batch route accepts, all with the run’s operation.
digestSHA-256 of the canonical JSON of {"items": [...]}, 64 lowercase hexadecimal characters. That canonical body must fit in maximumBytes.
prefixDigestSHA-256 of the source bytes from the start through the end of this chunk’s last item. On the last chunk it must equal inputDigest.

The chunking algorithm is greedy: each chunk takes as many consecutive source items as fit in maximumItems and maximumBytes, and every remaining chunk must still hold at least one item.

A committed chunk answers 200 with {"run", "receipt"}. Its records, revisions, audit, receipt, and checkpoint advance commit in one transaction. A chunk index below nextChunkIndex is a replay: when its digest and prefixDigest equal the stored ones, the answer is the stored receipt with replayed: true and nothing is written. Any other mismatch of index, digest, prefix digest, count, or operation answers 409 ingestion.chunk_mismatch and writes nothing.

Receipt memberMeaning
chunkIndex, digestThe committed chunk.
replayedtrue when this answer replays an earlier commit.
erasedAlways false in a served receipt.
batchThe {"snapshot", "results"} answer the chunk committed, projected to the fields the run’s profile can read now.

GET /v1/records/{route}/ingestion-runs/{run_id}/chunks/{chunk_index}/receipt returns the same receipt without resending the chunk. A receipt is erased with the record history it describes; then the read, and an exact replay of that chunk, answer 410 ingestion.receipt_erased. The chunk’s counts stay in the run.

The list and read routes are in the routes table. A restarting client rebuilds its plan from the run document alone and never resends a committed chunk.

POST /v1/records/{route}/ingestion-runs/{run_id}/cancel takes an empty body with no Content-Type and cancels an open or blocked run, answering 200 with {"run"}. A complete or cancelled run answers 409 ingestion.run_not_open. The counts, committed prefix, receipts, and audit stay.

GET /v1/records/{route}/{record_id}/revisions returns a collection of the record’s revisions, at most 100 per read, and .../revisions/{revision} returns one. Revisions show stored field values only; derived fields and read paths are not evaluated over history. Correction context from a batch appears only when the revisions grant lists provenanceFields (from kind, reasonCode, reasonText, sourceReferences) and every revision that batch produced is visible to the caller.

A temporal Record type, one with temporal.startField and temporal.endField, gains :current, the records whose interval contains now, and :as-of, the records whose interval contains asOf. Intervals include the start and exclude the end; a null end is open. asOf is a UTC RFC 3339 timestamp such as 2026-06-05T00:00:00Z.

GET /v1/records/{route}:snapshot without snapshot captures the latest committed state and returns its token with the page, so paging stays consistent while writes continue. With snapshot, it reproduces that earlier state. validAt filters by effective time inside the snapshot and is refused on a non-temporal Record type; it takes a calendar date for a date start field and a UTC RFC 3339 timestamp for a timestamp one. asOf and recordedAsOf are refused on this route.

A snapshot token grants nothing: the caller needs the snapshot operation, and current row boundaries apply. A snapshot whose history was erased, whose field metadata no longer matches, or whose two-second database budget runs out answers 503 source.unavailable.

An Action with execution.mode: proposal starts a durable Proposal at POST /v1/actions/{action}/proposals. The routes are in the routes table; Actions and Proposals explains the model.

OperationPurposeAuthority
editUpdate the caller-owned draft inputs.Explicit Edit and current draft/target scope.
submitFreeze the exact plan and queue required review or application.Explicit Submit and current author-stage target authority.
reviseRetire the frozen submission and return to an editable draft. Refused once a Review rejected the Proposal.Currently promoted Revise; old approval cannot authorize changed contents.
rebaseCreate a new draft binding after target/package evolution.Current Start and Rebase; requires resubmission and fresh review when required.
retryRetry eligible recoverable application work.Explicit Retry; automatic work additionally requires a verified human for external recovery.
cancelCancel currently cancellable durable work.Explicit Cancel under current ownership and target scope.
applyApply only the exact frozen effects.Separate Apply with current guards and required fresh approval; automatic system authority is restricted to its frozen binding.

A Proposal read carries an ETag. Every transition, attachment upload, and attachment removal requires If-Match with that value and answers 428 precondition.required without it. The Proposal ETag is distinct from a record ETag and is bound to the caller’s profile, principal, purpose, and target scope as well as the Proposal revision, so refetch after every transition and never reuse another caller’s value. Follow only the transition and attachment links the current representation promotes.

GET /v1/proposals/{proposal_id}/submissions/{submission_version} reads one exact submission under the Proposal Get authority: its frozen effectDigest, its review result, its decision, and its application outcome as separate members. Only the current submission reports application progress. Once its result window has lapsed and no application receipt remains, it answers 410 proposal.submission_expired; the Proposal stays readable.

GET, PATCH, and DELETE /v1/proposals/{proposal_id}/attachments/{slot_id} read, upload, and remove one slot’s bytes. Slot descriptors on the Proposal carry requiredness, byte limit, content types, classification, verification, and the links the caller may follow. Submission binds the exact content digest; changed content needs a new submission and fresh review.

An Action with execution.mode: immediate runs one atomic plan at POST /v1/actions/{action} with Idempotency-Key. Input names are exact apiName values, validated against the compiler-emitted JSON Schema in the Action descriptor. When the descriptor advertises target conditions, read them first at POST /v1/actions/{action}/target-conditions. Invoke requires Action authority over every target. Effects, revisions, audit, receipt, and outbox commit together or not at all; resend the original request with its original key when the outcome is uncertain. See Actions and Proposals for the model.

An immediate Action with a reference input advertises one routes.availability endpoint at POST /v1/actions/{action}/availability. Send targetInput with the reference input’s exact apiName, plus an input object containing its UUID and any declared form values already known. Each request checks one target. Query several reference inputs separately. The response reports advisory available as true, false, or null. A known answer includes observedRevision. guard-not-satisfied means a field guard failed. input-required names a nonempty set of declared API input names in missingInputs. not-disclosed carries observedRevision: null and no missing-input names because the selected profile cannot safely inspect the guard or boundary. A known failed guard takes precedence over missing guard input. No availability response names a guard ID or a Record value. The route evaluates only field guards for that one target. It does not run a Function, test SQL uniqueness or Evidence, inspect other targets, supply a condition token, or grant mutation authority. A true answer means only that the selected target’s field guards are currently satisfied, including when it has none. Form completeness, Functions, Evidence, other targets, and SQL constraints may still refuse. Invoke checks authority, concurrency, and every guard in the commit transaction.

A refusal is an RFC 9457 problem document with Content-Type: application/problem+json:

{
"type": "https://id.registrystack.org/problems/registry-breg/request/invalid",
"title": "Bad Request",
"status": 400,
"detail": "The request is invalid.",
"code": "request.invalid",
"traceId": "3f1a9c2e5b7d4e6f8a9b0c1d2e3f4a5b",
"fieldPath": "/data/householdCode"
}

Branch on code. type is the same code with each dot read as a path segment under https://id.registrystack.org/problems/registry-breg/. detail is the registered text of the code. traceId is 32 hexadecimal characters that also appear in the request log and, for every journaled request, in its audit record. A request refused before admission for presenting no credential is counted on the metrics listener rather than journaled; see scrape metrics.

fieldPath is present only when one member is at fault:

RefusalfieldPath
Create body/data for a missing data member or a field you may not write, known or not; /data/<apiName> for a missing required field or a wrong value of a field you may write.
Patch document/<n> for operation n, /<n>/op, /<n>/path for a path outside /data/ or a field you may not write, known or not, and /<n>/value.
Batch body/items, /changeContext, /items/<n>, /items/<n>/operation, /items/<n>/recordId, /items/<n>/ifMatch, /items/<n>/patch, and the create and patch forms above under /items/<n> and /items/<n>/patch.
HeaderIdempotency-Key when it is missing or malformed; If-Match when a batch request sends one, because each batch item carries its own ifMatch.
QueryThe parameter: $select, $filter, $orderby, $top, $count, $skiptoken, bbox, accessProfile, asOf, snapshot, or validAt; on statistics routes also from, to, or status. A revision route takes none but accessProfile and names the first other one you sent.

A name you sent is never echoed: an unknown member and a withheld one stop at the same location. An oversized or non-JSON body, an undefined query parameter, a malformed query string, and the lookup, ingestion, attachment, and GIS routes carry no fieldPath.

A direct create, patch, or batch that violates an authored row-local constraint answers 409 mutation.conflict and may add the declared location:

{
"recordType": "permit",
"constraintId": "chronological-validity",
"constraintKind": "compare",
"left": "valid-from",
"right": "valid-to"
}

compare carries left and right; int-range and vocabulary carry field. The members appear only when the selected profile can read every referenced field, and never on Action or Proposal effects. They hold declared identifiers only, never a submitted value or database message. An explicit constraint id is the stable key to map on; an omitted one is compiler-derived and changes with the declaration. unique and temporal-non-overlap conflicts stay generic.

CodeStatusWhen
authentication.refused401A bearer token is missing or rejected: bad signature, wrong issuer or audience, expired, denied key, or a claim outside policy.
resource.not_found404A verified token that does not satisfy the profile, a route the profile cannot reach, or a record outside the caller's row boundary. The server does not distinguish these cases.
lookup.unresolved404A lookup matched zero records or more than one.
request.invalid400The body, headers, or path did not match the compiled contract. fieldPath names the body member or header at fault when one is, and never a field name the grant does not admit.
query.invalid400A query option was malformed, over a bound, or not permitted by the grant. fieldPath names the query parameter, such as $select, when one is at fault.
query.cursor_invalid400A $skiptoken that is malformed, was issued for a different query, or has expired.
action.refused422An Action function intentionally refused the operation. refusalCode identifies a declared reason, detail contains its static label, and optional fieldPath names an input. No effects commit.
action.guard_failed412An immediate Action field guard failed and the selected profile can safely read the target and the field that establishes the failure. Hidden guard fields, failed authority, and stale conditions remain precondition.failed. No effects commit.
action.handler_failed500An activated Action function failed to produce an accepted result. Operators use the trace identifier and safe server diagnostics to repair the package. A function deadline returns 503 service.unavailable instead.
action.evidence_failed503An immediate-action handler declared an Evidence dependency that could not be acquired or accepted: the provider did not answer, or the assertion was refused, failed verification, or exceeded the retention bound. fieldPath names the declared Evidence alias when the failure is bound to one. No effects commit.
unsupported.media_type415A body without the required Content-Type.
precondition.required428A route that requires If-Match was called without it.
precondition.failed412If-Match did not match the current revision, or a proposed create, patch, or batch row was outside the caller's authorized boundary. Boundary refusals are value-free and retrying the unchanged request cannot succeed.
proposal.rejected409A Review rejected the Proposal, and a rejection is final: Submit, Edit, Revise, Rebase, Apply, Retry and Cancel are refused. Start a new Proposal to try again.
proposal.review_result_mismatch412The current Review result differs from the retained approval. Apply commits no effects. Reconcile the review result before attempting application again.
proposal.submission_expired410The submission's review result window has lapsed or its detail was erased, and it has no application receipt. The Proposal itself stays readable.
mutation.conflict409A constraint or workflow rule refused the write. A storage-pattern failure includes the declared recordType and fieldId; correct the input before retrying. A 409 does not imply that retrying an unchanged request can succeed.
idempotency.conflict409The caller reused the Idempotency-Key with a different request, another write included, or retried a key whose held response a retention erase removed. A byte-identical body also conflicts when the first attempt committed under a different package activation, access profile, or claims.
idempotency.expired410The caller's exact retry arrived after the receipt horizon (retention.receiptRetentionDays, 7 days by default). The first attempt committed and the key stays spent, so the request is never executed again.
ingestion.profile_mismatch403The selected profile is not the one the run bound, or does not satisfy the compiled batch route for the bound operation. A run id grants nothing on its own.
ingestion.run_not_open409The run completed or was cancelled, so it accepts no chunk and no cancel. The run stays readable.
ingestion.run_blocked409The run is blocked: the active package revision or schema fingerprint no longer matches the run binding, or the import authority the run consumes closed, expired, or has too little volume left for the chunk. The run's blockedReason names which. The run stays blocked and inspectable; continue in a successor run.
ingestion.chunk_mismatch409The chunk index, chunk digest, or rolling prefix digest does not match the run's expected next chunk. Nothing was written; reread the run and resubmit the exact chunk.
ingestion.receipt_erased410The chunk receipt was erased with the record history it describes. The chunk and its counts stay visible in the run.
request.timeout504The request exceeded operationalTimeouts.httpRequestMilliseconds.
source.unavailable503The database refused or timed out while the request held it.
service.unavailable503The runtime could not complete the mutation for a reason other than the request itself.
runtime.not_ready503The readiness probe before the package and database are usable.
runtime.field_encryption.unavailable503The route reaches a Record type with encrypted fields while the field-encryption key state is not usable. Record types without encrypted fields keep serving.
statistical_dataset.domain_violation500A non-null source value falls outside a declared dimension domain; the value is not disclosed.
statistical_dataset.release_refused422The period or lifecycle state is ineligible; refusalCode names a closed reason.
statistical_dataset.version_conflict409The active package changed or a fresher computation superseded this publication.
statistical_dataset.version_withdrawn410The directly requested version was withdrawn; reasonCode names the recorded reason.

Record type hooks[]

Source preview, pre-1.0

Generated from products/breg/generated/authoring/registry-module.schema.json. 27 key paths. Explained in docs/site/src/content/docs/configure/breg.mdx.

Key pathTypeRequiredAccepted valuesConstraints
displayAuthored presentation only; labels never establish identity or authority.objectNoNot specifiedNone specified
display.descriptionHuman-readable display text. Bidirectional embedding, override, and isolate controls, byte-order marks, line and paragraph separators, and Unicode noncharacters are refused; multilingual joiners and direction marks remain valid.stringNoNot specifiedNone specified
display.labelHuman-readable display text. Bidirectional embedding, override, and isolate controls, byte-order marks, line and paragraph separators, and Unicode noncharacters are refused; multilingual joiners and direction marks remain valid.stringNoNot specifiedNone specified
handlerGoverned, destination-neutral delivery. destinationId is a key in runtime eventDestinations; the project carries no URL or secret, and deployment configuration may tighten the bounds it binds but cannot supply or widen this authority. Production compilation requires it.objectNoNot specifiedNone specified
handler.abiFor rhai: The handler ABI the script speaks. For wasm: The handler ABI the module speaks.stringConditionalNot specifiedNone specified
handler.destinationIdKey in the runtime destination binding.stringConditionalNot specifiedNone specified
handler.moduleModule path in the project.stringConditionalNot specifiedNone specified
handler.scriptScript path in the project.stringConditionalNot specifiedNone specified
handler.typestringYes
  • rhai
  • url
  • wasm
None specified
idStable hook contract identifier, sent as ce-type. Use a new identifier for a breaking payload change.stringYesNot specified
  • pattern: ^[a-z][a-z0-9_-]{0,63}$
phaseWhen the hook runs relative to the triggering transaction.stringYes
  • after
  • before
None specified
principalThe access profile a proposal from this hook is applied under. Optional: a hook that declares none is a non-proposing hook, and a proposal from one is refused and dead-lettered at delivery time. Declaring none is not an authoring error.stringNoNot specifiedNone specified
projectionDeclared field identifiers to include in values. System event metadata is included separately.arrayYesNot specified
  • uniqueItems: true
projection[]Array itemstringNoNot specifiedNone specified
triggerCommitted record change that can produce this hook.stringYes
  • created
  • patched
  • tombstoned
None specified
whenOptional field tests, combined with AND. Omit to run on every matching trigger.objectNoNot specifiedNone specified
when.afterEqualsRequired values after the change. Valid with created and patched triggers.objectNoNot specifiedSchema default, where declared: {}None specified
when.afterEquals.*Map valueA comparison literal: a boolean, a number, or text.boolean | number | stringNoNot specified
  • propertyNames.pattern: ^[a-z][a-z0-9_-]{0,63}$
when.afterIsNullFields that must be unset after the change. Valid with created and patched triggers.arrayNoNot specifiedSchema default, where declared: []
  • uniqueItems: true
when.afterIsNull[]Array itemstringNoNot specifiedNone specified
when.beforeEqualsRequired values before the change. Valid with patched and tombstoned triggers.objectNoNot specifiedSchema default, where declared: {}None specified
when.beforeEquals.*Map valueA comparison literal: a boolean, a number, or text.boolean | number | stringNoNot specified
  • propertyNames.pattern: ^[a-z][a-z0-9_-]{0,63}$
when.beforeIsNullFields that must be unset before the change. Valid with patched and tombstoned triggers.arrayNoNot specifiedSchema default, where declared: []
  • uniqueItems: true
when.beforeIsNull[]Array itemstringNoNot specifiedNone specified
when.changedFields whose values must change. Only valid with the patched trigger.arrayNoNot specifiedSchema default, where declared: []
  • uniqueItems: true
when.changed[]Array itemstringNoNot specifiedNone specified
when.typestringYes
  • fields
None specified

Proposal hooks[]

Source preview, pre-1.0

Generated from products/breg/generated/authoring/registry-project.schema.json. 12 key paths. Explained in docs/site/src/content/docs/configure/breg.mdx.

Key pathTypeRequiredAccepted valuesConstraints
displayAuthored presentation only; labels never establish identity or authority.objectConditionalNot specifiedNone specified
display.descriptionHuman-readable display text. Bidirectional embedding, override, and isolate controls, byte-order marks, line and paragraph separators, and Unicode noncharacters are refused; multilingual joiners and direction marks remain valid.stringConditionalNot specifiedNone specified
display.labelHuman-readable display text. Bidirectional embedding, override, and isolate controls, byte-order marks, line and paragraph separators, and Unicode noncharacters are refused; multilingual joiners and direction marks remain valid.stringConditionalNot specifiedNone specified
handlerWhere a hook's handler runs. The pairing rule: rhai requires script, wasm requires module, url requires destinationId, and each kind requires exactly its own fields and nothing else. abi is required on rhai and wasm and forbidden on url; its value is closed and checked at compile time in [crate::validate_hooks].objectConditionalNot specifiedNone specified
handler.abiFor rhai: The handler ABI the script speaks. For wasm: The handler ABI the module speaks.stringConditionalNot specifiedNone specified
handler.destinationIdKey in the runtime destination binding.stringConditionalNot specifiedNone specified
handler.moduleModule path in the project.stringConditionalNot specifiedNone specified
handler.scriptScript path in the project.stringConditionalNot specifiedNone specified
handler.typestringConditional
  • rhai
  • url
  • wasm
None specified
idA local identifier: a lowercase letter, then up to 63 lowercase letters, digits, _, or -.stringConditionalNot specified
  • pattern: ^[a-z][a-z0-9_-]{0,63}$
transitionsOmit to observe every committed lifecycle transition.arrayConditionalNot specified
  • uniqueItems: true
transitions[]Array itemstringConditional
  • applied
  • cancelled
  • edited
  • rebased
  • rejected
  • retried
  • review-returned
  • revised
  • started
  • submitted
None specified

Record hooks live under recordTypes[].hooks; Proposal hooks under actions[].execution.proposal.hooks. Capture commits with the source operation and delivery follows the commit, so later edits never change a captured event. The deployment’s eventDestinations supplies the URL, network and TLS policy, HMAC key, classification ceiling, and limits; it may narrow the compiled contract but never widen it.

TriggerchangedbeforeEqualsafterEqualsProjected snapshot
createdNot allowedNot allowedAllowedAfter
patchedAllowedAllowedAllowedAfter
tombstonedNot allowedAllowedNot allowedBefore
HeaderFormat or valueMeaning
Acceptapplication/jsonAccepted response media type.
Content-Typeapplication/jsonCanonical JSON event data.
ce-specversion1.0CloudEvents version.
ce-idUUIDCaptured event identity; unchanged by retry or replay.
ce-sourceurn:registrystack:registry:<registry-id>:instance:<instance-id>Registry instance identity.
ce-typeCaptured event typeConfigured record event ID or native proposal.<transition> event type.
ce-timeRFC 3339 timestampMutation capture time; unchanged by retry or replay.
ce-dataschemaurn:breg:event-schema:<registry-id>:<record-type-id>:<event-id>:sha256:<digest>Record payload schema identity and fingerprint; Proposal events use https://id.registrystack.org/contracts/breg/proposal-event/v1.
Idempotency-Keysha256:<digest>Opaque key for deduplicating automatic retries.
X-Registry-Event-GenerationPositive integer, initially 1Increments on operator replay.
X-Registry-Delivery-AttemptPositive integer, initially 1Attempt number within the generation.
X-Registry-Delivery-TimeRFC 3339 timestampCurrent attempt time; used for receiver freshness checks.
X-Registry-Signaturev1=<base64url-without-padding>HMAC-SHA-256 signature over the versioned signing input.
FieldTypeMeaning
idUUID stringCaptured event identity; equals ce-id.
typeStringRecord events: the authored hook ID. Proposal events: proposal.<transition>. Equals ce-type.
sourceurn:registrystack:registry:<registry-id>:instance:<instance-id>Registry instance identity; equals ce-source.
timeRFC 3339 timestamp, UTC, whole millisecondsMutation capture time; equals ce-time.
subjectObject with recordReference and recordRevisionRecord events: recordReference is hmac-sha256:<64-hex>, never the raw record id, and recordRevision is the record revision the change produced. Proposal events: recordReference is the Proposal path /v1/proposals/<proposal-id> and recordRevision is the Proposal revision after the transition.
dataschemaurn:breg:event-schema:<registry-id>:<record-type-id>:<event-id>:sha256:<digest>Captured payload schema identity; equals ce-dataschema. Proposal events use their dedicated closed schema.
dataObjectThe captured event data, described below.
causationObject with root, hop, and optional parentChain position. A captured registry mutation is a root event: root equals id, hop is 0, and parent is absent.
FieldTypeMeaning
recordTypeStringAuthored Record type ID.
recordIdUUID stringRecord identity.
revisionPositive integerRecord revision after the mutation.
triggercreated, patched, or tombstonedCaptured record mutation trigger.
packageRevisionsha256:<digest> stringPackage active at capture time.
valuesObjectExactly the declared projection, using authored field IDs; unset optional fields are null.
FieldTypeMeaning
actionId, actionRevisionString and digestExact governed Action binding.
proposalId, submissionVersion, revisionUUID and positive integersExact durable work and transition revision.
fromState, state, transitionClosed stringsNative lifecycle transition and its states. transition is started, edited, submitted, revised, rebased, retried, cancelled, applied, rejected or review-returned; rejected ends the Proposal in the terminal rejected state.
effectDigestDigest or nullFrozen plan identity when available.
authorityKindcaller, system or reviewAuthority used for the transition; review for a transition a recorded review result caused.
systemApplyGrantRevisionDigest or nullRestricted system grant binding for automatic application.
turnparty, official or nobodyWhose move the Proposal waits on after the transition.

A Proposal event’s id is derived from its Action, Proposal, revision, and transition, so the same transition always carries the same event identity.

X-Registry-Signature is v1= followed by the unpadded base64url HMAC-SHA-256 of the signing input, under a key of at least 32 bytes. The signing input is the bytes breg-webhook-signature-v1 followed by these fields in order, each as an unsigned 64-bit big-endian length and then its bytes: the version 1.0, ce-id, ce-source, ce-type, ce-time, ce-dataschema, X-Registry-Event-Generation, X-Registry-Delivery-Attempt, X-Registry-Delivery-Time, the method, the request target (the destination’s configured path), Content-Type, Idempotency-Key, and the exact body. The body is at most 1 MiB and the other fields together at most 32 KiB.

Compare in constant time, then check the expected source, type, schema, and the freshness of X-Registry-Delivery-Time, not of ce-time, before accepting durably.

PropertyContract
Delivery modeAsynchronous, after commit, at least once.
AcknowledgementAny 2xx response.
Failed attemptRedirect, other non-2xx response, timeout, or transport failure. Redirects are not followed.
Total attemptsCompiled ceiling: 5 per generation, including the first attempt. Runtime may lower it.
Attempt timeoutCompiled ceiling: 5,000 ms. Runtime may lower it to a minimum of 100 ms.
Retry delays1, 2, 4, then 8 seconds, stopping at the deployed attempt limit.
Payload sizeEvent-specific compiled bound, at most 1 MiB.
Payload retentionretention.hookPayloadRetentionDays: default 7, range 1 to 30 days, measured from capture. Replay does not extend expiry.
Successful payloadErased when the successful terminal state commits.
OrderingNo global or per-record ordering guarantee.
PropertyAutomatic retryOperator replay
Event ID, source, type, time, and schemaUnchangedUnchanged
Body and captured package revisionUnchangedUnchanged
GenerationUnchangedIncremented
Idempotency keyUnchangedNew key
Attempt numberIncrementedFirst send is 1
Delivery time and signatureNew for the attemptNew for the attempt
Payload expiryUnchangedUnchanged
StateMeaningShown by webhook listReplay eligible
pendingWaiting for an attempt or retry.YesNo
leasedAn attempt is in progress.NoNo
deliveredAcknowledged successfully; payload erased.NoNo
dead-letteredAttempt limit exhausted.YesOnly with unexpired retained payload and matching generation and destination binding.
expiredPayload retention ended.YesNo
FieldTypeMeaning
eventIdUUID stringCaptured event ID; matches ce-id.
deliveryIdStringCompiled delivery identifier used for replay.
generationPositive integerCurrent generation used as --expected-generation.
statepending, dead-lettered, or expiredReported delivery state.
attemptNonnegative integerAttempt count; 0 before the first send of a generation.
payloadAvailableBooleanPayload is retained and unexpired.
payloadExpiresAtRFC 3339 timestampCapture-based payload expiry.
replayEligibleBooleanDead letter with an available payload; replay also validates generation and destination binding.
CommandRequired argumentsResult
explain events<project>Compiled event inventory; no database access.
webhook sample<project> --event <event-id>Synthetic request with a placeholder signature; no HTTP send.
webhook list--runtime-config <absolute-file>Delivery metadata without record values; optional --limit defaults to 50 and accepts 1 to 100.
webhook replay--runtime-config <absolute-file> --event-id <uuid> --delivery-id <id> --expected-generation <number>Queues an eligible dead letter in a new generation; does not confirm receiver acceptance.

Replay and discard name the exact event, delivery, and expected generation. Replay starts a new generation; discard stops future attempts and cannot undo an earlier acceptance. Each command’s --help lists its flags.

A Record type with a crs84-point field and a geojson.geometryField is also served under /v1/gis for desktop GIS clients, with bbox, limit (at most 10000), and cursor on the item route. It answers GeoJSON feature collections under the same profile, grant, and row-boundary rules as the record routes and declares the profile breg-gis-bounded-crs84-point-bbox-v1. It makes no OGC API Features conformance claim and computes no collection extents.

RouteProfile grantResult
GET /v1/statistics/{dataset}:liveLive profile with count-capable source list grantExact JSON or CSV; optional from and to.
GET /v1/statistics/{dataset}/releasesReleased-data reader, publisher, or live profileVersion headers and pageInfo; $top and opaque $skiptoken.
GET /v1/statistics/{dataset}/releases:seriesReleased-data accessLatest eligible document per period; required from and to, optional status=final.
GET /v1/statistics/{dataset}/releases/{period}Released-data accessLatest eligible JSON or CSV; optional status=final.
GET /v1/statistics/{dataset}/releases/{period}/versions/{version}Released-data accessImmutable JSON or CSV; 410 after withdrawal.
POST /v1/statistics/{dataset}/releases/{period}/versionsPublisher201 version header; {"status":"provisional"} or {"status":"final"}; Idempotency-Key required.
POST /v1/statistics/{dataset}/releases/{period}/versions/{version}/withdrawalPublisher200 header with withdrawal; closed reason; Idempotency-Key required.

Every statistical operation requires a profile selected with accessProfile; a released-data reader needs no record permissions.

Document reads from the live, series, latest-version, and exact-version routes send Cache-Control: no-store, Vary: authorization, accept, and Repr-Digest for the exact JSON or CSV bytes. The release listing omits Repr-Digest. A version header’s contentDigest identifies the canonical stored JSON; a withdrawn version’s header omits it. No strong ETag is issued.

CSV columns are period,periodStart,periodEnd,<dimension field IDs...>,value,status. A suppressed value is blank; a zero stays zero with its status. Totals round independently; see publish statistics for the inference risks. Unknown datasets and ungranted profiles answer the concealed 404, and invalid ranges 400 query.invalid.

Configuration fields are schema-generated; the route, query, problem, and protocol tables are maintained data checked against the implementation.