Skip to content
Registry StackDocsv0.34.0

Registry Stack client API reference

View as Markdown

One package carries every maintained Registry Stack client API: @registrystack/client on npm and registry-stack-client on PyPI. It holds five namespaces, one per product: discovery for Registry Discovery, evidence for Evidence Gateway, relay for Registry Relay, breg for Base Registry Engine (BReg), and casework for Registry Casework. Each namespace keeps its own routes, authentication, errors, and verification rules, because those contracts differ. Relay and BReg share only the neutral registry-record types they decode Registry Record responses into.

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.

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. Python requires version 3.10 or later and the wheels use the Python 3.10 stable ABI (abi3-py310), so a later Python imports the same file. Node requires version 22.12 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.

Read the version from the deployment’s binary rather than typing it, then install that exact version:

Terminal window
python -m pip install "registry-stack-client==<version>"
Terminal window
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:

Terminal window
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 five product clients under breg, casework, discovery, evidence, and relay 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>" }
from registry_client import breg, casework, discovery, evidence, relay
const { breg, casework, discovery, evidence, relay } = require('@registrystack/client');
use registry_stack_client::{breg, casework, discovery, evidence, relay};

Import only the namespaces an application uses. Nothing crosses a namespace: a Relay error is never a BReg error, and a Discovery selection is inert metadata that grants no access to either.

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 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 bytes in Python; plain objects, arrays, and Buffer in Node. No wrapper type is needed to read a result.
  • Timeouts are seconds in Python and milliseconds in Node. trusted_root_certificates accepts a PEM bundle as bytes in Python; trustedRootCertificates accepts it as a string in Node, except in the Discovery namespace, where it is a Buffer.

A method that sends an already-prepared request performs at most one service exchange. 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, retries an exchange, fetches referenced schemas, or advances pagination on its own.

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.

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.

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.

OperationRustPythonNode
Resolve a requirement to Evidence Typesresolve_evidence_typesresolve_evidence_typesresolveEvidenceTypes
Search the catalogsearch_servicessearch_servicessearchServices
Search Evidence servicessearch_evidence_servicessearch_evidence_servicessearchEvidenceServices
Search Relay servicessearch_relay_servicessearch_relay_servicessearchRelayServices

Only those four operations reach the network. Every selection below is local and offline.

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.

SelectionRustPython and Node
One record by identifier and capabilityresponse.select_exact(request)select_exact and selectExact
One Evidence Gateway serviceresponse.select_evidence(request)select_evidence_service and selectEvidenceService
One Relay serviceresponse.select_relay(request)select_relay_service and selectRelayService
One Evidence Type mapping alternativeresponse.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_structure and validateSelectionStructure check closed shape and capability binding, and nothing else. validate_selection and validateSelection are deprecated aliases kept for compatibility; the operation is structural, not trust.
  • accept_selection and acceptSelection apply an adopter-owned local policy callback and return an AcceptedServiceSelection carrying endpoint_url or endpointUrl. A callback that returns false raises kind local_acceptance_refused.
  • renew_unchanged_selection and renewUnchangedSelection return a freshly reselected service only when its trust-relevant semantics are unchanged, and raise kind selection_changed otherwise. 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.

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.

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.

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

SourcePythonNode
A fixed bearer, sent unchangedthe 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 Relay and BReg namespaces, which name 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, assertion_lifetime_seconds, refresh_margin_seconds, request_timeout_seconds, connect_timeout_seconds, and user_agent. Node takes the same six 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 only grant_type, client_assertion_type, and client_assertion. It sends no scope, no RFC 8707 resource, no body client_id, and no deployment-defined form member. Use a pre-acquired short-lived static bearer when an issuer requires those members.

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.

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:

StepRust and PythonNodeReturns
Close the expectations and generate a nonceprepare(spec)prepare(spec)PreparedEvidenceRequest
Send the prepared requestsend(prepared)send(prepared)RawEvidenceResponse
Verify the response against the requestverify(prepared, response)verify(prepared, response)VerifiedEvidence
Send and verify in one callrequest_and_verify(prepared)requestAndVerify(prepared)VerifiedEvidence
Verify at a stated instantverify_as_of(prepared, response, at)verifyAsOf(prepared, response, atMillis)VerifiedEvidence
Read the published definitionsdiscover()discover()The definitions document
Read the published key setfetch_jwks()fetchJwks()A JWKS document
Refresh cached deployment metadatarefresh_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: [...] }.

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:

