Skip to content
Registry StackDocsv0.34.0

Base Registry Engine API reference

View as Markdown

Lookup reference for the HTTP API a compiled Base Registry Engine (BReg) package serves and for the events it emits. Every registry serves the same route shapes; the entity routes, fields, profiles, and actions come from your project. The contract is not a frozen compatibility promise.

For the authoring grammar, use author a registry project. For deployment and recovery, use deploy a registry. For receivers, delivery, and replay, use bind webhook receivers. For every YAML key, use the configuration reference. A running server publishes its own OpenAPI document at /openapi.json; the schemas there are the exact ones its package compiled.

Every request carries Authorization: Bearer <token> unless the selected profile sets anonymous: true. The token is a JWT signed by the configured issuer with the configured algorithm. It must carry iss, aud, and exp, and its scopes come from the claim named by scopeClaim, split on scopeSeparator; both are required settings. The principal and purpose claims are the ones named under authorityClaims.

Each route has one default access profile. A caller selects another with the accessProfile query option. A profile grants access when the token satisfies its requiredScopes, requiredPurposes, and principalClaim, and when the profile grants the operation on that entity. Row boundaries on the grant compare a record field with a token claim and hide every record outside them, on reads and on writes.

A refusal is concealed as 404 resource.not_found whenever the token is missing, does not satisfy the profile, or names a record outside the caller’s boundary. The only exception is a token that was presented and rejected, which returns 401 authentication.refused. A caller therefore cannot distinguish a record that does not exist from one it may not see.

{route} is the entity’s route value. {readPath} is the route of one of the entity’s readPaths. {stage} is a review stage identifier, {action} under /v1/actions is an immediate action identifier, {runId} is an ingestion-run identifier, and {chunkIndex} is a chunk position. An unknown path or a method the route does not accept returns the same concealed 404 as an unauthorized request.

