Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/llms.txt
Full corpus: https://docs.registrystack.org/llms-full.txt

# Base Registry Engine API reference

> Routes, the Registry Record envelope, headers, query options, mutations, history reads, change requests, immediate actions, problem codes, and the event and webhook contract.

import ApiReferenceTable from '../../../components/ApiReferenceTable.astro';
import ConfigurationReference from '../../../components/ConfigurationReference.astro';
import EventReferenceTable from '../../../components/EventReferenceTable.astro';
import configuration from '../../../data/generated/breg-configuration.json';
import { eventConfiguration } from '../../../lib/breg-event-reference.mjs';

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](../../configure/breg/).
For deployment and recovery, use [deploy a registry](../../operate/breg/).
For receivers, delivery, and replay, use
[bind webhook receivers](../../operate/breg-webhooks/).
For every YAML key, use the [configuration reference](../breg-configuration/).
A running server publishes its own OpenAPI document at `/openapi.json`; the schemas there are
the exact ones its package compiled.

## Authentication and profile selection

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.

{/* Evidence: crates/registry-breg/src/auth.rs;
    crates/registry-breg/src/api/mod.rs, router();
    crates/registry-breg/src/query.rs, access_profile;
    crates/registry-breg/src/contract.rs, ProjectAccessProfileSource and AccessPermissionSource. */}

## Routes

`{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.

<ApiReferenceTable id="routes" />

{/* Evidence: crates/registry-breg/src/api/mod.rs, router(), registry_metadata(), entity_schema(), revision_dispatch(), and tombstone_dispatch();
    crates/registry-breg/src/compiler.rs, route_shape() and query_kind_id();
    crates/registry-breg/src/model.rs, MAX_REVISION_HISTORY_RECORDS. */}

## Record envelope

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

```json
{
  "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"
  }
}
```

| Member | Type | Meaning |
| --- | --- | --- |
| `data.recordIdentifier` | string, UUID | Stable identity of the record. |
| `data.revisionIdentifier` | string, `^[1-9][0-9]*$` | Revision counter, starting at `1`. Compare as a string. |
| `data.domainData` | object | The entity's fields under their `apiName`, narrowed to the grant's `readableFields` and to `$select`. |
| `data.snapshot` | string | Present on mutation responses: an opaque token for `:snapshot` reads, at most 4096 bytes. |
| `data.request` | object | Present on change request records: workflow state and the actions the caller may take. |
| `data.requestPresence` | object | Present when the grant declares `requestPresence`: the pending change requests that target this record. |
| `meta.*` | strings | Registry, 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:

| Header | Value |
| --- | --- |
| `Content-Type` | `application/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"`. |
| `Location` | On `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`.

<ApiReferenceTable id="field-encodings" />

{/* Evidence: crates/registry-breg/src/artifacts.rs, record_member_schema() and field_schema();
    crates/registry-breg/src/api/mod.rs, exact_read() and exact_mutation();
    crates/registry-breg/src/compiler.rs, reserved_logical_name();
    products/breg/acceptance/registry-record-conformance/registry.yaml. */}