responseFormatNode typePython typePayload member
signed-jwsProgressiveVerifiedAssertionVerifiedAssertionassertion, the signed bytes; credential is null
sd-jwt-vcProgressiveVerifiedAudienceScopedCredentialVerifiedAudienceScopedCredentialcredential, 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.

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.

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.

KindPython classMeaning
configurationConfigurationErrorThe construction inputs or the request specification were refused, or a prepared request had already spent its single send
nonceNonceErrorThe request nonce could not be generated
tokenTokenErrorThe configured credential source could not supply a bearer
transportTransportErrorThe exchange failed below HTTP
deniedDeniedErrorEvidence refused the request
not_availableNotAvailableErrorThe source could not answer the request
protocolProtocolErrorThe response violated the contract
verificationVerificationErrorThe 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.

The Relay clients cover the fixed Relay V2 HTTP surface without importing a deployment’s record, selector, filter, artifact, or SDMX schemas.

use registry_relay_client::{RelayClient, RelayClientConfig};
use url::Url;
let config = RelayClientConfig::new(
Url::parse("https://relay.example.invalid/institution-a")?,
);
let client = RelayClient::new(config)?;
from registry_client import relay
client = relay.RelayClient(
base_url="https://relay.example.invalid/institution-a",
request_timeout_seconds=30,
connect_timeout_seconds=10,
max_response_bytes=8 * 1024 * 1024,
)
const { relay } = require('@registrystack/client');
const client = new relay.RelayClient({
baseUrl: 'https://relay.example.invalid/institution-a',
requestTimeoutMilliseconds: 30_000,
connectTimeoutMilliseconds: 10_000,
maxResponseBytes: 8 * 1024 * 1024,
});

RelayClientConfig also provides with_token_provider, with_request_timeout, with_connect_timeout, with_max_response_bytes, with_user_agent, and with_trusted_root_certificates.

Authentication is optional. Probes and OpenAPI never ask a configured token provider for a token and never send authorization. Other methods attach a bearer when a provider is configured and Relay accepts credentials for that operation.

The supported modes are:

  • No bearer: omit with_token_provider in Rust or authorization in Python and Node.
  • Static bearer: pass Arc::new(StaticToken::new(token)?) to with_token_provider in Rust, authorization={"static": token} in Python, or authorization: { static: token } in Node.
  • Private-key JWT: pass PrivateKeyJwt as a TokenProvider in Rust, authorization={"private_key_jwt": config} in Python, or authorization: { privateKeyJwt: config } in Node.
  • Custom provider: implement the public TokenProvider trait in Rust. Python and Node do not expose custom token providers.

The private-key-JWT configuration names its required members token_endpoint, client_id, and client_key. Its optional members are audience, assertion_lifetime_seconds, refresh_margin_seconds, request_timeout_seconds, connect_timeout_seconds, user_agent, and byte-valued trusted_root_certificates. Node wraps the configuration as { privateKeyJwt: {...} }, names its required members tokenEndpoint, clientId, and clientKey, spells the timeouts requestTimeoutMilliseconds and connectTimeoutMilliseconds, and takes string-valued trustedRootCertificates. Rust constructs PrivateKeyJwtConfig, builds PrivateKeyJwt, and passes it through with_token_provider. Relay and token-endpoint certificate bundles are independent.

The built-in token exchange sends only grant_type, client_assertion_type, and client_assertion, exactly as the Evidence one does.

OperationRustPythonNode
Process probeshealth, readyhealth, readyhealth, ready
Discovery documentsopenapi, service_metadataopenapi, service_metadataopenapi, serviceMetadata
Resource discoveryresources, continue_resources, resourceresources, continue_resources, resourceresources, continueResources, resource
Record list and searchlist_records, search_records, continue_collectionlist_records, search, continue_list_records, continue_searchlistRecords, search, continueListRecords, continueSearch
Record readread_recordread_recordreadRecord
Governed lookuplookup_recordlookuplookup
Generated artifactartifactartifactartifact
SDMX documentssdmx_data, sdmx_structuresdmx_data, sdmx_structuresdmxData, sdmxStructure

The table uses the wire-level fact name. Python spells multiword keyword arguments in snake case, such as page_size, access_profile, and dimension_at_observation. Node places optional request facts in an options object and uses camel case, such as pageSize, accessProfile, and dimensionAtObservation. Rust uses the request types named in the final column.

health and ready take no arguments.