RouteOperationAvailable whenResponse
GET /v1/registrymetadataThe caller reaches at least one routeEntity inventory filtered to the caller: route, dataset identifier, the operations each entity exposes with the profile that grants each, the schema path, and for a request entity its changeRequest capability (planner kind, review mode, application policy). A caller with no visible route gets the concealed 404.
GET /v1/schemas/{entity}metadataThe caller reaches the entityJSON Schema for the entity record, 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}createEvery entity201 with the record envelope, Location, and ETag.
GET /v1/records/{route}listEvery entityCollection.
GET /v1/records/{route}/{recordId}getEvery entityRecord envelope with ETag.
PATCH /v1/records/{route}/{recordId}patchmutationMode: mutableRecord envelope with data.snapshot. Requires If-Match.
DELETE /v1/records/{route}/{recordId}tombstonetombstone: trueEnvelope of the final revision with data.snapshot. Requires If-Match.
POST /v1/records/{route}:lookuplookupEntity declares selectorProfilesRecord envelope, or 404 lookup.unresolved.
POST /v1/records/{route}:batchbatchEntity declares batch{"snapshot", "results": [...]}.
POST /v1/records/{route}/ingestion-runsbatchEntity declares batch201 with the run document: the binding, the announced counts and bounds, and nextChunkIndex.
GET /v1/records/{route}/ingestion-runsbatchEntity declares batchRun documents the caller created. limit (default 25, at most 100), keyset after, and optional status and inputDigest filters.
GET /v1/records/{route}/ingestion-runs/{runId}batchEntity declares batchThe run document with nextChunkIndex. Another caller's run and an unknown run are the same 404.
POST /v1/records/{route}/ingestion-runs/{runId}/chunksbatchEntity declares batchThe run document with the attempt outcome. Takes no Idempotency-Key: the server derives the attempt key from run id, input digest, chunk index, and chunk digest.
POST /v1/records/{route}/ingestion-runs/{runId}/cancelbatchEntity declares batchCancels an open or blocked run. Counts and audit are preserved.
GET /v1/records/{route}/ingestion-runs/{runId}/chunks/{chunkIndex}/receiptbatchEntity declares batchThe stored batch response the chunk committed, or 410 ingestion.receipt_erased once its records are erased.
GET /v1/records/{route}/{recordId}/revisionsrevisionsEvery entityCollection of the record's revisions, at most 100 per read.
GET /v1/records/{route}/{recordId}/revisions/{revision}revisionsEvery entityRecord envelope for one revision.
GET /v1/records/{route}:snapshotsnapshotEvery entityCollection as recorded at a saved snapshot.
GET /v1/records/{route}:currentlistEntity declares temporalCollection of the records valid now.
GET /v1/records/{route}:as-oflistEntity declares temporalCollection of the records valid at asOf.
GET /v1/records/{route}/{recordId}/{readPath}list through readPaths[]Entity declares readPathsCollection of related records.
POST /v1/records/{route}/{recordId}/actions/{action}submit_request, revise_request, cancel_request, apply_requestEntity declares changeRequestRequest action result. Requires If-Match and Idempotency-Key.
POST /v1/actions/{action}/target-conditionsinvokeRegistry declares actions[]{"preconditions": {...}} for the targets the input names.
POST /v1/actions/{action}invokeRegistry declares actions[]{"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.

Reads, creates, patches, tombstones, revisions, and snapshots return one shape, 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 entity’s fields under their apiName, narrowed to the grant’s readableFields and to $select.
data.snapshotstringPresent on mutation responses: an opaque token for :snapshot reads, at most 4096 bytes.
data.requestobjectPresent on change request records: workflow state and the actions the caller may take.
data.requestPresenceobjectPresent when the grant declares requestPresence: the pending change requests that target this record.
meta.*stringsRegistry, dataset, and entity identifiers from the compiled package.

A field name that would collide with an envelope member is refused at compile time, so domainData never contains data, meta, items, pageInfo, nextCursor, @context, @id, @type, or any of the identifier members named in the table.

Response headers:

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/{entity}>; rel="describedby".
LocationOn 201 Created: /v1/records/{route}/{recordId}.

Field values are encoded by type. The generated JSON Schema for each entity carries the same rules, with an optional field spelled as anyOf of the type and null.

Field typeJSON encodingNotes
booleantrue or false
stringstringminLength and maxLength from the field.
textstringmaxLength 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 maxBytes, advertised as x-registry-maxBytes.

A collection read returns the matching records as items, a pageInfo object, and the same meta as a single record. Only a single-record response wraps the record in data; a collection member carries 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 the cursor as $skiptoken with no other query option except accessProfile. The cursor is signed with the secret named by the runtime cursor.secretRef, binds the whole original query, and expires after cursor.maxAgeSeconds; a cursor that fails any of those checks returns 400 query.cursor_invalid. With $count, the response carries the matching total beside the page.

Options apply only where the grant permits them: $select draws from readableFields, $filter from filterableFields, $orderby from sortableFields, and $count needs allowCount. Anything else returns 400 query.invalid with the same status for an unknown field and for a field the grant withholds.

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.maxAgeSeconds.
bboxlist on entities 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 entity'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 entities.One value.
Whole query stringEvery routeDecoded size of all options together.16 KiB.

A filter is an expression over field names and literals:

$filter=status eq 'active' and (region in ('north','east') or startswith(label,'Depot'))
$filter=not contains(label,'closed') and validFrom ge 2026-01-01

Strings are single quoted, numbers and dates are bare, and null compares with eq and ne. Literals must fit the field type. A bounding box is four CRS84 degrees, west, south, east, north:

bbox=30.0,-2.1,30.9,-1.8

Every write needs Content-Type: application/json, except a patch, which needs application/json-patch+json. A body over the entity’s byte bound or with an unknown member is refused with 400 request.invalid and a 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. Required fields missing from the body, values outside their type bounds, a reference to a missing target, and a violated unique constraint are refused before anything is written.

PATCH /v1/records/{route}/{recordId} takes a JSON Patch document whose paths start at /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 is required and must carry the ETag of the revision you read; a stale value returns 412 precondition.failed and a missing one 428 precondition.required. A field outside writableFields, an envelope member, or a path that does not start with /data/ is refused. The response is the new revision’s envelope with data.snapshot.

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

POST /v1/records/{route}:batch applies several items to one entity in one transaction, bounded by the entity’s batch.maximumItems and batch.maximumBytes:

{
"items": [
{"operation": "create", "data": {"code": "SITE-002", "label": "East depot"}},
{"operation": "patch", "id": "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": [...]} where each result carries operation, id, revision, etag, and data. Note that the result items keep the older identifier spelling rather than the Registry Record envelope.

changeContext is optional and shared by 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, so two interval edits can be sent in either order. One stale ifMatch or one violated constraint rolls back every item.

Send Idempotency-Key (at most 256 bytes) on any create, patch, tombstone, batch, request action, or immediate action. A repeat with the same key and the same request returns the stored response without writing again. The same key with a different request returns 409 idempotency.conflict. Request actions and immediate actions require the header.

An ingestion run is the durable account of one bulk import that the client drives one bounded chunk at a time. The client keeps the source file; the run keeps the checkpoint. A run binds the active package revision, schema fingerprint, entity, selected access profile, create-or-patch operation, input digest, chunking algorithm, and the expected item and chunk counts. No server worker advances a run: only a chunk submission moves the checkpoint, and every operation rechecks the caller’s authority against the compiled batch route. A run identifier locates a run and grants nothing. Runs are visible to the caller that created them, and another caller’s run and an unknown run are the same concealed 404.

A run response carries operational metadata and bounded failure classifications only: never source rows, committed record values, or chunk bodies.

POST /v1/records/{route}/ingestion-runs with the binding:

{
"operation": "create",
"profileId": "operator",
"packageRevision": "sha256:9f2c…",
"schemaFingerprint": "sha256:1a7b…",
"inputDigest": "5e8d…",
"inputLength": 48211,
"itemCount": 12000,
"chunkCount": 13,
"chunkAlgorithmVersion": "greedy-canonical-http-batch-v1"
}

The response is 201 Created with the run document. inputDigest is the SHA-256 of the whole source as 64 lowercase hexadecimal characters. The chunking contract is greedy-canonical-http-batch-v1; a run refuses any other algorithm, so its remaining source bytes are never reinterpreted under a different chunking contract.

Every run response is one document:

MemberMeaning
runIdThe run identifier.
statusopen, complete, cancelled, or blocked.
blockedReasonPresent when the run is blocked: activePackageChanged.
entityId, operation, profileIdThe bound entity, its create-or-patch operation, and the selected profile.
packageRevision, schemaFingerprintThe binding the run was created under.
inputDigest, inputLength, itemCount, chunkCountThe announced source: digest, byte length, item count, and chunk count.
chunkAlgorithmVersionThe chunking contract the chunks follow.
maximumItems, maximumBytesThe compiled batch bounds every chunk stays inside.
nextChunkIndexThe checkpoint: the chunk index the next submission must name.
committedItems, committedPrefixDigestThe items committed so far and the digest of the source prefix they cover.
lastAttemptnull before the first attempt, otherwise {"outcome", "chunkIndex"}.
createdAt, updatedAtRFC 3339 timestamps.
completetrue when status is complete.

An open run whose binding no longer matches the active package reports blocked with blockedReason activePackageChanged. A blocked run is retained and inspectable, and it never reinterprets its remaining bytes under a new binding: continue in a successor run created under the new package.

lastAttempt.outcome classifies the last submission as committed, replayed, invalidItem, refused, bindingChanged, chunkMismatch, runNotOpen, or unavailable. invalidItem and refused leave the checkpoint where it was; the corrective move is a successor run, not a retry with different bytes and not a skipped row.

POST /v1/records/{route}/ingestion-runs/{runId}/chunks submits the chunk the run’s nextChunkIndex names:

MemberMeaning
chunkIndexThe chunk position, which must equal nextChunkIndex.
itemsThe chunk’s items, canonicalized into one batch body.
digestThe SHA-256 of that canonical batch body, 64 lowercase hexadecimal characters.
prefixDigestThe SHA-256 of every source byte through the end of the chunk, 64 lowercase hexadecimal characters.

These members must match the run’s committed prefix exactly: a divergent chunk index, chunk digest, prefix digest, or binding is refused, and nothing is written. The ingestion routes take no Idempotency-Key header because the server derives the attempt key from the run id, the input digest, the chunk index, and the chunk digest, so resubmitting the exact chunk is safe whether the first response was lost or the transport dropped it. A chunk’s records, revisions, audit, receipt, and the checkpoint advance commit in one transaction.

GET /v1/records/{route}/ingestion-runs lists the caller’s runs: limit (default 25, at most 100), keyset after a run id, and optional status and inputDigest filters. GET /v1/records/{route}/ingestion-runs/{runId} returns one run document with nextChunkIndex, so a restarting client rebuilds its plan from API state alone and never resends a committed chunk.

POST /v1/records/{route}/ingestion-runs/{runId}/cancel cancels an open or blocked run. The counts, the committed prefix, and the audit stay exactly as they were.

GET /v1/records/{route}/ingestion-runs/{runId}/chunks/{chunkIndex}/receipt returns the stored batch response the named chunk’s commit produced, so a lost response is recoverable without re-sending the chunk. A receipt is erased with the record history it describes; a later read then answers 410 ingestion.receipt_erased, and the chunk’s counts stay in the run.

FailureRecovery
Response lost after a chunk commitReread the run and submit the same chunk again. An exact replay returns the original receipt and writes nothing new.
Transport interruptionResubmit the chunk nextChunkIndex names.
An invalid item or a business refusalThe checkpoint stays where it is. Start a successor run for the remainder. Row skipping is never a default recovery.
Authorization lostProgress refuses: every operation rechecks the selected profile against the compiled batch route.
Package or schema binding changedThe run reports blocked with reason activePackageChanged and stays inspectable. Start a successor run under the new binding.
Operator stopCancel the open or blocked run. Counts and audit are preserved.

A run drives the same compiled batch route one chunk at a time, so an existing direct batch client keeps its contract unchanged.

Every write appends a revision; nothing is updated in place. Three route families read that history, each under its own grant operation.

GET /v1/records/{route}/{recordId}/revisions returns a collection of the record’s revisions, and .../revisions/{revision} returns one. A read covers at most 100 revisions. Revisions show stored field values only; derived fields and read paths are not evaluated over history.

Correction context from a batch is absent unless the revisions grant lists provenanceFields, drawn from kind, reasonCode, reasonText, and sourceReferences, and even then only when every revision that batch produced is visible to the caller.

A temporal entity, one with temporal.startField and temporal.endField, gains two collection routes. :current returns the records whose interval contains the current instant, and :as-of returns the records whose interval contains asOf. Intervals include the start and exclude the end, and a null end is open. asOf is a UTC RFC 3339 timestamp such as 2026-06-05T00:00:00Z for either field type.

GET /v1/records/{route}:snapshot reproduces a collection as the registry recorded it. Without snapshot, it captures the latest committed state once and returns the token with the page, so a client can page consistently while writes continue. With snapshot, it reproduces that earlier state. validAt adds an effective-time filter inside the snapshot and is refused on a non-temporal entity. Its value follows the start field’s type: a calendar date such as 2026-06-05 for a date field, a UTC RFC 3339 timestamp for a timestamp field. recordedAsOf is not an option.

A snapshot token is a bookmark, not a credential: the caller needs the snapshot operation on the route, and today’s row boundaries apply. Historical queries use stored fields only.

History starts at an exact empty or verified baseline, and a saved token never falls back to current data. A snapshot whose history is unavailable, whose field metadata no longer matches, or whose database budget of two seconds is exhausted returns 503 source.unavailable. One query resolves at most 64 originating schema descriptors. A retained-history erasure makes every snapshot at or after the earliest erased commit unavailable, including later ones.

A change request is a record of an entity that declares changeRequest. Creating it stores a draft; a submitted proposal changes its targets only when it is applied. BReg owns four source states:

draft ──submit──▶ submitted ──apply with exact approved evidence──▶ applied
▲ │
└────revise─────────┘
draft or submitted ──cancel by owner──▶ cancelled

submit, revise, and cancel belong to the principal that owns the request. Another principal does not see those actions in actions, and calling one regardless is refused with precondition.failed. Review decisions and reviewer stages belong to the configured external authority, such as Registry Casework. BReg does not expose local approval routes.

With review: {mode: none}, the ordinary apply action needs no external result. Otherwise BReg submits the exact frozen proposal binding to review.authority and accepts application only after it has reconciled an approved result for the same subject, policy, and submission digest. onApproved.mode: manual waits for a currently authorized caller. automatic names a logical executor whose separately configured ordinary credential follows the same BReg apply action and guards. A completion notification never carries application authority.

An rhai planner runs at submit. A planner that produces no plan refuses the submission with 400 request.plan_refused, whose detail names the failure kind from the planner’s closed vocabulary and nothing else; a planner that ran out of its time budget returns 503 service.unavailable instead. Nothing is stored either way: the request stays a draft, and the refusal is journaled with the same failure kind.

A read of the request record adds data.request:

{
"bregState": "submitted",
"proposalVersion": 2,
"effectDigest": "sha256:9f2c…",
"proposal": {"review": {"authority": "casework", "policyId": "asset-placement-correction"}},
"review": {
"submission": {"state": "accepted", "authority": "casework", "requestId": "…", "submissionDigest": "sha256:…", "policy": {"id": "asset-placement-correction", "version": "1", "digest": "sha256:…"}},
"result": {"state": "approved", "resultId": "…", "completedAt": "2026-09-20T00:00:00.000000Z", "availableUntil": "2026-10-20T00:00:00.000000Z"},
"delivery": {"state": "reconciled", "eventId": "…", "receivedAt": "2026-09-20T00:00:01.000000Z"},
"application": {"mode": "manual", "state": "ready"},
"recovery": {"state": "none"}
},
"editable": false,
"actions": [
{"operation": "apply_request", "href": "/v1/records/placement-corrections/2d7e…/actions/apply?accessProfile=correction-applier", "ifMatch": "\"breg-hmac-sha256:…\"", "proposalVersion": 2, "effectDigest": "sha256:9f2c…"}
]
}

actions lists only what the caller’s profile may do in the current state, with the exact href and the ifMatch value to send. Follow those links rather than composing routes.

proposal appears once the request has been submitted and describes its frozen review binding. review is separately disclosure-controlled by readableRequestFields: [review_state]. It reports durable submission, terminal result, delivery, application, and recovery state. Optional binding, result, delivery, and application identifiers are omitted until known. These observations do not grant authority. editable is true only while the request is a draft, the caller owns it, and the selected profile may patch it.

A settled result never moves the request out of submitted: its owner acts on it, and the result decides which actions stay on offer.

Result for the current proposalActions offeredRefused
rejectedcancel_requestrevise_request with 409 mutation.conflict; apply_request with 412 precondition.failed
changesRequestedrevise_request with rebase: false, cancel_requesta rebase with 409 mutation.conflict; apply_request with 412 precondition.failed
approved, availableUntil not yet reachedapply_request for the applier; revise_request with rebase: true and cancel_request for the ownernothing beyond the ordinary guards
approved, availableUntil passed before applicationrevise_request with rebase: false, cancel_requesta rebase with 409 mutation.conflict; apply_request with 412 precondition.failed
answered, cancelled, or supersededrevise_request with rebase: true, cancel_requestapply_request with 412 precondition.failed
none yetunchangednothing beyond the ordinary guards

Send the rebase value the revise_request action carries: false records the new draft as a revision of the proposal, true as a rebase onto updated context. An approval that expired unapplied reports review.application.state expired beside the result’s availableUntil, also when its automatic application is still queued; an application already applying keeps that state because it may still succeed. The other application states are awaitingReview, ready, queued, applying, applied, and blocked.

The offered actions follow the result even when the selected profile does not disclose review_state, so the owner and the applier can tell a coarse outcome from which actions remain, for example that a request was rejected. This is deliberate: the same caller would learn as much from the refusal of an action BReg no longer offers.

The read and apply decide expiry on different clocks. The read compares the cached result’s availableUntil with the database clock, while apply fetches the result from the review authority and checks it with the BReg service clock. Close to availableUntil the two can briefly disagree: a read can still offer apply_request that then answers 412 precondition.failed, or report expired while the authority’s result is still available. Apply is authoritative.

A request list whose selected profile discloses review_state may also filter on reviewOutcome, with eq or in. Its values are pending, approved, approvedExpired, rejected, changesRequested, answered, cancelled, and superseded; a request whose current proposal was never submitted for review matches none of them. The filter describes the review only, so combine it with bregState: an applied request keeps the outcome that authorized it, and its approval can pass availableUntil afterwards. The approvals still waiting for an applier, and those that expired before anyone applied them, are:

$filter=bregState eq 'submitted' and reviewOutcome eq 'approved'
$filter=bregState eq 'submitted' and reviewOutcome eq 'approvedExpired'

A profile that does not disclose review_state is refused the filter with 400 query.invalid, the same answer as for any field its grant withholds.

ActionRoute suffixBodyAccepted from
submit/actions/submit{}draft; freezes the effects, review requirement, and application guards, then queues external review submission when required
revise/actions/revise{"rebase": false}submitted; starts a new draft proposal version
revise with rebase/actions/revise{"rebase": true}submitted; starts a new draft after target changes
cancel/actions/cancel{}draft or submitted, only for the principal that owns the request; an accepted external review is cancelled asynchronously
apply/actions/apply{"proposalVersion", "effectDigest"}, optional reason stringsubmitted with an exactly correlated approved external result, or explicit no-review; current source authority and guards are rechecked

Every action requires If-Match and Idempotency-Key. An apply names the exact proposal through proposalVersion and effectDigest; both come from the read that showed the action. BReg rejects a stale or substituted binding before applying any effect.

An apply body may also include an optional reason string of up to 4096 Unicode characters. The server preserves whitespace and Unicode exactly, accepts an empty string, and refuses null, other types, NUL, or longer text with 400 request.invalid. Include the same reason when replaying an action with its original idempotency key; changing it is a different request body. The applied request’s data.request.application carries reasonPresent, and carries reason only when retained and permitted by the selected profile. Request-detail erasure removes the text while retaining the presence flag.

readableRequestFields accepts review_state, which discloses the closed review projection, and actor_reference, which discloses submitterReference and applierReference. Both require a non-anonymous profile. Every change-request action response separately includes actorReference, the caller-supplied actor identity recorded with that action, independent of readableRequestFields.

GET /v1/registry lists the same grant as readableRequestFields on each operation, beside readableFields, so a caller can learn before a read which request fields its selected profile receives. The list follows the rule request reads apply: an anonymous profile, or an entity without a change request, lists no request fields.

History returns up to 50 whole proposals per page. The response byte budget can shorten a page without splitting a proposal. To continue, send the returned data.request.history.nextAfterProposalVersion as requestHistoryAfterProposalVersion on the same request GET, keeping the same access profile. Follow each returned cursor until it is null; a page with fewer than 50 proposals can still have a next page.

The response of an action is not a Registry Record envelope:

{
"id": "2d7e5e0c-…",
"revision": 4,
"snapshot": "…",
"request": {
"bregState": "applied",
"proposalVersion": 2,
"effectDigest": "sha256:9f2c…",
"application": {"applicationId": "7c1d…", "proposalVersion": 2, "effectDigest": "sha256:9f2c…", "appliedAt": "2026-09-02T10:15:00Z"}
}
}

application is null until the request is applied. When the entity sets retention.mode: operator_erase, an operator can erase the proposal payload of a terminal request; a later read then shows request.detailErased: true and the provenance stub stays while target revisions reference the request.

GET /v1/registry describes each request entity’s changeRequest capability: the planner kind (declarative, or rhai with its ABI, limits, and possible write count), the review mode, and the application policy with its allowed dispositions and queue reasons.

The capability also lists the request’s effects: each effect’s id, operation, the target entity with the request field (fromField) or earlier effect (fromEffect) that selects it, the fields it sets with where each value comes from, and the fields it clears. A rhai planner likewise lists its declared writes, each with its target, operation, and fields. Both are filtered to what the caller may read: an effect or write on an entity the caller cannot read is left out, a target field the caller cannot read is not named, and a request field the caller cannot read is never given as a source, nor is an effect that is left out. possibleWriteCount still counts every declared write. Fields in effects and writes are named by their authored field id, as in readableFields, not by the apiName a record uses in data.domainData. To show one under its API name, look up the id in the fields list of a capability on that entity, which gives each readable field’s id and apiName. The description cannot grant write authority: whether a request may be submitted or applied is still decided by the request routes. The Rust, Node.js, and Python clients require effects, and writes on a rhai planner, so use a client and runtime from the same release.

A request entity can declare up to eight attachments[] slots. Each slot holds one file and declares id, required, maximumBytes (1 to 16,777,216), contentTypes (concrete lowercase media types), and classification. An incomplete draft is allowed; submission refuses a missing required slot with 412 Precondition Failed.

Method and routeRequestAuthority and preconditions
PATCH /v1/records/{route}/{record_id}/attachments/{slot_id}Raw nonempty bytes with an allowed Content-TypeDraft owner, selected profile’s request patch grant and slot writableFields, row boundary, record If-Match, Idempotency-Key
DELETE /v1/records/{route}/{record_id}/attachments/{slot_id}Empty bodySame as upload
GET /v1/records/{route}/{record_id}/attachments/{slot_id}?proposalVersion=NNo bodyAuthenticated current request GET authority for the exact proposal version and slot readableFields

All three accept accessProfile. Discover concrete slot paths from caller-filtered OpenAPI or the field schema’s x-registry-attachment extension in /v1/registry. That extension repeats the compiled slot policy: requiredForSubmit, maximumBytes, contentTypes, classification, and the verification policy. Upload and removal return the ordinary record mutation response. Refetch the request to obtain its updated attachment metadata and record ETag. A slot is engine-owned and cannot be set by ordinary create, JSON Patch, or request effects.

A selected slot appears under domainData using its slot ID verbatim. It is null when empty. Filled metadata contains slotId, proposalVersion, filled, erased, byteSize, sha256, contentType, uploadedAt, uploadedBy, and verificationStatus. BReg computes size and SHA-256 from the received bytes. requiredForSubmit in discovery is independent of the ordinary create-field required flag. Current upload limits and allowed media types do not rewrite retained metadata: older proposals can describe content accepted under an earlier slot policy.

verificationStatus is notRequired when external verification is disabled. When configured, an upload is quarantined as pending until the asynchronous verifier returns approved or rejected. Pending and rejected content cannot be downloaded (404 Not Found) and blocks submission with 412 Precondition Failed, including a filled optional slot. Refetch the request to observe the result. External service failure leaves the status pending for retry. Replacing content requires approval for the exact hash, media type, and policy; an existing matching approval can be reused. With verification configured, submission binds the verification policy digest into the frozen manifest.

Upload, replacement, and removal advance request concurrency state. Submission binds the slot manifest into proposal integrity. Revision and rebase carry slot references into the next draft; editing that draft preserves earlier submitted manifests. Downloads use the explicit proposal version, never a hash or storage URL. Owner and applier access use their current compiled request read authority. Casework reviewers read source context through a separately configured Casework source profile; the caller’s delegated credential is checked against that ordinary BReg read grant. applyTargets continues to constrain the applier. Field projection also applies: a slot reaches a caller only through the profile’s readableFields. The authored classification is served so a caller can handle the bytes accordingly; it grants and withholds nothing.

Downloads return complete, integrity-verified bytes with attachment disposition, Cache-Control: no-store, and X-Content-Type-Options: nosniff. Hidden requests, slots, versions, and erased content do not become discoverable through a download. Erasure retains slot ID, proposal version, filled/erased state, size, and hash; it removes content type, verification status, and uploading actor/time from the projected metadata.

An action declared under actions[] applies its effects in one request. Call POST /v1/actions/{action}/target-conditions with the input to learn which records the effects would touch and the ifMatch each one currently has:

{"input": {"assetCode": "AST-0042", "siteCode": "SITE-001"}}

Then call POST /v1/actions/{action} with the same input and those preconditions:

{
"input": {"assetCode": "AST-0042", "siteCode": "SITE-001"},
"preconditions": {"asset": {"ifMatch": "\"breg-hmac-sha256:…\""}}
}

The response names the application and every record the effects wrote:

{
"action": "register-asset",
"applicationId": "7c1d…",
"results": {"asset": {"entity": "asset-item", "recordId": "2d7e…", "revision": 2}}
}

A precondition that no longer matches returns 412 precondition.failed and nothing is written. The caller needs the invoke operation on the action, and its targets row boundaries apply to every record an effect reaches.

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

{
"type": "https://id.registrystack.org/problems/registry-breg/precondition/failed",
"title": "Precondition failed",
"status": 412,
"detail": "The If-Match header does not match the current revision.",
"code": "precondition.failed",
"traceId": "3f1a9c2e5b7d4e6f8a9b0c1d2e3f4a5b",
"fieldPath": null
}

traceId is a 32-character hexadecimal value that also appears in the server’s request log and, for every request the journal records, in its audit record, so an operator can find the refusal without the client sending any record value. A request that presents no credential and is refused before admission is counted on the metrics listener rather than journaled, so its traceId is in the request log only; see Scrape metrics. fieldPath names the offending body member when the refusal is about one.

type is the published identifier for the code, and code is the same fact in the short form a program branches on: the type is the code with each dot read as a path segment, under https://id.registrystack.org/problems/registry-breg/. Branch on code; resolve type when you want the published description of a refusal you have not seen before.

CodeStatusWhen
authentication.refused401A bearer token was presented and rejected: bad signature, wrong issuer or audience, expired, denied key, or a claim outside policy.
resource.not_found404No token, a 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 member when there is one.
query.invalid400A query option was malformed, over a bound, or not permitted by the grant.
query.cursor_invalid400A $skiptoken that is malformed, was issued for a different query, or has expired.
request.plan_refused400A change request was submitted and its bounded planner produced no plan. The detail names the failure kind from a closed vocabulary and nothing else. A planner that ran out of time returns 503 service.unavailable instead.
action.refused422An immediate-action handler intentionally refused the operation. refusalCode identifies a declared reason, detail contains its static label, and optional fieldPath names an input. No effects commit.
action.handler_failed500An activated immediate-action handler failed to produce an accepted result. Operators use the trace identifier and safe server diagnostics to repair the package. A handler 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.
mutation.conflict409A constraint or workflow rule refused the write. A storage-pattern failure includes the declared entityId and fieldId; correct the input before retrying. A 409 does not imply that retrying an unchanged request can succeed.
idempotency.conflict409The Idempotency-Key was reused with a different request.
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 active package revision or schema fingerprint no longer matches the run binding. 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 an entity with encrypted fields while the field-encryption key state is not usable. Entities without encrypted fields keep serving.

An event is declared as a hook on an entity or a module extension. A url handler delivers to a webhook destination bound in the runtime configuration; a rhai or wasm handler runs its reviewed program in the post-commit worker. Field paths in the next table are relative to one hook under entities[].hooks[]; types, defaults, and descriptions come from the generated module schema, and compiler rules follow the table.

Key pathTypeRequiredAccepted valuesConstraints
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.null | objectNoNot specifiedNone specified
handler.abiFor rhai: The handler ABI the script speaks. For wasm: The handler ABI the module speaks.null | stringConditionalNot specifiedNone specified
handler.destinationIdKey in the runtime destination binding.stringConditionalNot specifiedNone specified
handler.kindstringConditional
  • rhai
  • url
  • wasm
None specified
handler.moduleModule path in the project.stringConditionalNot specifiedNone specified
handler.scriptScript path in the project.stringConditionalNot specifiedNone specified
idStable hook contract identifier, sent as ce-type. Use a new identifier for a breaking payload change.stringYesNot specifiedNone specified
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.null | 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
  • request_lifecycle
  • tombstoned
None specified
whenOptional field tests, combined with AND. Omit to run on every matching trigger.null | objectNoNot specifiedNone specified
when.afterEqualsRequired values after the change. Valid with created and patched triggers.objectConditionalNot specifiedSchema default, where declared: {}None specified
when.afterEquals.*Map valueA comparison literal in the closed field-condition language. Objects and arrays are refused during source parsing. The compiler then validates each scalar against the declared Registry field type.boolean | null | number | stringConditionalNot specifiedNone specified
when.beforeEqualsRequired values before the change. Valid with patched and tombstoned triggers.objectConditionalNot specifiedSchema default, where declared: {}None specified
when.beforeEquals.*Map valueA comparison literal in the closed field-condition language. Objects and arrays are refused during source parsing. The compiler then validates each scalar against the declared Registry field type.boolean | null | number | stringConditionalNot specifiedNone specified
when.changedFields whose values must change. Only valid with the patched trigger.arrayConditionalNot specifiedSchema default, where declared: []
  • uniqueItems: true
when.changed[]Array itemstringConditionalNot specifiedNone specified
when.kindstringConditional
  • fields
  • request_lifecycle
None specified
when.toStatesarrayConditionalNot specifiedSchema default, where declared: []
  • uniqueItems: true
when.toStates[]Array itemstringConditionalNot specifiedNone specified
when.transitionsarrayConditionalNot specifiedSchema default, where declared: []
  • uniqueItems: true
when.transitions[]Array itemstringConditionalNot specifiedNone specified

Compiler rules:

  • Hook IDs are unique across the registry.
  • A projection names at least one declared field.
  • An entity hook declares phase: after; the compiler refuses any other phase. handler.kind is url, rhai, or wasm; a local handler names its reviewed script or module path.
  • Production compilation requires every hook to declare a handler, either a bound destination or a reviewed local program.
  • Conditions and projections use authored field IDs, not the record API’s apiName spellings.
  • The event’s classification is the highest classification of every projected or condition field, and a lifecycle event is at least as classified as its request entity.

Destination URLs, keys, network policy, and ceilings live in runtime eventDestinations, keyed by handler.destinationId. The runtime binds the exact compiled destination set, and each destination’s classificationCeiling must cover the event classification; runtime configuration cannot add projected fields.

TriggerchangedbeforeEqualsafterEqualsProjected snapshot
createdNot allowedNot allowedAllowedAfter
patchedAllowedAllowedAllowedAfter
request_lifecycleNot allowedNot allowedNot allowedRequest at transition
tombstonedNot allowedAllowedNot allowedBefore

A create-only entity refuses patched and tombstoned, and tombstoned also requires tombstone support. Only a change request entity permits request_lifecycle. Omitting when selects every matching trigger. With kind: fields, at least one test is nonempty, every test combines with AND, and changed compares before and after values rather than whether a field appeared in the patch. With kind: request_lifecycle, at least one of transitions, toStates, or stages is set and each nonempty list must match; the projection then contains request fields, not target values.

Capture and mutation commit in one transaction. Delivery uses the captured projection; later record or package changes do not recompute it.

Method POST to the configured fixed path, without query parameters, encoded as CloudEvents 1.0 HTTP binary mode with canonical JSON in the body. Every listed header is present on every delivery.

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-typeAuthored event IDExternal event contract, such as record-labelled-v1.
ce-timeRFC 3339 timestampMutation capture time; unchanged by retry or replay.
ce-dataschemaurn:breg:event-schema:<registry-id>:<entity-id>:<event-id>:sha256:<digest>Generated event payload schema identity and fingerprint.
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.

The package carries each event schema at generated/event-schemas/<entity-id>.<event-id>.schema.json. Record identifiers appear in the body, never in CloudEvents headers.

The body is one shared hook envelope, the same envelope every Registry Stack product delivers. Its eight members are required and nothing else appears beside them. The envelope restates the event identity the CloudEvents headers carry, so a receiver that reads only the body still knows which event it holds; a body and its headers can never describe two different events. Delivery attributes stay on the transport: attempt, generation, delivery time, idempotency key, and signature are headers, never envelope members.

FieldTypeMeaning
idUUID stringCaptured event identity; equals ce-id.
typeAuthored event IDExternal event contract; 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 recordRevisionHashed record reference and the revision the change produced. The reference is hmac-sha256:<64-hex>, never the raw record id.
dataschemaurn:breg:event-schema:<registry-id>:<entity-id>:<event-id>:sha256:<digest>Generated event data schema identity; equals ce-dataschema.
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.

data carries the captured event data. The six common members are required, and a lifecycle event also requires request. Other members and unprojected values are absent.

FieldTypeMeaning
entityStringAuthored entity ID.
recordIdUUID stringRecord identity.
revisionPositive integerRecord revision after the mutation.
triggercreated, patched, request_lifecycle, or tombstonedCaptured mutation or request-transition trigger.
packageRevisionsha256:<digest> stringPackage active at capture time.
valuesObjectExactly the declared projection, using authored field IDs; unset optional fields are null.
requestObject; required only for request_lifecycleProposal and workflow identity, transition, states, optional stage and effect digest, stable deduplication key, and reasonPresent. Rejection and request-revision may include the reviewer explanation as reason.

The lifecycle request object requires proposalVersion and workflowRevision as positive integers, transition, fromState, toState, and deduplicationKey as strings, and stage and effectDigest as strings or null. The deduplication key is stable across retries and operator replay. Receiving a lifecycle event grants no authority to approve or apply a request.

{
"causation": {"hop": 0, "root": "<event-uuid>"},
"data": {
"entity": "record",
"packageRevision": "sha256:<package-digest>",
"recordId": "<record-uuid>",
"revision": 2,
"trigger": "patched",
"values": {"code": "EX-003", "label": "Updated label"}
},
"dataschema": "urn:breg:event-schema:<registry-id>:record:record-patched-v1:sha256:<digest>",
"id": "<event-uuid>",
"source": "urn:registrystack:registry:<registry-id>:instance:<instance-id>",
"subject": {"recordReference": "hmac-sha256:<64-hex>", "recordRevision": 2},
"time": "2026-01-01T00:00:00.123Z",
"type": "record-patched-v1"
}

Members appear in canonical JSON order, the order the bytes on the wire use.

X-Registry-Signature is an HMAC-SHA-256 over the request, keyed with the exact bytes resolved by hmacSha256KeyRef, at least 32 bytes, neither trimmed nor hex-decoded. This is a Base Registry Engine format, not a CloudEvents signing standard. Node receivers can use the registry client’s webhook delivery verifier instead of constructing this signing input themselves. The receiver still owns clock-skew, expected-event, and deduplication policy.

The signing input starts with the ASCII bytes breg-webhook-signature-v1. Each later value is prefixed with its byte length as an unsigned 64-bit big-endian integer, in this order:

  1. ce-specversion
  2. ce-id
  3. ce-source
  4. ce-type
  5. ce-time
  6. ce-dataschema
  7. X-Registry-Event-Generation
  8. X-Registry-Delivery-Attempt
  9. X-Registry-Delivery-Time
  10. The method bytes, POST
  11. The configured request path bytes, without the origin
  12. The content type bytes, application/json
  13. Idempotency-Key
  14. The exact body bytes

Header values keep their original bytes; header names are not included. The header value is:

v1=BASE64URL_WITHOUT_PADDING(HMAC_SHA256(key, signing_input))

A receiver compares the signature in constant time, validates the expected source, type, schema, and payload, bounds the skew of X-Registry-Delivery-Time (not ce-time), and deduplicates before it acts. The accepted skew is receiver policy, not a BReg limit.

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 retentioneventDelivery.payloadRetentionDays: 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.

Runtime ceilings may tighten compiled limits, never widen them. A durable attempt audit precedes every send, and the terminal audit, state transition, and payload erasure commit together. Activation refuses a destination change that would strand retained nonterminal work.

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

An idempotency key identifies one delivery generation, not an event across generations. A replay opens a new generation with a new key, so a receiver whose effect must happen once deduplicates on event identity, ce-source plus ce-id, or on a business key.

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

An empty webhook list result is neither a delivery history nor proof of receiver acceptance, and an expired payload cannot be replayed.

Command prefix bregctl; add --format json for a machine-readable report.

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.

list and replay connect directly to PostgreSQL through the runtime configuration, and bind webhook receivers is the operator procedure that runs them against a bound destination. The public record API has no outbox, payload, delivery, or replay endpoint. A replay needs a retained, unexpired dead letter and the matching generation and activated destination binding. The JSON result of list is a deliveries array whose entries carry these fields and no record values:

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.

An entity with a crs84-point field and a geojson.geometryField is also served under /v1/gis for desktop GIS clients. The adapter exposes /v1/gis, /v1/gis/api, /v1/gis/conformance, /v1/gis/collections, /v1/gis/collections/{collection}, and /v1/gis/collections/{collection}/items with bbox, limit (at most 10000), and cursor. It answers with GeoJSON feature collections under the same profile, grant, and row-boundary rules as the record routes, and it declares the profile breg-gis-bounded-crs84-point-bbox-v1. It does not declare OGC API Features conformance, compute collection extents, or add a query engine of its own.

The implementation and generated schemas in the same source checkout are the source of truth. Configuration fields are schema-generated; route, query, problem, and protocol tables are maintained data checked against that implementation.