## Collections and paging

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`:

```json
{
  "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.

{/* Evidence: crates/registry-record/src/lib.rs, RegistryRecordCollectionResponse and RegistryRecord;
    crates/registry-record/tests/contract.rs, product_extensions_are_preserved_at_every_open_level();
    crates/registry-breg/src/query.rs, ParsedReadQueryMode;
    crates/registry-breg/src/api/mod.rs, cursor_invalid();
    crates/registry-breg/src/runtime_config.rs, CursorConfig. */}

## Query options

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.

<ApiReferenceTable id="query-options" />

A filter is an expression over field names and literals:

```text
$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:

```text
bbox=30.0,-2.1,30.9,-1.8
```

{/* Evidence: crates/registry-breg/src/query.rs, parse_filter(), MAX_SELECTED_FIELDS, MAX_IN_VALUES, MAX_FILTER_PREDICATES, MAX_FILTER_DEPTH, MAX_FILTER_NODES, MAX_LITERAL_BYTES, MAX_OPAQUE_VALUE_BYTES, MAX_QUERY_PAYLOAD_BYTES, MAX_BBOX_PARAMETER_BYTES, and MAX_TOP. */}

## Mutations

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`.

### Create

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

```json
{"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

`PATCH /v1/records/{route}/{recordId}` takes a JSON Patch document whose paths start at
`/data/`, followed by the field's `apiName`:

```json
[
  {"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`.

### Tombstone

`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`.

### Batch

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

```json
{
  "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.

### Idempotency

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.

{/* Evidence: crates/registry-breg/src/api/mod.rs, patch_dispatch(), tombstone_dispatch(), and precondition_required();
    crates/registry-breg/src/mutation/request.rs;
    crates/registry-breg/src/postgres/mutation.rs;
    crates/registry-breg/src/artifacts.rs, batch_response_schema;
    crates/registry-breg/tests/postgres_temporal_corrections.rs. */}

## Ingestion runs

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.

{/* Evidence: crates/registry-breg/src/ingestion_store.rs, IngestionRunStatus and IngestionRefusal;
    crates/registry-breg/src/data.rs, ingestion_batch_route() and ingestion_chunk_idempotency_key();
    crates/registry-breg/src/api/mod.rs, router(). */}

### Create a run

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

```json
{
  "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.

### The run document

Every run response is one document:

| Member | Meaning |
| --- | --- |
| `runId` | The run identifier. |
| `status` | `open`, `complete`, `cancelled`, or `blocked`. |
| `blockedReason` | Present when the run is `blocked`: `activePackageChanged`. |
| `entityId`, `operation`, `profileId` | The bound entity, its create-or-patch operation, and the selected profile. |
| `packageRevision`, `schemaFingerprint` | The binding the run was created under. |
| `inputDigest`, `inputLength`, `itemCount`, `chunkCount` | The announced source: digest, byte length, item count, and chunk count. |
| `chunkAlgorithmVersion` | The chunking contract the chunks follow. |
| `maximumItems`, `maximumBytes` | The compiled batch bounds every chunk stays inside. |
| `nextChunkIndex` | The checkpoint: the chunk index the next submission must name. |
| `committedItems`, `committedPrefixDigest` | The items committed so far and the digest of the source prefix they cover. |
| `lastAttempt` | `null` before the first attempt, otherwise `{"outcome", "chunkIndex"}`. |
| `createdAt`, `updatedAt` | RFC 3339 timestamps. |
| `complete` | `true` 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.

{/* Evidence: crates/registry-breg/src/ingestion_store.rs, IngestionRunRecord and IngestionAttemptOutcome;
    crates/registry-breg/src/data.rs, RUN_CHUNK_ALGORITHM_VERSION. */}

### Submit chunks

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

| Member | Meaning |
| --- | --- |
| `chunkIndex` | The chunk position, which must equal `nextChunkIndex`. |
| `items` | The chunk's items, canonicalized into one batch body. |
| `digest` | The SHA-256 of that canonical batch body, 64 lowercase hexadecimal characters. |
| `prefixDigest` | The 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.

{/* Evidence: crates/registry-breg/src/ingestion_store.rs, IngestionChunkBinding and IngestionChunkCommit;
    crates/registry-breg/src/data.rs, canonical_chunk_body() and ingestion_chunk_idempotency_key(). */}

### List and read runs

`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.

{/* Evidence: crates/registry-breg/src/ingestion_store.rs, MAX_RUN_PAGE_SIZE and DEFAULT_RUN_PAGE_SIZE. */}

### Cancel and recover

`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.

{/* Evidence: crates/registry-breg/src/ingestion_store.rs, StoredChunkReceipt and IngestionBlockedReason;
    crates/registry-breg/src/data.rs, ingestion_batch_route(). */}

### Recovery

| Failure | Recovery |
| --- | --- |
| Response lost after a chunk commit | Reread the run and submit the same chunk again. An exact replay returns the original receipt and writes nothing new. |
| Transport interruption | Resubmit the chunk `nextChunkIndex` names. |
| An invalid item or a business refusal | The checkpoint stays where it is. Start a successor run for the remainder. Row skipping is never a default recovery. |
| Authorization lost | Progress refuses: every operation rechecks the selected profile against the compiled batch route. |
| Package or schema binding changed | The run reports `blocked` with reason `activePackageChanged` and stays inspectable. Start a successor run under the new binding. |
| Operator stop | Cancel 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.

{/* Evidence: crates/registry-breg/src/data.rs, ingestion_batch_route(). */}

## History reads

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

### Revisions

`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.

### Effective time

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.

### Snapshots

`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.

{/* Evidence: crates/registry-breg/src/api/mod.rs, revision_dispatch();
    crates/registry-breg/src/query.rs, ParsedSnapshotQuery;
    crates/registry-breg/src/postgres/history_read.rs;
    crates/registry-breg/src/postgres/revision_read.rs;
    crates/registry-breg/src/history_erasure.rs;
    crates/registry-breg/tests/postgres_historical.rs. */}

## Change requests

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:

```text
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`:

```json
{
  "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 proposal | Actions offered | Refused |
|---|---|---|
| `rejected` | `cancel_request` | `revise_request` with `409 mutation.conflict`; `apply_request` with `412 precondition.failed` |
| `changesRequested` | `revise_request` with `rebase: false`, `cancel_request` | a rebase with `409 mutation.conflict`; `apply_request` with `412 precondition.failed` |
| `approved`, `availableUntil` not yet reached | `apply_request` for the applier; `revise_request` with `rebase: true` and `cancel_request` for the owner | nothing beyond the ordinary guards |
| `approved`, `availableUntil` passed before application | `revise_request` with `rebase: false`, `cancel_request` | a rebase with `409 mutation.conflict`; `apply_request` with `412 precondition.failed` |
| `answered`, `cancelled`, or `superseded` | `revise_request` with `rebase: true`, `cancel_request` | `apply_request` with `412 precondition.failed` |
| none yet | unchanged | nothing 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:

```text
$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.

{/* Evidence: crates/registry-breg/src/postgres/request_read.rs, action_is_available and revise_rebase_available;
    crates/registry-breg/src/mutation/request.rs, the settled review outcome check;
    crates/registry-breg/src/review_store.rs, settled_outcome, read_projection, and approved_evidence;
    crates/registry-breg/src/review_integration.rs, from_approved;
    crates/registry-breg/src/postgres/read.rs, REQUEST_REVIEW_OUTCOME_SQL;
    crates/registry-breg/src/compiler.rs, query_operation;
    crates/registry-breg/tests/postgres_change_requests.rs, rejected_review_offers_only_cancel_and_refuses_revise_and_apply,
    changes_requested_review_offers_revision_and_cancel_and_refuses_rebase_and_apply,
    revision_after_changes_requested_records_revision_not_rebase,
    answered_review_hides_apply_and_keeps_revise_and_cancel,
    unexpired_approval_offers_apply_and_the_apply_succeeds,
    expired_approval_hides_apply_and_is_answered_by_a_revision,
    expired_automatic_approval_projects_expired_until_an_apply_is_in_flight, and
    request_list_filters_on_the_settled_review_outcome. */}

<ApiReferenceTable id="request-actions" />

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`.

{/* Evidence: crates/registry-breg/src/artifacts.rs openapi_request_action_input_schema; crates/registry-breg/src/postgres/request_read.rs request_proposal_metadata and may_disclose_review_state; crates/registry-breg/src/review_store.rs read_projection; crates/registry-breg/src/contract.rs readable_request_fields; crates/registry-breg/src/request_retention.rs. */}

`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.

{/* Evidence: crates/registry-breg/src/api/metadata.rs, readable_request_fields;
    crates/registry-breg/tests/http_read_only.rs, workspace_metadata_projects_request_field_disclosure_per_caller_profile. */}

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.

{/* Evidence: crates/registry-breg/src/request_retention.rs load_retained_history; crates/registry-breg/src/postgres/request_read.rs retained_history and bound_request_metadata; crates/registry-breg/src/api/mod.rs requestHistoryAfterProposalVersion. */}

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

```json
{
  "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 `set`s with where each value comes from, and the fields it `clear`s. 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.

{/* Evidence: crates/registry-breg/src/request_workflow.rs, RequestState;
    crates/registry-breg/src/mutation/request.rs, action_requires_request_owner();
    crates/registry-breg/src/api/mod.rs, request_action_dispatch() and planner_failure_problem();
    crates/registry-breg/src/artifacts.rs, request_action_response_schema() and request_capability_metadata();
    crates/registry-breg/src/api/mod.rs, registry_metadata();
    crates/registry-breg/src/fixtures.rs, assert_request_action_shape();
    crates/registry-breg/src/postgres/request_read.rs;
    products/breg/acceptance/asset-site-placement-change-requests/registry.yaml;
    products/breg/acceptance/person-name-change-rhai/registry.yaml. */}

### Request attachments

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 route | Request | Authority and preconditions |
| --- | --- | --- |
| `PATCH /v1/records/{route}/{record_id}/attachments/{slot_id}` | Raw nonempty bytes with an allowed `Content-Type` | Draft 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 body | Same as upload |
| `GET /v1/records/{route}/{record_id}/attachments/{slot_id}?proposalVersion=N` | No body | Authenticated 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.

{/* Evidence: crates/registry-breg/src/contract.rs, AttachmentSlotSource;
    crates/registry-breg/src/artifacts.rs, attachment_metadata_schema() and openapi_attachment_operation();
    crates/registry-breg/src/api/metadata.rs, attachment_request();
    crates/registry-breg/src/attachment_store.rs;
    crates/registry-breg/src/request_workflow.rs, AttachmentManifestEntry. */}

## Immediate actions

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:

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

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

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

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

```json
{
  "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.

{/* Evidence: crates/registry-breg/src/api/mod.rs, router();
    crates/registry-breg/src/contract.rs, ActionSource;
    crates/registry-breg/src/fixtures.rs;
    products/breg/generated/household-contact-actions/generated/openapi.json. */}

## Problem documents

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

```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](../../operate/breg/#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.

<ApiReferenceTable id="problem-codes" />

{/* Evidence: crates/registry-breg/src/api/mod.rs, fixed_problem(), lookup_unresolved(), cursor_invalid(), and precondition_required();
    crates/registry-breg/src/auth.rs;
    crates/registry-breg/src/problem.rs, ProblemCode;
    crates/registry-breg/src/artifacts.rs, problem_schema. */}

## Events and webhooks

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.

<ConfigurationReference contracts={eventConfiguration(configuration)} prefix="breg-events" compact />

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.

{/* Evidence: crates/registry-breg/src/contract.rs, HookSource and HookHandlerSource;
    crates/registry-breg/src/compiler.rs, validate_hooks() and compile_event_delivery_inventory();
    crates/registry-breg/src/event_destination.rs, EventDestinationConfig.
    Configuration fields selected by docs/site/src/lib/breg-event-reference.mjs, eventConfiguration(),
    from products/breg/generated/authoring/registry-module.schema.json through
    docs/site/scripts/generate-breg-configuration.mjs. Run npm run generate from docs/site. */}

### Triggers and conditions

<EventReferenceTable id="triggers" />

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.

{/* Evidence: crates/registry-breg/src/compiler.rs, validate_event_condition();
    crates/registry-breg/src/outbox.rs, condition_matches() and insert_configured_events();
    crates/registry-breg/src/request_events.rs, lifecycle_condition_matches() and insert_request_lifecycle_events();
    crates/registry-breg/src/mutation.rs. */}

### HTTP request

Method `POST` to the configured fixed path, without query parameters, encoded as
[CloudEvents 1.0 HTTP binary mode](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/bindings/http-protocol-binding.md)
with canonical JSON in the body. Every listed header is present on every delivery.

<EventReferenceTable id="headers" />

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.

<EventReferenceTable id="envelope" />

`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.

<EventReferenceTable id="payload" />

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.

```json
{
  "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.

{/* Evidence: crates/registry-platform-hooks/src/envelope.rs, HookEnvelope, EventSubject, and Causation;
    crates/registry-platform-hooks/src/delivery/service.rs, reload_and_send() and accept_stored_envelope();
    crates/registry-breg/src/artifacts.rs, event_data_schema_binding();
    crates/registry-platform-httputil/src/destination.rs, EventDestinationRequestTemplate;
    crates/registry-breg/src/outbox.rs, insert_configured_events() and capture_envelope();
    crates/registry-breg/src/request_events.rs, insert_request_lifecycle_events();
    crates/registry-breg/tests/postgres_webhook_delivery.rs, assert_exact_request(). */}

### Signature

`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](../client-api/#verify-a-webhook-delivery) 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:

```text
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.

{/* Evidence: crates/registry-platform-crypto/src/delivery_signature.rs, SignatureFields, sign_v1(),
    and v1_binds_every_field_and_exact_body();
    crates/registry-breg/src/event_destination.rs, MIN_HMAC_SHA256_KEY_BYTES;
    crates/registry-platform-config/src/secrets.rs, resolve_reference() and read_secret_file();
    products/breg/demo/support/demo.py, _verify_webhook_request(). */}

### Delivery, retry, and replay

<EventReferenceTable id="delivery" />

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.

<EventReferenceTable id="retry-replay" />

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.

<EventReferenceTable id="states" />

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

{/* Evidence: crates/registry-platform-hooks/src/delivery/service.rs, claim(), finalize(), replay(), delivery_idempotency_key(), and verify_retained_bindings();
    crates/registry-breg/src/webhook.rs, WebhookDeliveryService and WebhookDeliveryStatusKind;
    crates/registry-breg/src/compiler.rs, compile_event_delivery_inventory() and MAX_WEBHOOK_PAYLOAD_BYTES;
    crates/registry-breg/src/runtime_config.rs, default_webhook_payload_retention_days();
    crates/registry-breg/src/event_destination.rs, EventDestinationDeliveryCeilings;
    crates/registry-bregctl/src/webhook_lifecycle.rs, list_item(). */}

### Operator commands

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

<EventReferenceTable id="commands" />

`list` and `replay` connect directly to PostgreSQL through the runtime configuration, and
[bind webhook receivers](../../operate/breg-webhooks/) 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:

<EventReferenceTable id="operator-fields" />

{/* Evidence: crates/registry-bregctl/src/lib.rs, WebhookCommand, WebhookListArgs, and WebhookReplayArgs;
    crates/registry-bregctl/src/webhook_lifecycle.rs, WebhookListOutcome and WebhookListItem;
    crates/registry-breg/src/webhook.rs, WebhookOperatorService and MAX_WEBHOOK_STATUS_RESULTS. */}

## Spatial adapter

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.

{/* Evidence: crates/registry-breg/src/api/gis.rs;
    products/breg/acceptance/spatial-service-sites/README.md. */}

## Sources

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.

{/* Route, query, problem, and request tables generated from docs/site/src/data/breg-api.yaml,
    protocol tables from docs/site/src/data/breg-events.yaml, by
    docs/site/scripts/generate-data.mjs. Run npm run generate from docs/site. */}

## Next

- [Deploy a registry](../../operate/breg/) for runtime configuration and erasure.
- [Bind webhook receivers](../../operate/breg-webhooks/) for destinations, the signing key, and replay.
- [Author a registry project](../../configure/breg/) for entities, profiles, change requests, and actions.
- [Base Registry Engine configuration reference](../breg-configuration/) for every project, module, and runtime key.
- [Create and query your first registry](../../tutorials/first-breg/) to see the envelope and headers in a running server.