OperationRequired argumentsOptional argumentsRust request type
OpenAPI, service metadataNoneETagOption<&StrongEtag>
Resource listNonePage size from 1 through 100; ETagResourceListRequest
Resource detailResource identifierETagResource identifier as &str
Resource continuationComplete resource continuationETagResourceContinuation
Record listResource identifierPage size, record options, filters, ETagListRequest
Record searchResource identifier, search identifier, bboxPage size, record options, ETagSearchRequest
Record or search continuationComplete matching continuationETagCollectionContinuation
Record readResource and record identifiersRecord options; ETagRecordOptions
Governed lookupResource and lookup identifiers; selectorsRecord options; ETagLookupRequest
ArtifactArtifact identifierETagIdentifier as &str
SDMX dataAgency, resource, three-part versionKey, constraints, offset, limit, dimension at observation, format, ETagSdmxDataRequest
SDMX structureKind, agency, resource, three-part versionETagSdmxStructureRequest

RecordOptions contains fields, access profile, and record format. ListRequest adds a positive page size and a string-to-string filter mapping. SearchRequest::new requires a BoundingBox ordered as [west, south, east, north] and can add a positive page size. Python passes list filters to list_records and requires the bbox keyword on search. Node uses ListOptions for listRecords and SearchOptions, whose bbox member is required, for search.

Input record formats are json, json-ld, geojson, and json-fg in Python and Node. Rust uses RecordFormat::Json, JsonLd, GeoJsonRfc7946, or JsonFg.

A lookup selector mapping must contain at least one named string, signed integer, or boolean value. Python supplies it as selectors and Node as the third lookup argument. Rust builds it with one or more LookupRequest::selector calls.

SDMX data format is json or csv. The optional constraints are a string-to-string component mapping. Python passes all SDMX data facts as arguments to sdmx_data; Node passes one SdmxDataRequest object; Rust constructs SdmxDataRequest::new(agency, resource, version) and uses its builders. SDMX structure kind is dataflow or datastructure in Python and Node, and SdmxStructureKind::Dataflow or DataStructure in Rust. Every SDMX version has the exact x.y.z form.

Every conditional Rust method takes an optional final &StrongEtag. Python exposes etag as the last optional keyword. Node exposes it as the final optional argument after any options or request object. Continuation methods accept only the matching complete continuation and optional ETag.

Process probes return complete responses only. Cacheable operations return one of two outcomes:

  • Rust returns Conditional::Complete(Complete<T>) or Conditional::NotModified(NotModified).
  • Python returns a mapping with kind: "complete", value, trace_id, and nullable etag, or kind: "not_modified", etag, and trace_id.
  • Node returns an object with kind: 'complete', value, traceId, and an etag that can be omitted, or kind: 'notModified', etag, and traceId.

Deployment-defined domainData stays dynamic. Python returns nested mappings, lists, and scalars, and Node returns plain JSON values for that field. Both binding declarations type the fixed service, capability, resource, Record envelope, page, trace, and ETag structures that surround it. Rust owns the matching fixed models around the dynamic JSON field.

Resource discovery returns this complete continuation projection:

{
"cursor": "<opaque-cursor>"
}

A record-list continuation has this shape:

{
"route": {
"kind": "records",
"resource": "<resource>"
},
"cursor": "<opaque-cursor>",
"format": "json",
"accessProfile": "<access-profile>"
}

A search continuation changes route to include kind: "search" and its exact search identifier. accessProfile is absent when the first request did not select one. format is one of json, json-ld, geojson-rfc7946, or json-fg.

Pass the complete returned projection unchanged to the matching continuation method. Python and Node reject a continuation handed to the wrong route-specific method. Rust exposes one typed continue_collection method and follows the route carried by that continuation. A continuation binds the opaque cursor to its resource or search route, representation format, and optional access profile. It intentionally does not carry first-page fields, filters, bounding box, or page size. The clients reject raw cursor strings, extra members, and attempts to combine a cursor with first-page choices.

A complete cacheable response can carry a validated strong ETag. It has exactly one quoted, lowercase SHA-256 value. Pass it through the method’s ETag argument to send If-None-Match.

A valid 304 Not Modified response has an empty body, echoes the requested ETag, and carries the same strictly validated trace context as a complete response. Reuse a stored body only when it was stored with that exact ETag. The clients do not retain bodies or issue conditional requests on their own.

OpenAPI, generated artifacts, SDMX data, and SDMX structures remain raw protocol documents:

