Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.43.0.
One package carries every maintained Registry Stack client API: @registrystack/client on npm and
registry-stack-client on PyPI. It holds one namespace per product: discovery for Registry
Discovery, evidence for Registry Evidence, breg for Base Registry
Engine (BReg), casework for Registry
Casework, messaging for Registry Messaging, and scheduling for
Registry Scheduling. Each namespace keeps its own routes,
authentication, errors, and verification rules, because those contracts differ.
Rust is the canonical implementation. The Python and Node packages are thin bindings over it: they convert native values, construct the Rust client, run one method, and map the validated result or error. They implement no Python or JavaScript HTTP, route, authentication, Problem, redirect, retry, cache, or pagination policy.
Availability and compatibility
Section titled “Availability and compatibility”Registry Stack is pre-1.0 Beta software. Keep a client on the same Registry Stack release as the deployment it calls unless a release note states a wider compatibility range.
The unified packages are published beginning with Registry Stack v0.26.1. Releases through v0.26.0
carry no Base Registry Engine client package and no unified package; they distribute one standalone
package per product instead. The casework namespace is present in those same unified packages
beginning with Registry Stack v0.30.0; the messaging namespace is present
beginning with Registry Stack v0.38.0; the scheduling namespace is present
beginning with Registry Stack v0.40.0.
Python requires version 3.11 or later and the wheels use the
Python 3.11 stable ABI (abi3-py311), so a later Python imports the same file. Node requires version 24.11
or later. Both packages select a native build for Linux amd64 with glibc, Linux arm64 with glibc, or
macOS arm64, and require glibc 2.17 or newer on Linux. The version-specific GitHub Release retains
the exact wheel and npm tarballs for an offline install.
The Rust crates have publish = false, stay source-only in the Registry Stack workspace, and are
not published to crates.io.
Install a client
Section titled “Install a client”Read the version from the deployment’s binary rather than typing it, then install that exact version:
python -m pip install "registry-stack-client==<version>"npm install "@registrystack/client@<version>"The distribution installs as registry-stack-client and imports as registry_client. The two
spellings differ, so import registry_stack_client raises ModuleNotFoundError.
A TypeScript consumer also needs the Node type definitions, and must name them. The published
declarations use Buffer, a Node.js global, and TypeScript 6 does not load installed @types
packages on its own, so a project compiling against the declarations reports TS2591 on every use
of Buffer until it has both installed the package and listed it in compilerOptions.types:
npm install --save-dev "@types/node"{ "compilerOptions": { "types": ["node"] }}The package does not declare that dependency itself, because a JavaScript consumer does not need it.
A Rust application takes the source crate from the matching release tag. registry-stack-client is
the facade that re-exports the product clients under breg, casework, discovery,
evidence, messaging, and scheduling modules; a single-product application can depend on that product’s crate
directly instead.
[dependencies]registry-stack-client = { git = "https://github.com/registrystack/registry-stack", tag = "v<version>" }One import, one namespace per product
Section titled “One import, one namespace per product”from registry_client import breg, casework, discovery, evidence, messaging, schedulingconst { breg, casework, discovery, evidence, messaging, scheduling } = require('@registrystack/client');use registry_stack_client::{breg, casework, discovery, evidence, messaging, scheduling};The unified packages carry no Registry Coordinator namespace. Its client is the
registry-coordinator-client Rust crate, with Node.js and Python bindings built from source in the
same repository. The Registry Coordinator API reference describes
the routes it reaches.
Import only the namespaces an application uses. Nothing crosses a namespace, and a Discovery selection is inert metadata that grants no access.
What every client shares
Section titled “What every client shares”The naming convention is fixed across the bindings:
- Rust methods that perform an exchange are asynchronous.
- Python uses the Rust method and option names in snake case, takes options as keyword arguments,
and is synchronous. Each method releases the global interpreter lock while a private Tokio runtime
performs the exchange, so other Python threads keep running. Nothing here crosses into
asyncio. - Node uses the camel case form, takes read options as one object, and returns a promise from every method that performs an exchange. A method that performs none returns its value directly.
- Reads return language-native values: dicts, lists, and
bytesin Python; plain objects, arrays, andBufferin Node. No wrapper type is needed to read a result. - Timeouts are seconds in Python and milliseconds in Node.
trusted_root_certificatesaccepts a PEM bundle asbytesin Python;trustedRootCertificatesaccepts it as a string in Node, except in the Discovery namespace, where it is aBuffer.
A method that sends an already-prepared request performs at most one service exchange, except for the same-key retry of a keyed mutation. A configured private-key-JWT provider can make a separate token-endpoint exchange when it needs to acquire or refresh a bearer. No client follows redirects, reads ambient proxy configuration, fetches referenced schemas, or advances pagination on its own, and none retries any other exchange.
The Evidence client’s profile-driven methods are the exception. request, prepare_progressive,
contracts_candidate, and refresh_metadata assemble or refresh a cached service snapshot first,
which can cost a protected-resource metadata fetch, an authorization-server metadata fetch, and, for
the https-discovery and local-loopback-discovery trust profiles, a service JWKS fetch. request,
prepare_progressive, and contracts_candidate also fetch the published definitions catalog when
the profile’s contracts are published rather than a reviewed file. A cached, unexpired snapshot
skips all of it.
Base URLs may carry a deployment prefix. HTTPS is required except for loopback HTTP, and the URL cannot contain credentials, a query, a fragment, or ambiguous empty path segments.
Unknown outcomes and the same-key retry
Section titled “Unknown outcomes and the same-key retry”A 5xx answer may follow a commit. A BReg, Casework, Messaging,
or Scheduling mutation that carries an idempotency key has an unknown outcome after every 5xx answer,
after a timeout or a broken exchange once the request was sent, and after a protocol failure: a 3xx
answer, or an answer that is oversized, cannot be parsed, or does not meet the contract. Its outcome
is known after a refusal before sending, a connection that was never established (a connect timeout
or a TLS failure included), and every typed 4xx refusal, such as a 429 limit or an idempotency
conflict. A credential the BReg token provider could not supply is known too: the client acquires it
once, before the first send. BReg’s 410 idempotency.expired is a known refusal as well: the first
attempt committed, and only its held response is gone.
Two typed BReg 500 failures are known outcomes too: action.handler_failed and
statistical_dataset.domain_violation. The engine returns them only with the attempt rolled back,
and the same request fails the same way, so the client never resends them. Every other 5xx answer
stays unknown.
The client resends the identical request, with the same key, headers, and body bytes, after a 5xx
answer with an unknown outcome, a timeout, or a broken exchange: at most twice by default, waiting 250 ms and then 500 ms, or
the service’s Retry-After when that is longer and at most 5 seconds. A longer requested wait, or a
Retry-After that is not one delta-seconds value, ends the retries. A protocol failure is resent
only when its answer carried a 5xx status, and nothing is resent after a 4xx status line, even when
the rest of that answer could not be read. Each attempt has the full request timeout. Reads, and
operations that carry no idempotency key, are never resent.
In Rust, is_outcome_unknown() on the client error reports the classification, and
with_max_mutation_retries on the client configuration sets the count from 0, which sends once, to
2. The Python and Node clients run the Rust client and apply the same rule. Python takes the count
as the max_mutation_retries constructor keyword and Node as the maxMutationRetries config member;
a value outside 0 to 2 is a configuration error. The Python error’s outcome_unknown attribute and
the Node error’s outcomeUnknown property report the classification as a boolean.
When the returned error still has an unknown outcome, the safe recovery is the same request with the same key, sent within the service’s receipt horizon: the service replays or settles it. A new key can apply the mutation twice. A refusal that answers a resend does not prove the earlier attempt left no effect, so the client returns the earlier unknown-outcome error instead of that refusal.
The receipt horizon is how long a service holds a key’s first answer: BReg’s
retention.receiptRetentionDays, 7 days by default,
Messaging’s retention.receiptRetentionDays, and Scheduling’s
retention.receiptRetentionDays. Every service answers a spent key by one rule: a different request
under it is an idempotency conflict, 409 idempotency.conflict in BReg and
409 idempotency.key-reused in the others, whatever the key’s age. Past the horizon the same
request is refused with 410 idempotency.expired and the key stays spent. In BReg that 410 means
the first attempt committed: read the record or its history rather than sending the write again
under a new key. In Casework it means an earlier attempt under the key committed and retention
erased only its stored response: reconcile by reading the review request, task, or Directory before
choosing a new key. In Messaging, once retention deletes
the message record after retention.recordRetentionDays, the request hash goes with it and any request under
the key is refused with 410.
In Scheduling, the keyed commands are hold, appointment create, reschedule, and cancel. Releasing a hold carries no key and is never resent.
In Casework, every keyed review-task and Directory mutation is resent, and the error’s
mutation_class() is Ambiguous exactly when is_outcome_unknown() holds. Review request create
and cancel carry a key but go through the review client’s single exchange and are never resent.
Grant revocation and the caseload and clock previews carry no key and are never resent either.
Casework answers every 503 with Retry-After: 5, so a resend after one waits 5 seconds.
Registry Discovery
Section titled “Registry Discovery”The Discovery client searches a published index, resolves a requirement to Evidence Types, and records an inert exact selection. It is not a trust broker: a selection carries catalog metadata and proves nothing about origin authenticity, catalog currentness, authorization, or adopter trust. Endpoint trust and the call that follows stay with the relying application.
Construct a Discovery client
Section titled “Construct a Discovery client”use registry_discovery_client::{DiscoveryClient, DiscoveryClientConfig};use url::Url;
let config = DiscoveryClientConfig::new(Url::parse("https://discovery.example.invalid/")?);let client = DiscoveryClient::new(config)?;from registry_client import discovery
client = discovery.DiscoveryClient( base_url="https://discovery.example.invalid/", request_timeout_seconds=30, connect_timeout_seconds=10, maximum_response_bytes=8 * 1024 * 1024,)const { discovery } = require('@registrystack/client');
const client = new discovery.DiscoveryClient({ baseUrl: 'https://discovery.example.invalid/', requestTimeoutMilliseconds: 30_000, connectTimeoutMilliseconds: 10_000, maximumResponseBytes: 8 * 1024 * 1024,});Node also accepts the base URL as a bare string. The client sends no credentials: a Discovery index is public by construction, so there is no authorization option to set.
DiscoveryClientConfig provides with_request_timeout, with_connect_timeout,
with_maximum_response_bytes, and with_trusted_root_certificates.
Discovery operations
Section titled “Discovery operations”| Operation | Rust | Python | Node |
|---|---|---|---|
| Resolve a requirement to Evidence Types | resolve_evidence_types | resolve_evidence_types | resolveEvidenceTypes |
| Search the catalog | search_services | search_services | searchServices |
| Search Evidence services | search_evidence_services | search_evidence_services | searchEvidenceServices |
Only those four operations reach the network. Every selection below is local and offline.
Selections
Section titled “Selections”A search returns ServiceSearchResponse, holding a catalog_revision and the matched
ServiceRecord items. Selecting turns exactly one record into a selection bound to the capability
that matched it.
| Selection | Rust | Python and Node |
|---|---|---|
| One record by identifier and capability | response.select_exact(request) | select_exact and selectExact |
| One Registry Evidence service | response.select_evidence(request) | select_evidence_service and selectEvidenceService |
| One Evidence Type mapping alternative | response.select_alternative(id) | select_evidence_alternative and selectEvidenceAlternative |
Rust supplies the selections as extension traits on the response, ServiceSearchSelectionExt and
EvidenceTypeResolveSelectionExt. Python and Node expose the same four both as client methods and
as module functions, so an application that stored a response can select from it without holding a
client.
Three local operations act on a stored selection in Python and Node:
validate_selection_structureandvalidateSelectionStructurecheck closed shape and capability binding, and nothing else. Structural validation grants no trust.accept_selectionandacceptSelectionapply an adopter-owned local policy callback and return anAcceptedServiceSelectioncarryingendpoint_urlorendpointUrl. A callback that returns false raises kindlocal-acceptance-refused.renew_unchanged_selectionandrenewUnchangedSelectionreturn a freshly reselected service only when its trust-relevant semantics are unchanged, and raise kindselection-changedotherwise. The current selection must come from a new online lookup.
Rust names the same three validate_service_selection_structure, accept_service_selection, and
renew_unchanged_service_selection.
Discovery error contract
Section titled “Discovery error contract”Python raises discovery.DiscoveryClientError and Node throws the same named class. Both carry
kind plus status, problem, and the transport subcategory (transport_kind in Python,
transportKind in Node) when the failure carries them. kind is one of configuration, query,
no-matching-service, ambiguous-selection, no-matching-alternative, ambiguous-alternative,
capability-mismatch, local-acceptance-refused, selection-changed, transport, problem,
protocol, and client. Rust returns the matching DiscoveryClientError variants.
For kind problem, problem is one of invalid-request, not-found, result-bound-exceeded,
unavailable, or unknown.
Registry Evidence
Section titled “Registry Evidence”The Evidence client requests minimum-disclosure assertions over the public Evidence HTTP contract
and verifies every response. Verification is the point of the client: a client built directly with
EvidenceClientConfig::new, or read from_profile with the pinned-jwks trust profile, verifies
against only a key set the caller pinned out of band; the deployment’s own published key set is
never fetched to judge a response it also produced. A profile using the default https-discovery
trust (or local-loopback-discovery) instead fetches that published key set itself before verifying
against it.
Construct an Evidence client
Section titled “Construct an Evidence client”trusted_jwks and revoked_key_ids are mandatory trust inputs pinned out of band. A key set or
revoked-key list the verifier could never use is refused at construction.
use std::sync::Arc;use registry_evidence_client::{EvidenceClient, EvidenceClientConfig, StaticToken};use url::Url;
let config = EvidenceClientConfig::new( Url::parse("https://evidence.example.invalid/")?, Arc::new(StaticToken::new(token)?), trusted_jwks, Vec::new(),);let client = EvidenceClient::new(config)?;from registry_client import evidence
client = evidence.EvidenceClient( base_url="https://evidence.example.invalid/", trusted_jwks=trusted_jwks, revoked_key_ids=[], token=token, request_timeout_seconds=30, connect_timeout_seconds=10,)const { evidence } = require('@registrystack/client');
const client = new evidence.EvidenceClient({ baseUrl: 'https://evidence.example.invalid/', trustedJwks: trustedJwks, revokedKeyIds: [], token: { static: token }, requestTimeoutMs: 30_000, connectTimeoutMs: 10_000,});maxResponseBytes and max_response_bytes bound the signed response send reads.
maxMetadataBytes and max_metadata_bytes bound the documents discover and fetch_jwks read,
which are neither signed nor verified, and are a separate decision. user_agent and userAgent
replace the default; trusted_root_certificates and trustedRootCertificates add trust roots for
the deployment.
Token configurations
Section titled “Token configurations”token names exactly one credential source. A configuration naming two is refused, so merging two
authentication settings cannot silently pick one. The two bindings spell the choice differently, and
this is the one place in the package where they do:
| Source | Python | Node |
|---|---|---|
| A fixed bearer, sent unchanged | the bearer as a plain string | { static: token } |
| A private-key-JWT client-credentials exchange | {"private_key_jwt": {...}} | { privateKeyJwt: {...} } |
A Python token that is neither a string nor an object carrying exactly the one key
private_key_jwt is refused at construction with kind configuration. Note that this differs from
the authorization option of the BReg namespace, which names the static case explicitly
in both languages.
The private-key-JWT configuration requires token_endpoint, client_id, and client_key (Node:
tokenEndpoint, clientId, clientKey). Its optional members in Python are audience,
resource, scopes, assertion_lifetime_seconds, refresh_margin_seconds,
request_timeout_seconds, connect_timeout_seconds, and user_agent. Node takes the same eight
in camel case, with requestTimeoutMs and connectTimeoutMs for the timeouts, and adds string-valued
trustedRootCertificates for the token endpoint. Python has no nested trust anchor for the token
endpoint: its top-level trusted_root_certificates crosses as genuine bytes and the nested
configuration crosses as JSON, which carries no byte values. client_key holds the private half; it
stays in the process and reaches nothing but the token endpoint. Rust passes an
Arc<dyn TokenProvider> instead, and can implement a custom provider.
The built-in exchange sends grant_type, client_id, client_assertion_type, and
client_assertion, adds scope when scopes is set, and adds the RFC 8707 resource when
resource is set. It sends no other form member. Use a pre-acquired short-lived static bearer when
an issuer requires one.
Read a client from a profile
Section titled “Read a client from a profile”from_profile and fromProfile read an application-owned Evidence client profile, which carries
the base URL, a trust configuration, a contracts configuration, and the credential configuration in
one file; each is a reference to material, never the material itself. The profile is the owner-only
JSON document evidencectl client profile create --output writes. The trust configuration decides
whether the deployment’s published key set is fetched: the default https-discovery trust (and
local-loopback-discovery) fetches and verifies against it, while pinned-jwks instead points to a
reviewed local key set file, so nothing is fetched. The optional second argument is a private JWK
held in memory, for an application that reads its key from a secret manager rather than from disk.
It is never retained by the wrapper, and profile paths and file-backed secrets are redacted from
every error.
client = evidence.EvidenceClient.from_profile("evidence-client.json")const client = evidence.EvidenceClient.fromProfile('evidence-client.json');Rust names the same entry points EvidenceClient::from_profile,
EvidenceClient::from_profile_with_key, EvidenceClient::from_profile_path, and
EvidenceClient::from_profile_path_with_key.
Prepare, send, and verify
Section titled “Prepare, send, and verify”The three-step path closes what an application expects before anything is sent, spends that expectation on one exchange, and judges the bytes that come back:
| Step | Rust and Python | Node | Returns |
|---|---|---|---|
| Close the expectations and generate a nonce | prepare(spec) | prepare(spec) | PreparedEvidenceRequest |
| Send the prepared request | send(prepared) | send(prepared) | RawEvidenceResponse |
| Verify the response against the request | verify(prepared, response) | verify(prepared, response) | VerifiedEvidence |
| Send and verify in one call | request_and_verify(prepared) | requestAndVerify(prepared) | VerifiedEvidence |
| Verify at a stated instant | verify_as_of(prepared, response, at) | verifyAsOf(prepared, response, atMillis) | VerifiedEvidence |
| Read the published definitions | discover() | discover() | The definitions document |
| Read the published key set | fetch_jwks() | fetchJwks() | A JWKS document |
| Refresh cached deployment metadata | refresh_metadata() | refreshMetadata() | Nothing |
prepare performs no input or output. The request it returns is good for exactly one exchange:
spend it with send or request_and_verify. Python takes the instant for verify_as_of as Unix
seconds; Node takes asOfMillis as Unix milliseconds.
The batch forms take the same shape one level up: prepare_batch, send_batch, verify_batch,
request_and_verify_batch, and verify_batch_as_of (camel case in Node). A verified batch carries
items, each {"status": "available", "verified": ...} or {"status": "not_available"} (Node:
notAvailable). SdJwtVcBatchResponse reads the issuance envelope answering a request that
presented several holder keys: credentials in the order the request presented them, count, and
credential_for_holder_key(index) or credentialForHolderKey(index). Reading the envelope judges
nothing; each credential is verified individually.
A VerifiedEvidence carries evidence (the verified payload, field for field), the trace
identifier, and pinned_subject_expectations or pinnedSubjectExpectations. Persist those bindings
after a first-use acceptance and pass them back in the next request specification. Python spells the
specification member subject_expectations, taking either "accept_first_use" or the sequence of
role bindings itself. Node spells it subjectExpectations, taking either 'acceptFirstUse' or
{ pinned: [...] }.
One-call requests and progressive results
Section titled “One-call requests and progressive results”request is the short path for an application configured from a profile: it names a requirement and
its selectors, and the client resolves the definition, prepares, sends, and verifies in one call.
result = client.request("adult-status", person_id="person-123")print(result.values)const result = await client.request({ requirement: 'adult-status', selectors: { personId: 'person-123' },});console.log(result.values);Python takes the requirement positionally and the selectors as keyword arguments, with
response_format, subjects, and binding_receipt as named options. Node takes one
AudienceScopedRequest object with requirement, responseFormat, selectors, subjects, and
bindingReceipt. Use subjects when a request has several roles, or when a selector field needs a
name the option map reserves.
The result is one of two variants, discriminated by the response format closed before the request was sent:
responseFormat | Node type | Python type | Payload member |
|---|---|---|---|
signed-jws | ProgressiveVerifiedAssertion | VerifiedAssertion | assertion, the signed bytes; credential is null |
sd-jwt-vc | ProgressiveVerifiedAudienceScopedCredential | VerifiedAudienceScopedCredential | credential, the serialized credential; assertion is null |
Both variants carry evidence, values, value, the trace identifier, and subject_continuity or
subjectContinuity. Continuity has a status of first_use or matched (Node: firstUse or
matched) and an opaque receipt. The receipt is the application’s to keep: nothing in the package
persists one. Pass a retained receipt back as binding_receipt or bindingReceipt to assert that
the next answer describes the same subject. Python serializes one with
SubjectBindingReceipt.to_json and reads it back with SubjectBindingReceipt.from_json.
Trace identifiers
Section titled “Trace identifiers”RawEvidenceResponse, VerifiedEvidence, and every progressive result carry the validated W3C
trace identifier for the exchange, as trace_id in Python and traceId in Node. It is None or
null when the response carried none. It is support correlation only: it is not an Evidence audit
operation identity, and it is not a substitute for the deployment’s audit record.
Evidence error contract
Section titled “Evidence error contract”Python raises one of eight subclasses of evidence.EvidenceClientError, so an application can catch
the category it handles and let the rest propagate. Node throws evidence.EvidenceClientError with
kind carrying the same eight values.
| Kind | Python class | Meaning |
|---|---|---|
configuration | ConfigurationError | The construction inputs or the request specification were refused, or a prepared request had already spent its single send |
nonce | NonceError | The request nonce could not be generated |
token | TokenError | The configured credential source could not supply a bearer |
transport | TransportError | The exchange failed below HTTP |
denied | DeniedError | Evidence refused the request |
not-available | NotAvailableError | Evidence could not be produced: no match, an ambiguous match, a missing fact, or a source that answered with no record, all collapsed into one answer |
protocol | ProtocolError | The response violated the contract |
verification | VerificationError | The response did not satisfy the pinned verification |
kind is always present. The rest are present only when the underlying failure carries them:
status on denied, protocol, and a token failure whose token subcategory is protocol;
code on denied, protocol, verification, and a token failure whose token subcategory is
refused; the trace identifier on denied, not-available, and protocol; the retry delay on
denied and protocol; the transport subcategory on transport and a token failure whose token
subcategory is transport; and the token subcategory on every token failure. Python spells them
status, code, trace_id, retry_after_seconds, transport_kind, and token_kind; Node spells
them status, code, traceId, retryAfterSeconds, transportKind, and tokenKind.
Base Registry Engine
Section titled “Base Registry Engine”The Base Registry Engine (BReg) client reads and writes Records, invokes Actions, and works Proposals in one registry. Every write goes through an opaque binding selected from the registry’s caller-filtered metadata. This section covers construction, reads, writes, and errors. The BReg client capabilities reference lists every operation, with Proposals, recovery after a restart, and the full error tables.
Construct a BReg client
Section titled “Construct a BReg client”use registry_stack_client::breg::{BaseRegistryClient, BaseRegistryClientConfig};use url::Url;
let config = BaseRegistryClientConfig::new( Url::parse("https://breg.example.invalid/institution-a")?,);let client = BaseRegistryClient::new(config)?;from registry_client import breg
client = breg.BaseRegistryClient( base_url="https://breg.example.invalid/institution-a", authorization={"static": token}, request_timeout_seconds=30, connect_timeout_seconds=10, max_response_bytes=8 * 1024 * 1024,)const { breg } = require('@registrystack/client');
const client = new breg.BaseRegistryClient({ baseUrl: 'https://breg.example.invalid/institution-a', authorization: { static: token }, requestTimeoutMilliseconds: 30_000, connectTimeoutMilliseconds: 10_000, maxResponseBytes: 8 * 1024 * 1024,});The Rust facade does not re-export Url; add the url crate beside it. BaseRegistryClientConfig
provides with_token_provider, with_request_timeout, with_connect_timeout,
with_max_response_bytes, with_user_agent, with_trusted_root_certificates, and
with_max_mutation_retries. Node and Python take the same settings as constructor options.
authorization is optional. Without it the client reaches only the health and readiness probes,
which never send a bearer token; every other route refuses a request without one. An unsupported
shape is refused at construction with kind configuration.
Verify a webhook delivery
Section titled “Verify a webhook delivery”verifyWebhookDelivery in Node and verify_webhook_delivery in Python authenticate one exact BReg
Version 1 webhook request with the shared Rust verifier. Pass the received method, request target,
headers, body bytes, and key without normalizing them. The result carries the authenticated
CloudEvents attributes, delivery metadata, and body. It does not decide whether the delivery is
fresh, expected by this receiver, or already applied; the receiver checks those, in this order.
In both examples, event_store.apply_once() (eventStore.applyOnce() in Node) is an
application-owned transaction that claims the event identity and commits the effect once, and
apply_registry_event() applies the payload the receiver expects.
const { breg } = require('@registrystack/client');
const MAX_BODY_BYTES = 1_048_576;const MAX_CLOCK_SKEW_MS = 5 * 60 * 1_000;const EXPECTED_SOURCE = 'urn:registrystack:registry:example:instance:primary';const EXPECTED_TYPE = 'case-created-v1';const EXPECTED_DATASCHEMA = 'urn:registrystack:registry:example:event:case-created-v1:schema:sha256:aaa';
function exactHeaders(rawHeaders) { const headers = Object.create(null); const names = new Set(); for (let index = 0; index < rawHeaders.length; index += 2) { const name = rawHeaders[index]; const normalized = name.toLowerCase(); if (names.has(normalized)) throw new Error('duplicate webhook header'); names.add(normalized); headers[name] = rawHeaders[index + 1]; } return headers;}
async function exactBody(request) { const chunks = []; let length = 0; for await (const chunk of request) { const bytes = Buffer.from(chunk); length += bytes.length; if (length > MAX_BODY_BYTES) throw new Error('webhook body is too large'); chunks.push(bytes); } return Buffer.concat(chunks, length);}
async function receiveWebhook(request, response, key, eventStore) { const body = await exactBody(request); const delivery = breg.verifyWebhookDelivery({ method: request.method, path: request.url, headers: exactHeaders(request.rawHeaders), body, key, });
const skew = Math.abs(Date.now() - Date.parse(delivery.deliveryTime)); if (!Number.isFinite(skew) || skew > MAX_CLOCK_SKEW_MS) { throw new Error('webhook delivery is outside the accepted clock skew'); } if (delivery.source !== EXPECTED_SOURCE || delivery.type !== EXPECTED_TYPE || delivery.dataschema !== EXPECTED_DATASCHEMA) { throw new Error('unexpected webhook contract'); }
const eventIdentity = JSON.stringify([delivery.source, delivery.id]); await eventStore.applyOnce(eventIdentity, async () => { await applyRegistryEvent(JSON.parse(delivery.body.toString('utf8'))); }); response.writeHead(204).end();}import jsonfrom datetime import datetime, timezone
from registry_client import breg
MAX_CLOCK_SKEW_SECONDS = 5 * 60EXPECTED_SOURCE = "urn:registrystack:registry:example:instance:primary"EXPECTED_TYPE = "case-created-v1"EXPECTED_DATASCHEMA = ( "urn:registrystack:registry:example:event:case-created-v1:schema:sha256:aaa")
def receive_webhook(method, path, headers, body, key, event_store): delivery = breg.verify_webhook_delivery( method=method, path=path, headers=headers, body=body, key=key, )
sent = datetime.fromisoformat(delivery["delivery_time"]) skew = abs((datetime.now(timezone.utc) - sent).total_seconds()) if skew > MAX_CLOCK_SKEW_SECONDS: raise ValueError("webhook delivery is outside the accepted clock skew") if (delivery["source"], delivery["type"], delivery["dataschema"]) != ( EXPECTED_SOURCE, EXPECTED_TYPE, EXPECTED_DATASCHEMA, ): raise ValueError("unexpected webhook contract")
event_identity = json.dumps([delivery["source"], delivery["id"]]) event_store.apply_once( event_identity, lambda: apply_registry_event(json.loads(delivery["body"])), )Python takes headers as a dict, which cannot hold a repeated header, so refuse a request that
repeats one before building the dict, and bound the body size before reading it. Node refuses body
and key buffers backed by shared memory and copies the rest before verifying.
Keep the key in server-side secret storage and never log the input or a failed delivery. A failure
raises BaseRegistryClientError with kind webhook-verification; its code and message name the
input to check without rendering its value. Generation and attempt are decimal strings, so a 64-bit
generation never rounds in JavaScript. A delivery outside the skew bound can still carry a valid
signature, so reject it before deduplication. The idempotency key identifies one delivery generation
and survives automatic retries, but an operator replay creates a new generation and key. Deduplicate
an effect that must happen once on source plus id, or on a business key.
Webhooks covers provisioning the key and building a receiver.
Read records
Section titled “Read records”| Operation | Rust and Python | Node |
|---|---|---|
| Probes | health(), ready() | health(), ready() |
| Parsed registry metadata | registry_contract(access_profile) | registryContract(accessProfile) |
| One Record | get_record | getRecord(route, recordIdentifier, options) |
| First page of a list | list_records | listRecords(route, options) |
| Next page | continue_list(continuation) | continueList(continuation) |
| Lookup by selector | lookup_record | lookupRecord(route, selector, values, options) |
route is the Record type’s API route. Python takes read options as keyword arguments: select,
access_profile, and format (json or json-ld), plus top, filter, orderby, count, and
bbox for a list. Node takes the same options in one object, in camel case.
An outcome is a dict in Python and an object in Node. It carries kind (complete), value, the
trace identifier, and etag and location when the response had them. A record read carries an
ETag; lists and lookups do not. A list outcome adds continuation: pass it unchanged to
continue_list or continueList. It cannot be combined with first-page options.
registry_contract returns BRegMetadata itself, the source of every binding below.
Bindings and writes
Section titled “Bindings and writes”Select a binding from BRegMetadata before a write: selectCreate or selectPatch (snake case in
Python; select_direct_write in Rust), then pass it back to the client. A binding exposes nothing and
grants nothing; it ties the call to the client’s origin, profile, route, and request contract. If the
metadata does not offer the operation, selection fails with kind metadata-selection.
Python: create_record(binding, data, idempotency_key, *, format="json") and
patch_record(binding, record_identifier, etag, operations, idempotency_key, *, format="json").
Node: createRecord(binding, data, idempotencyKey, format) and
patchRecord(binding, recordIdentifier, etag, operations, idempotencyKey, format). data maps API
field names to JSON values. Each patch operation is {"op": "add" | "replace" | "test", "field": ..., "value": ...} or {"op": "remove", "field": ...}. A patch needs the ETag from a fresh read
through the same route and profile. The client checks field names, value bounds, nesting, and body
size before sending; an input it refuses raises kind invalid-request with no exchange.
An idempotency key is 1 to 256 visible ASCII characters, excluding comma and semicolon. The client
never creates one, and resends a write only under the
same-key retry: same binding, body, precondition,
representation, and key. BReg keeps the response for a committed write for
retention.receiptRetentionDays. A resend within that horizon replays it; a resend after it answers
410 idempotency.expired. That 410 is a known outcome: the write committed.
Actions and Proposals
Section titled “Actions and Proposals”selectAction picks an Action, whose mode is fixed: an immediate Invoke or a durable Proposal
Start. Invoke with invokeAction; start durable work with startProposal, then list, read, and move
the Proposal through its transitions with prepareProposal and executeProposal. Each Proposal link
carries its own ifMatch condition, never a Record ETag. Python takes dicts throughout. The
client capabilities reference covers Proposals,
submissions, review reads, attachments, batch, tombstone, and
recovery after a restart.
Exact JSON in Node
Section titled “Exact JSON in Node”Three Node methods take JSON text rather than plain objects: startProposal(binding, inputsJson, key), listProposals(collection, queryJson), and prepareProposal(proposal, operation, bodyJson, key). They return opaque Proposal objects; read documentJson for the validated representation.
Many other methods have a *Json twin that returns the validated response as exact text in
valueJson, for applications that parse JSON numbers losslessly.
BReg error contract
Section titled “BReg error contract”Python raises breg.BaseRegistryClientError, an Exception, with kind, code, refusal_code,
reason_code, field_path, constraint, status, trace_id, transport_kind, token_kind, and
the boolean outcome_unknown; an absent member is None. Node rejects with
breg.BaseRegistryClientError, an Error with kind, the boolean outcomeUnknown, and the
optional code, refusalCode, reasonCode, fieldPath, constraint, status, traceId,
transportKind, and tokenKind.
| Kind | Meaning |
|---|---|
configuration | Construction inputs were refused |
invalid-request | An input was refused before any exchange |
token | The token provider could not supply a bearer |
transport | The exchange failed below HTTP |
not-found | The server answered resource.not_found |
problem | The server answered with any other Problem |
protocol | The response broke the contract |
metadata-selection | No binding matched the metadata |
webhook-verification | A webhook delivery failed authentication |
client | Any other client-side failure |
refusalCode carries the declared reason for action.refused or
statistical_dataset.release_refused, and reasonCode the reason for
statistical_dataset.version_withdrawn. A row-local constraint conflict sets constraint, with
recordType, constraintId, constraintKind, and left and right for compare or field for
int-range and vocabulary. The server omits it when any field involved is unreadable to the
caller, and default error rendering leaves it out.
A Problem code this client does not know arrives as kind problem with only status and the trace
identifier. Decide on the status: an unknown 4xx is a refusal, and an unknown 5xx may have
committed, so follow the same-key retry. idempotency.expired is a known code. The
error tables list which attributes each kind sets and every
code value.
Registry Casework
Section titled “Registry Casework”The Casework client covers pinned review policies, producer requests and results, human review
tasks, and Directory operations. A ReviewResult records the human outcome. You independently
authorize any later business mutation in the producer’s system.
Construct a Casework client
Section titled “Construct a Casework client”use registry_stack_client::casework::{CaseworkClient, CaseworkClientConfig};
let client = CaseworkClient::new(CaseworkClientConfig::new( "https://casework.example.invalid/".parse()?,))?;from registry_client import casework
client = casework.CaseworkClient("https://casework.example.invalid/")const { casework } = require('@registrystack/client');
const client = new casework.CaseworkClient({ baseUrl: 'https://casework.example.invalid/',});CaseworkClientConfig provides request and connection timeouts, a response-size bound, trusted
root certificates, a user agent, and a mutation retry ceiling. Python takes these as constructor
keyword arguments; Node uses config members. Responses are bounded to 4 MiB by default and 16 MiB
at most. Configuration retains no bearer token or selected profile.
Casework authentication and profiles
Section titled “Casework authentication and profiles”For producer, human-review, and Directory calls, pass the request’s bearer token and Casework
profile. Rust uses CaseworkAuth::new(&token, profile); Node and Python take these as the first
two arguments.
Current-source context operations also take a source profile as an optional final argument, or
through Rust’s with_source_profile. Submitted-context reviews need no provider or source profile.
The selected source profile travels in Registry-Source-Profile, never in a decision body.
Producer authority is separate from human review authority. Agent assertion and grant-status calls take only a bearer token and grant ID. The local digest helper takes no credential and performs no I/O.
Submit and recover a review
Section titled “Submit and recover a review”Select the policy by ID in ReviewCreateRequest.policy; Casework freezes its concrete version and
digest at acceptance. To require an exact pin, use review_policy (reviewPolicy in Node) to
discover a compiled closure and supply its complete identity, or use your reviewed project’s
binding. See the Casework API policy contract
for selector validation. Submitted context uses mode: submitted and a bounded snapshot;
current-source context uses mode: current-source and a reference binding.
The subject supplies all five immutable binding fields.
These host-side functions take your complete ReviewCreateRequest, declared trusted-producer ID,
and retained idempotency key. They use the shared Rust canonicalizer, including its portable JSON
validation and Unicode key ordering.
import { casework } from '@registrystack/client';
const client = new casework.CaseworkClient({ baseUrl: 'https://casework.example.invalid/' });
async function submitReview( token: string, producerId: string, request: casework.ReviewCreateRequest, key: string,) { const digest = casework.submissionDigest(producerId, request.subject.source, request); return client.createOrRecoverReviewRequest(token, 'requester', key, request, digest);}def submit_review(token, producer_id, request, key): digest = casework.submission_digest(producer_id, request["subject"]["source"], request) return client.create_or_recover_review_request(token, "requester", key, request, digest)Rust exposes the same local helper as submission_digest(producer_id, source_namespace, &request).
Persist the accepted concrete request binding. Recover a lost response with the original selector,
subject, request body, key, and digest, even after a policy update. Do not rewrite an ID-only
selector into the accepted exact binding during recovery: that changes the submission identity.
Read review_result with the accepted binding or resume the authenticated review_results feed.
Producer create and cancel calls are never automatically resent.
Casework operations by role
Section titled “Casework operations by role”Rust and Python use snake case; Node uses the corresponding camel case.
| Caller | Operations | Boundary |
|---|---|---|
| Producer | review_policies, review_policy, request creation, cancellation, result reads and feed | Exact admitted producer and subject binding |
| Human reviewer | review_tasks, next_review_task, review_task_holdings, task context, claim, release, assignment, delegation, private drafts, decisions, history and notes | Current profile, membership, exclusions and ownership; current source disclosure for lists, open work, context and new notes |
| Deciding caller | own_review_decisions, own single-task receipt | Retained authorship, not prior holding |
| Supervisor | supervisory_review_tasks, review_accountability | Currently served queues; accountability is a separate audited read |
| Administrator | Directory, absence, holiday, caseload and clock preview/apply operations | Administration grants no review or external write authority |
Retained decided-task and terminal-history reads use current Casework authority and result retention. They do not require current source disclosure and never release source context. Own-decisions and supervision lists still require current source disclosure.
ReviewStaffingQuery supports queue, ownership, visible reference, and either all five exact
subject selectors or none. Sort by due, subject, or reference. next_review_task is
due-ordered and returns one task or no task; holdings return a bounded page.
Caseload and clock apply calls send the reviewed preview ID and caller’s key. The runtime
revision-fences the complete preview set; clock recomputation uses pinned policy meaning.
Revisions, actions, and recovery
Section titled “Revisions, actions, and recovery”Task mutations take the task UUID, displayed numeric revision, and caller-chosen idempotency key. The client constructs the route and quoted revision header. Follow the same-key retry contract for ambiguous keyed mutations; never invent a replacement key merely because the response was lost.
Read review_task to recover the caller’s retained decisionReceipt, or use
own_review_decisions after a fresh sign-in. Receipts disclose the deciding caller’s outcome and
pinned label without private reason or structured result data. Supervisory discovery returns
bounded task/accountability references with a holder-free state; it grants no decision or context
authority. It intentionally omits holder identities and due dates and does not provide a team
holdings board. The separate audited accountability read has its own retention.
Caseload preview selects the source officer’s complete claimed set, optionally within one queue,
up to 100 tasks. It has no subset, limit or continuation parameter. A larger set refuses with
review.query-too-broad. Apply is atomic over the exact previewed set; individual task assignments
remain available for a selected subset, with independent revisions and outcomes.
Casework outcomes
Section titled “Casework outcomes”Successful calls return CaseworkComplete<T> in Rust, a Python dict with kind: complete,
value, and trace_id, or a Node object with kind, value, and traceId. Wire DTO fields remain
camel case in both language bindings. Result reads instead distinguish available, pending,
concealed_or_unknown, and expired; only available carries a result.
Pass nextCursor unchanged through the same operation with the same caller, profiles, and query.
Staffing cursors are opaque visibility snapshots valid for at most 15 minutes. Holding,
membership, or current-source visibility changes can invalidate them. After
410 review.result-expired, restart without the cursor. Task list, next-task and holdings reads
scan the complete candidate set before sorting or paging, including unfiltered reads. Exhausted
candidate or source-read budgets return 422 review.query-too-broad, with no partial page;
narrow the queue or exact subject selectors before retrying. A smaller page size or a visible
reference filter does not reduce that scan. See the inbox keys of the runtime configuration.
An unavailable required provider is a failure, not an empty reviewer inbox.
Casework error contract
Section titled “Casework error contract”CaseworkClientError is distinct from every other product’s error. Rust uses Configuration,
InvalidRequest, Transport, Problem, and Protocol. Python raises
casework.CaseworkClientError; Node rejects with the same named error class. The language bindings
expose kind, status, problem code, validated trace context, bounded validation detail, transport
category, protocol failure, and outcome_unknown (Node: outcomeUnknown) when applicable.
Errors do not repeat rejected values or credentials.
Unknown problem codes retain the status and trace context without gaining known recovery meaning.
review.policy-mismatch is a 409 refusal of the expected policy binding. Validation details name
a bounded field path and closed reason, such as policy-not-allowed, schema-mismatch,
outcome-not-declared, result-required, or constraint-violated. A 410 idempotency.expired
requires reconciling the original task, request, or Directory operation before choosing a new key.
See the Casework API contract for exact Problems and headers.
Registry Messaging
Section titled “Registry Messaging”The Messaging client covers six routes: liveness, readiness, the submission of one message, the read of one message’s status, the cancellation of one message before dispatch, and the preview of one template version. It carries no operator route and no provider callback route. The client never reaches the Messaging runtime or its operator tooling; it speaks the public HTTP contract any caller uses.
Construct a Messaging client
Section titled “Construct a Messaging client”use registry_messaging_client::{MessagingClient, MessagingClientConfig};use url::Url;
let client = MessagingClient::new(MessagingClientConfig::new( Url::parse("https://messaging.example.invalid/")?,))?;from registry_client import messaging
client = messaging.MessagingClient("https://messaging.example.invalid/")const { messaging } = require('@registrystack/client');
const client = new messaging.MessagingClient({ baseUrl: 'https://messaging.example.invalid/',});MessagingClientConfig provides with_request_timeout, with_connect_timeout,
with_max_response_bytes, with_user_agent, and with_trusted_root_certificates. The Python
constructor takes the same settings as keyword arguments and the Node constructor as config members.
with_max_mutation_retries, the Python max_mutation_retries keyword, and the Node
maxMutationRetries member set the same-key retry count
for submit.
A response is bounded to 4 MiB by default and to 16 MiB at most. The configuration holds no
credential, and its debug output redacts the base URL, the user agent, and any trusted root
certificates.
Messaging operations
Section titled “Messaging operations”Method names are given in the Rust and Python form. Node uses the camel case form of the same name.
| Method | Takes | Returns |
|---|---|---|
health | Nothing | An empty value while the process serves |
ready | Nothing | An empty value when the runtime can serve, and a service.unavailable problem otherwise |
submit | The bearer token, an idempotency key, and the submission | The MessageReceipt: the message id, its status, and links |
message | The bearer token and a message identifier | The MessageView: status, dispatch, report, the attempt summaries, and links |
cancel | The bearer token and a message identifier | The MessageView as the cancellation left it |
preview | The bearer token, a template identifier, a version, and a TemplatePreviewRequest of locale and data | The TemplatePreview: the template reference, locale, channel, packageDigest, the rendered parts, and sms segment counts for an SMS template |
Every call that reaches a caller route takes the request’s bearer token as its first argument and
retains none. In Rust it is a BearerToken, built with BearerToken::new(token). The module is for
a trusted server host, the way the Casework module is.
A submission requires an idempotency key the caller chose: 1 to 128 visible ASCII characters,
refused before any exchange otherwise. A key is scoped to the caller’s verified issuer and subject.
The client never generates or replaces a key, and resends a submission only under the
same-key retry. After a lost response it still reports,
send the same key with the same submission within retention.receiptRetentionDays, and the runtime
answers the stored receipt instead of accepting a second message. A 403 or 422 answering an exact
retry does not show that the first attempt failed, so never resubmit under a new key: have the
operator restore the access, template version, or locale the refusal names, then retry under the
same key within retention.receiptRetentionDays, as the
replay rule explains. A message
identifier must be the lowercase hyphenated UUID the runtime answered; any other value is refused
before any exchange. The view carries the recipient’s channel with the value redacted, and no
rendered content.
cancel withdraws a message whose dispatch has not started. A cancellation that lost the race
answers message.dispatch-started when dispatch began first, and message.terminal when the
message was already final; the client never retries either. preview renders one template version
for a locale and data and sends nothing. The template identifier must be a lowercase kebab
identifier of at most 64 bytes and the version a package version label of at most 32 bytes, and
both are refused before any exchange otherwise. A version the active package does not ship answers
template.not-found; data its schema refuses, a locale it does not carry, and a render the runtime
stopped answer template.data-invalid, template.locale-unavailable, and
template.render-refused.
receipt = client.submit( token, "reminder-case-42-1", { "senderProfile": "reminders-sms", "to": {"phone": "+15555550100"}, "template": {"id": "appointment-reminder-sms", "version": "1"}, "locale": "en", "data": {"name": "Ada", "day": "2026-10-01", "office": "North"}, },)view = client.message(token, receipt["value"]["id"])cancelled = client.cancel(token, receipt["value"]["id"])preview = client.preview( token, "appointment-reminder-sms", "1", {"locale": "en", "data": {"name": "Ada", "day": "2026-10-01", "office": "North"}},)Messaging outcomes
Section titled “Messaging outcomes”A successful call returns MessagingComplete<T> in Rust, a dict carrying kind (complete),
value, and trace_id in Python, and an object carrying kind, value, and traceId in Node. The
value is the camel case wire DTO the service returned; health and ready carry none.
Messaging error contract
Section titled “Messaging error contract”MessagingClientError is distinct from every other namespace’s error type. Rust answers with
Configuration, InvalidRequest, Transport, Problem, or Protocol. A Problem carries the
status, the typed ProblemCode from the Messaging problem catalogue, and the validated trace
identifier. A Protocol carries a MessagingProtocolFailure of HeaderBounds, TraceContext,
Body, MediaType, Problem, or Status.
The catalogue is closed. A problem becomes a typed Problem only when its code is in the
catalogue, its status is the one the code pins, its type URI is the pinned one, and its trace
identifier matches the response’s trace header. Anything else, including a code the client does
not know, is a Protocol failure with Problem. The title and detail are not compared: the caller
reads the pinned copies through code.title() and code.detail(), so a deployment that rewords
them still answers a refusal the caller can match.
A submission over its access profile’s request rate or daily limit is refused with
rate-limit.exceeded or quota.exceeded and status 429. That Problem also carries
retry_after_seconds, the whole seconds the response’s Retry-After asked the caller to wait. The
client accepts only a single delta-seconds value from 1 through MAXIMUM_RETRY_AFTER_SECONDS
(86,400, one day); an absent, repeated, or out-of-range header leaves the wait unset and the refusal
still typed. The wait is unset on every other status. The client never waits out a 429 or resends
the refused submission.
Python raises messaging.MessagingClientError, an Exception whose attributes are kind, code,
title, detail, status, trace_id, retry_after_seconds, transport_kind,
protocol_failure, and the boolean outcome_unknown; an absent member is None. Node rejects with
messaging.MessagingClientError, an Error with kind, the boolean outcomeUnknown, and the
optional code, title, detail, status, traceId, retryAfterSeconds, transportKind, and
protocolFailure; an absent member is undefined. The
MessagingProblemCode type in both bindings names every code of the catalogue, including
rate-limit.exceeded and quota.exceeded.
| Kind | Meaning | Members set |
|---|---|---|
configuration | The construction inputs were refused | none |
invalid-request | An input was refused before any exchange | none |
transport | The exchange failed below HTTP | transport_kind |
problem | Messaging answered with an accepted problem | status, code, title, detail, and trace_id, plus retry_after_seconds on a 429 that named a wait |
protocol | The response violated the contract | status, protocol_failure, and trace_id when one was validated |
Registry Scheduling
Section titled “Registry Scheduling”The Scheduling client reads the published catalogue and availability, explains availability at one start, places and releases holds, and books, reschedules, cancels, and reads appointments. A hold, a booking, a reschedule, and a cancellation take a caller-chosen idempotency key, which a binding never generates. The Registry Scheduling API holds the authority profiles, task grants, and problem codes each operation assumes.
use registry_scheduling_client::{SchedulingClient, SchedulingClientConfig};use url::Url;
let client = SchedulingClient::new(SchedulingClientConfig::new( Url::parse("https://scheduling.example.invalid/")?,))?;from registry_client import scheduling
client = scheduling.SchedulingClient("https://scheduling.example.invalid/")const { scheduling } = require('@registrystack/client');
const client = new scheduling.SchedulingClient({ baseUrl: 'https://scheduling.example.invalid/',});SchedulingClientConfig provides with_request_timeout, with_connect_timeout,
with_max_response_bytes, with_user_agent, with_trusted_root_certificates, and
with_max_mutation_retries. The Python constructor takes the same settings as keyword arguments and
the Node constructor as config members.
Shared implementation boundary
Section titled “Shared implementation boundary”The canonical Rust clients own product-neutral outbound policy, bearer acquisition, prefix-safe route construction, bounded reads, OAuth response decoding, trace validation, and Problem validation. Product-specific request models and fixed route semantics stay in each product client. Deployment-defined data stays dynamic at the SDK boundary.
Related reference
Section titled “Related reference”- Query a registry from Python and Node for a worked BReg run against the quickstart.
- Request Evidence from your application for a worked Evidence run.
- Base Registry Engine API reference for the BReg HTTP and Problem contracts.
- Registry Casework API for the Casework HTTP, header, and problem contracts.
- Registry Scheduling API for the Scheduling HTTP, authority, idempotency, and problem contracts.
- Errors and status codes for the wider Registry Stack error vocabulary.
- Control access per profile for the profiles and capability bindings a BReg selection matches against.
- Registry Messaging API for the Messaging HTTP, idempotency, limit, and problem contracts.
- Registry Coordinator API for the Coordinator HTTP, run state, and problem contracts.