Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.26.1.
Send registry events to a webhook
For the data publisher
If you finished Create and query your first registry, you are a data publisher with the
checkout, the installed binaries, and a tutorial-work directory, and you want to know what leaves a
registry when a record changes.
In this tutorial you run the business demo, whose launcher starts a local receiver that verifies signatures,
read the event the project declares, cause one delivery yourself, and inspect an automatic retry,
a dead letter, and its replay in the receiver’s report.
You write no receiver and edit no project.
Start the demo with a receiver
Section titled “Start the demo with a receiver”The demo replaces products/breg/demo/.run/ on each launch, and stopping it removes its database container.
Use synthetic records only, and keep anything you want to keep in tutorial-work/.
In the first terminal, at the root of the breg-tutorial checkout and with the quickstart from the first
tutorial stopped, run:
products/breg/demo/run.sh --installed --webhookWait for these two lines; other setup output is omitted:
Webhook delivery, retry, dead-letter inspection, and replay passed.Base Registry Engine business demo is ready.Leave this terminal running. The launcher seeded eight establishments, seven of them operating, and each of those emitted the configured event. Its receiver refused some attempts on purpose, so the report you read later already contains a retry, a dead letter, and a replay.
Read the event declaration
Section titled “Read the event declaration”In the second terminal, save the demo’s run directory and open the module that declares the event:
webhook_run="$PWD/products/breg/demo/.run"cat "$webhook_run/project/modules/business-establishment-summary/module.yaml"An event is a declaration on an entity that says which committed changes leave the registry and what they carry.
Find the events list under the establishment extension:
events: - id: operating-created-v1 trigger: created projection: [establishment-code, operating-status] when: kind: fields afterEquals: operating-status: operating webhook: destinationId: business-event-receiverThree parts do the work.
trigger names the change that may emit the event, here a create.
projection is the complete list of values that may leave: two fields, not the site name or opening date.
when narrows the trigger to changes whose values match, here a new establishment whose operating-status is operating.
webhook.destinationId names a destination by id only; where the request goes is not the project’s business.
Find the destination
Section titled “Find the destination”An event destination is the deployment’s binding for that id: the receiver’s address, network profile, signing key reference, and delivery ceilings. It lives in the runtime configuration, not in the project:
grep -A11 '^eventDestinations:' "$webhook_run/runtime.yaml"eventDestinations: business-event-receiver: origin: http://127.0.0.1:<port> path: /events networkProfile: loopbackDevelopmentHttp dnsFamily: dualStackStrict allowedPrivateCidrs: [] hmacSha256KeyRef: secret:file/webhook-key classificationCeiling: restricted deliveryCeilings: attemptTimeoutMilliseconds: 1000 maximumAttempts: 3The key reference points at a file the launcher generated; the key itself never appears in configuration. Do not edit these running deployment files: a change needs a checked package and a restart.
Now see how the compiled project pairs the declaration with its retry profile:
bregctl explain events "$webhook_run/project"After explain succeeded and the project’s findings, the report has one delivery,
events.establishment.operating-created-v1.webhook.
A delivery is one event bound to one destination, and the report shows what the project declares for it:
deliveryMode is after_commit, maximumAttempts is 5 with retryDelaysMs of 1000, 2000, 4000, 8000,
attemptTimeoutMs is 5000, deadLetter is required, and operatorReplay is true.
The destination’s deliveryCeilings lower those numbers to three attempts and a one-second timeout for this demo.
A ceiling may tighten what the project declares, never widen it.
Preview the delivery
Section titled “Preview the delivery”Render the request the registry would send, without sending it:
bregctl webhook sample "$webhook_run/project" --event operating-created-v1webhook sample succeededevent: operating-created-v1POST <configured-webhook-request-target> HTTP/1.1Accept: application/jsonContent-Type: application/jsonIdempotency-Key: sha256:0000000000000000000000000000000000000000000000000000000000000000X-Registry-Delivery-Attempt: 1X-Registry-Delivery-Time: 2026-01-01T00:00:00ZX-Registry-Event-Generation: 1X-Registry-Signature: v1=<computed-at-delivery>ce-dataschema: urn:breg:event-schema:business-establishments:establishment:operating-created-v1:sha256:<digest>ce-id: 00000000-0000-4000-8000-000000000001ce-source: urn:registrystack:registry:business-establishments:instance:<configured-instance>ce-specversion: 1.0ce-time: 2026-01-01T00:00:00Zce-type: operating-created-v1
{"entity":"establishment","packageRevision":"sha256:0000000000000000000000000000000000000000000000000000000000000000","recordId":"00000000-0000-4000-8000-000000000002","revision":1,"trigger":"created","values":{"establishment-code":"x","operating-status":"operating"}}A CloudEvents delivery is one HTTP POST in CloudEvents 1.0 binary mode: the ce- headers carry the event’s
identity, source, type, and time, and the JSON body carries the entity, record identifier, revision, trigger,
and the projected values under their authored names.
Three registry headers sit beside them: Idempotency-Key so a receiver can drop a duplicate,
X-Registry-Event-Generation with X-Registry-Delivery-Attempt so it can tell a retry from a replay,
and X-Registry-Signature, an HMAC-SHA-256 signature the running worker computes with the destination’s key.
The sample’s identifiers and signature are placeholders; everything else is exact.
Cause a delivery
Section titled “Cause a delivery”Obtain a fresh token from this demo’s Mint client and keep it in an owner-only header file:
webhook_url=$(cat "$webhook_run/breg-origin")umask 077mint token \ --url "$(cat "$webhook_run/mint-origin")/token" \ --client-id business-demo \ --key "$webhook_run/keys/operator/signing-p256-private-jwk" \ > "$webhook_run/secrets/operator-token" &&sed 's/^/Authorization: Bearer /' "$webhook_run/secrets/operator-token" \ > tutorial-work/webhook-authorization.headerSuccess prints nothing; the token lasts five minutes, and the same command renews it. Create an operating establishment:
curl --silent --show-error --fail-with-body \ --header @tutorial-work/webhook-authorization.header \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: tutorial-establishment-9' \ --data '{"data":{"establishmentCode":"ESTABLISHMENT-TUTORIAL-009","siteName":"North Quay Annex","establishmentKind":"warehouse","operatingStatus":"operating"}}' \ --output tutorial-work/establishment.json --write-out 'HTTP %{http_code}\n' \ "$webhook_url/v1/records/establishments?accessProfile=business-operator"HTTP 201Count the events the receiver has accepted:
python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))["events"]))' \ "$webhook_run/webhook-receiver-state.json"The count reaches 8: the seven seeded deliveries plus yours.
The registry evaluated the event inside your create and handed the delivery to a worker that sends it
after the commit, so if you still see 7, count again in a moment.
Repeat the create with its original idempotency key and the count stays 8: a retried request is not a second change.
Create another establishment with operatingStatus set to suspended and a new key, and the count still
stays 8: the when condition did not match.
Read the delivery history
Section titled “Read the delivery history”Open the receiver’s report:
python3 -m json.tool "$webhook_run/webhook-receiver-state.json"verificationFailures is 0: every request carried a valid signature and well-formed headers.
Each event is keyed by its ce-id and lists its attempts, each with a generation, an attempt number,
and whether the receiver accepted it.
Most events show one accepted attempt.
One shows a refused attempt 1 followed by an accepted attempt 2 in the same generation: a retry is the
worker sending the same delivery again after a refused or failed attempt, with the delays you saw in
explain events, the same event id, and the same idempotency key.
Then find the dead letter and its replay:
"attempts": [ { "accepted": false, "attempt": 1, "generation": 1 }, { "accepted": false, "attempt": 2, "generation": 1 }, { "accepted": false, "attempt": 3, "generation": 1 }, { "accepted": true, "attempt": 1, "generation": 2 }],"idempotencyKeys": { "1": "sha256:<key-for-generation-1>", "2": "sha256:<key-for-generation-2>"}Three refused attempts is the destination’s ceiling, so the delivery became a dead letter:
the worker stopped, kept the payload, and left the delivery for an operator.
The launcher then replayed it.
A replay is an operator-requested new generation of the same event: the same ce-id,
X-Registry-Event-Generation raised to 2, a fresh idempotency key, and an audit entry.
The report records no establishment values, only delivery metadata.
Read the dead-letter list the launcher saved before that replay:
python3 -m json.tool "$webhook_run/webhook-list.json"{ "ok": true, "command": "webhook list", "deliveries": [ { "eventId": "<event-id>", "deliveryId": "events.establishment.operating-created-v1.webhook", "generation": 1, "state": "dead_lettered", "attempt": 3, "payloadAvailable": true, "payloadExpiresAt": "<timestamp>", "replayEligible": true } ]}replayEligible is true because the delivery is in its terminal dead-letter state and its payload is still retained.
The demo keeps payloads for one day; after payloadExpiresAt, a replay is no longer possible.
Now list the current queue.
The command connects to PostgreSQL directly, so give it the demo’s certificate authority file:
SSL_CERT_FILE="$webhook_run/tls/ca.pem" bregctl --format json webhook list \ --runtime-config "$webhook_run/runtime.yaml"{ "ok": true, "command": "webhook list", "deliveries": []}The list is empty: the registry erases a payload as soon as its delivery succeeds, and this command shows pending, dead-lettered, and expired work, not a history. The receiver’s report is where you look for what happened, and the public record API exposes no route to the queue at all.
Stop the demo
Section titled “Stop the demo”Pressing Ctrl+C in the first terminal stops Base Registry Engine, Mint, and the receiver, and removes the PostgreSQL
container with every establishment in it.
Stop the demo now.
The reports remain under .run/ until the next launch, beside disposable keys and tokens.
tutorial-work/webhook-authorization.header holds a token: the checkout’s .gitignore keeps it out of
commits, so keep it out of support messages too.
What you built
Section titled “What you built”You caused one signed CloudEvents delivery from a record you created, read the declaration that selected it and the destination that received it, and traced a retry, a dead letter, and its replay through the receiver’s report and the registry’s queue listing. Declaring events in your own project, and binding receivers with real keys and ceilings, come next.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Next move |
|---|---|
A request returns 401 | The token expired. Rerun the mint token command in Cause a delivery while the first terminal is still running. |
The count stays at 7 | Delivery runs after the commit, on the worker’s schedule. Count again after a few seconds, then check .run/logs/ under the demo directory. |
webhook list reports webhook.operation.refused | The command could not verify the database’s TLS certificate. Prefix it with SSL_CERT_FILE="$webhook_run/tls/ca.pem" as shown. |
| The launcher stops before the ready lines | Read its last lines for the cause, then rerun it; a rerun replaces the previous disposable run. |
- Author a registry project to declare events on your own entities.
- Bind webhook receivers for destinations, keys, retry limits, and replaying a dead letter yourself.
- Base Registry Engine API reference for the delivery contract a receiver validates, verifies, and deduplicates.