LanguageComplete raw response
RustRawDocument with media_type() and as_bytes()
Pythonbody: bytes and media_type: str
Nodebody: Buffer and mediaType: string

The client validates exact OpenAPI and SDMX media types before returning bytes. An artifact can declare any single syntactically valid media type, which the client preserves. Every raw method enforces its body bound. The client does not interpret deployment-defined artifacts or SDMX payloads. SDMX versions use the exact x.y.z form.

Rust returns RelayClientError variants for configuration, invalid request, token, transport, Relay Problem, and protocol failures. Python raises relay.RelayClientError with snake-case attributes. Node throws the same named class with camel-case attributes.

MeaningPythonNode
Closed failure categorykindkind
Registered Problem codecodecode
Public HTTP statusstatusstatus
Validated trace identifiertrace_idtraceId
Bounded 429 delayretry_after_secondsretryAfterSeconds
Transport subcategorytransport_kindtransportKind
Token subcategorytoken_kindtokenKind

Optional error attributes are absent or None when the failure does not carry that fact. A Relay Problem is accepted only when its exact six-member document, registered code and status, response media type, and header and body trace agree. Only a registered 429 Problem can expose a numeric Retry-After from 1 through 60 seconds.

Errors retain fixed local reasons, public status and Problem codes, validated trace identifiers, and bounded retry guidance. They do not expose credentials, JWKs, selectors, filters, response bodies, header values, URLs, or underlying HTTP error chains.

The BReg client owns BReg routes, query types, Problems, entity tags, capability bindings, and lifecycle actions. It is a separate client and a separate transport from Relay; the two share only the neutral registry-record DTOs for strict Registry Record response decoding.

use registry_breg_client::{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,
});

BaseRegistryClientConfig provides with_token_provider, with_request_timeout, with_connect_timeout, with_max_response_bytes, with_user_agent, and with_trusted_root_certificates. Health and readiness never acquire or send a bearer. Other operations attach one token when a provider is configured.

authorization is optional; a client without it reaches only the probes and whatever the deployment serves anonymously. It takes the same two shapes and the same member names as the Relay authorization above, and any other shape is refused at construction with kind configuration.

Write and lifecycle authority stays opaque. BRegCreateBinding, BRegPatchBinding, and BRegLifecycleAuthority are selected from BRegMetadata, passed back to the client, and expose nothing. A promoted BRegLifecycleAction exposes read-only operation, stage, href, body, and review, and only the client can execute it.

The Node breg.verifyWebhookDelivery() helper authenticates one exact Base Registry Engine Version 1 webhook request with the shared Rust verifier. Pass the received method, request target, headers, body bytes, and key without normalizing their values. Header names are matched case-insensitively. 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.

This plain Node receiver core checks those boundaries in order. eventStore.applyOnce() is an application-owned durable transaction that claims the event identity and commits the effect once; applyRegistryEvent() validates and applies the receiver’s expected payload contract.

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();
}

Keep the key in server-side secret storage and never log the input or a failed delivery. Body and key buffers with shared backing stores are refused; accepted buffers are copied before entering the native verifier so another worker cannot change the bytes being authenticated. The helper throws BaseRegistryClientError with kind webhook_verification and code missing_header, malformed_signature, signature_mismatch, or unsupported_version. Generation and attempt are authenticated decimal strings, which avoids rounding a signed 64-bit generation at the JavaScript number boundary. A delivery outside the receiver’s skew bound can still have a valid signature, so reject it before the deduplication transaction and effect. idempotencyKey identifies one delivery generation and is stable across automatic retries. An operator replay creates a new generation and key, so an effect that must happen only once is deduplicated on authenticated event identity, source plus id, or on an application business key.

The BReg capability matrix also covers native bbox and GeoJSON, temporal and snapshot collections, retained revisions, proposal history, relationship traversal, immediate actions, atomic batches, tombstone, and persisted recovery across all three languages.

OperationRust and PythonNodeRust returns
Liveness and readiness probeshealth(), ready()health(), ready()BRegComplete<BRegProbeStatus>
Caller-filtered documentsopenapi(access_profile), registry_metadata(access_profile)openapi(accessProfile), registryMetadata(accessProfile)BRegComplete<BRegRawDocument>
Parsed registry metadataregistry_contract(access_profile)registryContract(accessProfile)BRegComplete<BRegMetadata>
Entity schemaentity_schema(entity_identifier, access_profile)entitySchema(entityIdentifier, accessProfile)BRegComplete<BRegRawDocument>
Record readget_record(entity_route, record_identifier, options)getRecord(entityRoute, recordIdentifier, options)BRegComplete<RegistryRecordSingleResponse>
Record listlist_records(entity_route, request)listRecords(entityRoute, options)BRegComplete<BRegPage<RegistryRecordCollectionResponse>>
List continuationcontinue_list(continuation)continueList(continuation)BRegComplete<BRegPage<RegistryRecordCollectionResponse>>
Lookuplookup_record(entity_route, request)lookupRecord(entityRoute, selector, values, options)BRegComplete<RegistryRecordSingleResponse>

BRegResponseMetadata::trace_id() is always present. A record read carries a BRegEtag; probes, discovery documents, lists, and lookups do not. A direct patch requires an ETag from a fresh read through the same route and access profile.

BRegContinuation binds an opaque skip token to the entity route, representation, and optional access profile. Its validated projection contains no $select, $filter, $orderby, $top, or $count setters, so callers cannot combine a continuation with first-page options.

In Python, get_record, list_records, and lookup_record take their options as keyword arguments: select, access_profile, and format (json or json-ld), plus top, filter, orderby, and count and bbox for a direct list. lookup_record(entity_route, selector, values=None, ...) names the selector and its values directly. Node takes the same options as an object with select, accessProfile, format, top, filter, orderby, count, and bbox for a direct list.

A Python outcome is a dict and a Node outcome is an object. Both carry kind (complete), value, the trace identifier (trace_id in Python, traceId in Node), and etag and location when the response carried them. A member the response did not carry is None in Python and absent in Node. A raw document carries body (bytes or Buffer) and media_type or mediaType in place of value. A list outcome adds continuation, a dict or object holding the route, skip token, representation, optional access profile, and the registry, dataset, and entity type identifiers; pass it unchanged to continue_list or continueList.

registry_contract and registryContract return BRegMetadata itself rather than an outcome. It exposes registry_identifier, registry_version, registry_revision, trace_id, and etag (camel case in Node) beside the selection methods below.

registry_contract parses caller-filtered Registry Metadata into BRegMetadata and binds the metadata to the client’s exact origin. select_direct_write returns BRegDirectWrite::Create or BRegDirectWrite::Patch only when the operation, route, request contract, capabilities, registry, dataset, entity, profile, revision, and origin match the supported shape. select_lifecycle returns one BRegLifecycleAuthority for the matching entity and profile.

A mismatch returns BRegMetadataSelectionError. The error carries a closed reason kind and no response-controlled values.

The bindings split the direct write selection by operation. select_create(operation_identifier, expected_profile) returns a BRegCreateBinding, select_patch(operation_identifier, expected_profile) returns a BRegPatchBinding, and select_lifecycle(entity_identifier, expected_profile) returns a BRegLifecycleAuthority (Node: selectCreate, selectPatch, selectLifecycle). A mismatch raises the client error with kind metadata_selection and a code from not_found, unbound_source, profile_mismatch, unsupported_operation, required_capability, and contract_mismatch.

BRegCreateRequest::new(data) and BRegPatchRequest::builder() validate API field names, I-JSON-safe values, nesting depth, operation count, and encoded body size before HTTP I/O. A patch operation serializes under /data/<field>.

BRegIdempotencyKey::parse(value) accepts 1 to 256 visible ASCII bytes except comma and semicolon. The client never creates a key or retries a mutation. A retry after an unknown outcome must reuse the same binding, body, precondition, representation, and key.

create_record sends the selected create operation and requires a 201 response whose Location matches the created record. patch_record sends the selected patch operation with If-Match and requires a 200 response without Location.

The bindings take the same inputs as plain values. 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 I-JSON values. Each patch operation is {"op": "add" | "replace" | "test", "field": ..., "value": ...} or {"op": "remove", "field": ...}. Node checks every input before conversion: plain objects and arrays only, at most 128 levels, 100,000 nodes, and 4 MiB of string data. Python converts dicts, lists, tuples, and JSON scalars. An input either binding cannot convert, and any value the Rust validation refuses, raises kind invalid_request before an exchange.

Change-request records carry a BReg-owned request extension. lifecycle_actions promotes only the actor-bound actions from a fresh record against a matching BRegLifecycleAuthority. execute_lifecycle_action sends the promoted href, body, ETag, and caller-supplied idempotency key.

BRegLifecycleOperation contains SubmitRequest, ApproveRequest, RejectRequest, RequestRevision, ReviseRequest, CancelRequest, and ApplyRequest. Refetch the record after a success or refusal before choosing the next operation. Lifecycle ETags are not record ETags.

Python lifecycle_actions(authority, record, *, format="json") returns a sequence of BRegLifecycleAction; Node lifecycleActions(authority, record, format) returns an array and performs no exchange. record is the value of a fresh record read. An action’s operation is the snake case operation name, such as submit_request; stage and review are None or null when absent. A promotion whose authority does not conform, or whose action is not bound to that authority and record, raises kind lifecycle_promotion with code authority or binding. execute_lifecycle_action(action, idempotency_key) and executeLifecycleAction(action, idempotencyKey) return the receipt as an outcome.

A change-request entity can declare governed attachment slots. Slot content is engine-owned: a create body, a patch operation, and a request effect never set it, and only the served attachment routes move the bytes. select_attachments(entity_identifier, expected_profile) returns every slot the caller-filtered metadata advertises for that entity and profile, bound to the client’s origin (Node: selectAttachments). A mismatch raises kind metadata_selection with a code from the same closed list as the other selections.

A BRegAttachmentSlot exposes slot_identifier, entity_identifier, access_profile, required_for_submit, maximum_bytes, content_types, classification, can_download, can_upload, can_remove, and accepts_content_type(content_type), camel case in Node. classification is the authored sensitivity of the slot content, public, internal, or restricted. The served routes stay opaque, so only the client can spend a slot.

prepare_upload(content_type, body) and prepareUpload(contentType, body) check the bytes against that served policy before any I/O and return an opaque BRegAttachmentUpload carrying content_type and byte_size. They raise kind invalid_request when the slot advertises no upload route, when the body is empty, when the body exceeds maximum_bytes, when the content type is not a lowercase type/subtype, and when that media type is outside content_types. A parameter, a wildcard, and an uppercase spelling all refuse, since a served type is matched exactly.

OperationRust and PythonNodeRust returns
Slot selectionselect_attachments(entity_identifier, expected_profile)selectAttachments(entityIdentifier, expectedProfile)Vec<BRegAttachmentSlot>
Slot state in a recordvalue_in(record)valueIn(record, format)BRegAttachmentSlotValue
Uploadupload_attachment(slot, record_identifier, etag, upload, idempotency_key)uploadAttachment(slot, recordIdentifier, etag, upload, idempotencyKey, format)BRegComplete<RegistryRecordSingleResponse>
Downloaddownload_attachment(slot, record_identifier, proposal_version)downloadAttachment(slot, recordIdentifier, proposalVersion)BRegComplete<BRegRawDocument>
Deletedelete_attachment(slot, record_identifier, etag, idempotency_key)deleteAttachment(slot, recordIdentifier, etag, idempotencyKey, format)BRegComplete<RegistryRecordSingleResponse>

An upload and a delete take an ETag from a fresh read through the same route and access profile and a caller-supplied idempotency key, on the same terms as patch_record, and answer with the updated record. The record format comes last: a keyword argument in Python and an optional argument in Node. uploadAttachmentJson and deleteAttachmentJson return those receipts as exact JSON text, like the other Node exact-JSON methods. A download names a positive proposal version, answers with a raw document, and reads its body under the client’s maximum response bytes, 8 MiB by default, so raise that bound before fetching a slot that serves more.

value_in and valueIn read one slot’s state out of the domainData of a fresh record read rather than from the wire. The result carries kind, one of not_selected, empty, and filled, and a value present only when filled. A filled value carries slot_identifier, proposal_version, erased, byte_size, sha256, content_type, uploaded_at, uploaded_by, and verification_status, camel case in Node. verification_status is notRequired, pending, approved, or rejected when the record carries one. A record whose slot value does not match the served shape raises kind invalid_request.

The Node methods whose names end in Json keep domain values on the Rust side of the JavaScript number boundary. getRecordJson, listRecordsJson, continueListJson, lookupRecordJson, createRecordJson, patchRecordJson, executeLifecycleActionJson, uploadAttachmentJson, and deleteAttachmentJson return a JsonOutcome: the validated response as JSON text in valueJson, with the same trace, ETag, location, and continuation semantics as their object counterparts. lifecycleActionsJson takes a complete record envelope as JSON text, and every action it promotes carries bodyJson and reviewJson beside body and review. The Python binding has no such methods.

An input is the same domain data object or field-based patch array the object method takes, encoded as JSON text, not an arbitrary HTTP body. The client refuses duplicate members, invalid JSON, bounds violations, and numeric literals whose value would change during decoding, among them 0.10000000000000001, 1e9999, and 9007199254740993.0. BReg’s numeric model still governs a write: 9007199254740992 is a supported write, and 9007199254740993 is refused with kind invalid_request before any exchange. Do not round a refused input to make it pass.

const result = await client.createRecordJson(
binding,
'{"quantity":9007199254740992,"amount":"12.3400","date":"2026-09-08"}',
'caller-chosen-idempotency-key',
);

A response passes the same decoder and reaches the caller unchanged: a 9007199254740993 that arrives in a response is returned intact in valueJson, and a response literal that cannot round-trip fails with kind protocol. Fixed-scale decimals stay JSON strings, and a null member stays distinct from an absent one. JSON whitespace and equivalent number spellings may be canonicalized, because the promise covers values and types. Decode valueJson with a lossless JSON library to display a number outside the JavaScript safe integer range.

The client never retries a mutation on its own. After an unknown outcome, the caller decides whether to send the same action again with the same idempotency key.

BaseRegistryClientError is distinct from RelayClientError. Its public accessors expose only the fixed error kind, status, registered BReg Problem code, validated trace identifier, bounded transport or token category, and the bounded refusal code a declared immediate-action refusal names.

A Problem is accepted only when its closed members, code, status, title, detail, type URI, and header and body trace match. Unknown Problem codes and malformed responses fail closed as BRegProtocolFailure. Request, metadata, ETag, idempotency-key, and lifecycle validation errors do not retain response-controlled values.

Python raises breg.BaseRegistryClientError, an Exception whose message is str(error) and whose attributes are kind, code, plan_refusal, refusal_code, status, trace_id, transport_kind, and token_kind; an absent member is None. Node rejects with breg.BaseRegistryClientError, an Error with kind and the optional code, planRefusal, refusalCode, status, traceId, transportKind, and tokenKind; an absent member is undefined.

kind is one of:

KindMeaningMembers set
configurationThe construction inputs were refusednone
invalid_requestAn input was refused before any exchangenone
tokenThe private-key JWT provider could not supply a bearertoken_kind, plus transport_kind, code, or status depending on how the token endpoint failed
transportThe exchange failed below HTTPtransport_kind
problemThe registry answered with an accepted Problemstatus, code, trace_id, plan_refusal when the code is a request-plan refusal, and refusal_code when the code is an immediate-action refusal
protocolThe response violated the contractstatus, code (header_bounds, trace_context, media_type, body, problem, entity_tag, profile_link, location, cache_policy, status, or protocol), and trace_id when one was validated
metadata_selectionA binding could not be selected from the metadatacode
lifecycle_promotionThe authority did not conform, or an action was not bound to it and the recordcode (authority or binding)
clientAny other client-side failurenone

The Casework client owns source work-item, unified review, and directory routes and types, its closed problem catalogue, and its attempt recovery contract. It carries no Base Registry Engine routes or protocol types, even where the deployment it calls reads source-backed work from a registry.

use registry_casework_client::{CaseworkClient, CaseworkClientConfig};
use url::Url;
let client = CaseworkClient::new(CaseworkClientConfig::new(
Url::parse("https://casework.example.invalid/")?,
))?;
from registry_client import casework
client = casework.CaseworkClient("https://casework.example.invalid/")
page = client.review_tasks(token, "staff", {"limit": 25})
const { casework } = require('@registrystack/client');
const client = new casework.CaseworkClient({
baseUrl: 'https://casework.example.invalid/',
});
const page = await client.listWorkItems(token, 'staff', 'reviewer', { view: 'my_teams', limit: 25 });

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

In Python and Node, every method takes the request’s bearer token and the selected Casework profile as its first two arguments, and a source-scoped method takes the selected source profile as its third. Unified review task reads take a source profile only when the pinned context strategy is source; submitted-context tasks need none. In Rust the same three values travel together as CaseworkAuth, built with CaseworkAuth::new(token, profile) and extended with with_source_profile.

The client sends those values as Authorization, Registry-Casework-Profile, and Registry-Source-Profile on that one call, and retains none of them. The module is for a trusted server host: the host holds the session, chooses the profile for the request, and passes both in. A browser sends the host’s session cookie and never a Casework token.

Method names are given in the Rust and Python form. Node uses the camel case form of the same name.

  • Service and review descriptions. description, review_kinds, and review_kind return the source-neutral configuration the selected profile may read.
  • Producer. create_or_recover_review_request, review_request, review_result, review_results, and cancel_review_request cover requests inside the producer’s exact binding.
  • Human reviewer. review_tasks, review_task, review_task_context, claim_review_task, assign_review_task, delegate_review_task, release_review_task, review-task draft methods, decide_review_task, review_history, and review-note methods cover unified tasks.
  • Staff on source-backed work. list_work_items, next_work_item, get_work_item, claim_work_item, release_work_item, get_draft, save_draft, delete_draft, decide_work_item, work_item_history, and work_item_clocks, each under the selected source profile, with recover_decision and recover_decision_by_key for a lost decision response.
  • Supervisor. holdings, assign_work_item, delegate_work_item, preview_caseload_move, apply_caseload_move, and review_accountability for a retained protected record.
  • Administrator. directory, directory_targets, bootstrap_directory, update_directory_team, create_absence, update_absence, delete_absence, holiday_revision, create_holiday_revision, preview_clock_recompute, and apply_clock_recompute.

Absence reads are scoped rather than reserved to one role: Staff read their own absences, Supervisors read absences for staff they currently supervise, and Administrators read all of them. In Rust, absences reads with the default query and absences_page takes one; the Python and Node absences takes the query as an optional argument.

A claim, release, or decision takes the caller-filtered CaseworkAction the item carried, which holds the exact route and the if_match revision to send, so the caller never assembles that route. Every other revisioned mutation takes the revision the caller displayed and a caller-chosen idempotency key, both as explicit arguments. No client retries a mutation, and none replaces a key.

After a lost response, recover_decision_by_key replays the original idempotency key, so recovery does not depend on the attempt identifier reaching the caller. recover_decision names the attempt identifier instead, for a caller that did receive one, including through the Registry-Casework-Attempt header on a refusal.

A successful call returns CaseworkComplete<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.

A paged read returns items, a status of complete, budget_exhausted, or source_unavailable, and nextCursor when another page exists. An empty page and an exhausted budget are both successful answers; pass the cursor back through the same method to continue.

CaseworkClientError is distinct from every other namespace’s error type. Rust answers with Configuration, InvalidRequest, Transport, Problem, or Protocol. A Problem carries the status, a CaseworkProblemCode, the fixed detail, the validated trace identifier, the original attempt identifier when the refusal named one, and the review validation detail when the service refused a submitted field. A Protocol carries a CaseworkProtocolFailure of HeaderBounds, TraceContext, MediaType, Body, Problem, or Status.

CaseworkProblemCode registers the closed catalogue the service publishes and answers expected_status for each code. A code outside the catalogue arrives as Unknown rather than a parse failure, so an older client keeps the status and trace context of a refusal it does not recognize.

Python raises casework.CaseworkClientError, an Exception whose attributes are kind, code, detail, status, trace_id, original_attempt_id, validation, transport_kind, and protocol_failure; an absent member is None. Node rejects with casework.CaseworkClientError, an Error with kind and the optional code, detail, status, traceId, originalAttemptId, validation, transportKind, and protocolFailure; an absent member is undefined.

kind is one of:

KindMeaningMembers set
configurationThe construction inputs were refusednone
invalid_requestAn input was refused before any exchangenone
transportThe exchange failed below HTTPtransport_kind
problemCasework answered with an accepted problemstatus, code, detail, trace_id, plus original_attempt_id on a recoverable attempt and validation on a refused review submission
protocolThe response violated the contractstatus, protocol_failure, and trace_id when one was validated

A validation member carries the bounded path of the refused field and one reason from a closed set: kind_not_allowed, reference_invalid, object_required, maximum_bytes_exceeded, maximum_depth_exceeded, schema_mismatch, outcome_not_declared, reason_required, text_invalid, result_not_declared, result_required, field_not_declared, constraint_invalid, and constraint_violated. Neither member repeats a submitted value.

The five result reasons govern a review request’s structured result. result_not_declared refuses a result or a result constraint when the review kind declares no resultSchema. result_required refuses a decision for an outcome whose resultRequired is set when the body carries none. field_not_declared refuses a result constraint that names a property the kind’s schema does not declare. constraint_invalid refuses a constraint that is not a narrowing of the schema: an unknown keyword, a bound outside the declared one or on a property whose type the schema does not declare inline, or a choice value the schema refuses. constraint_violated refuses a submitted result that passes the schema but falls outside the item’s constraints. schema_mismatch covers a value outside the schema itself, and both reasons carry the instance path of the offending field.

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.