Skip to content
Registry StackDocsv0.20.0

Relay client API reference

View as Markdown

The Registry Relay clients cover the fixed Relay V2 HTTP surface without importing a deployment’s record, selector, filter, artifact, or SDMX schemas. Rust is the canonical implementation. The Python and Node packages are thin bindings that return native plain values and keep HTTP, route, authentication, Problem, trace, and response validation in Rust.

Registry Stack is pre-1.0 Beta software. Keep a client on the same Registry Stack release as the Relay deployment unless a release note states a wider compatibility range.

Registry Stack v0.20.0 is the first release with prebuilt Relay clients. The release workflow attaches three Python wheels and three Node tarballs to the matching GitHub Release. It does not publish them to PyPI or npm. The Rust crate has publish = false, remains source-only in the Registry Stack workspace, and is not published to crates.io.

Python requires version 3.10 or later and uses abi3-py310. Node requires version 22.12 or later. The release assets cover Linux amd64, Linux arm64, and macOS arm64.

Download Python and Node packages from the GitHub Release that matches the Relay deployment. The Python asset is named registry_relay_client-<version>-cp310-abi3-<wheel-platform>.whl. Install the local file:

Terminal window
python -m pip install ./registry_relay_client-<version>-cp310-abi3-<wheel-platform>.whl

Use linux_x86_64, linux_aarch64, or macosx_11_0_arm64 for <wheel-platform>. The Node asset is named relay-client-node-v<version>-<platform>.tgz. Install it into an application:

Terminal window
npm install ./relay-client-node-v<version>-<platform>.tgz

Node package platforms are linux-amd64-glibc, linux-arm64-glibc, and macos-arm64. Rust applications take the source crate from the matching release tag because it is not a registry package:

[dependencies]
registry-relay-client = { git = "https://github.com/registrystack/registry-stack", tag = "v<version>" }

All three clients accept a prefix-bearing base URL. HTTPS is required except for loopback HTTP. The URL cannot contain credentials, a query, a fragment, or ambiguous empty path segments.

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)?;

RelayClientConfig also provides with_token_provider, with_request_timeout, with_connect_timeout, with_max_response_bytes, with_user_agent, and with_trusted_root_certificates. Rust methods are asynchronous.

from registry_relay_client import RelayClient
client = RelayClient(
base_url="https://relay.example.invalid/institution-a",
request_timeout_seconds=30,
connect_timeout_seconds=10,
max_response_bytes=8 * 1024 * 1024,
)

Python methods are synchronous. They release the global interpreter lock while the private Tokio runtime waits for network I/O. trusted_root_certificates accepts a PEM bundle as bytes.

const { RelayClient } = require('@registrystack/relay-client');
const client = new RelayClient({
baseUrl: 'https://relay.example.invalid/institution-a',
requestTimeoutMilliseconds: 30_000,
connectTimeoutMilliseconds: 10_000,
maxResponseBytes: 8 * 1024 * 1024,
});

Node methods return promises. trustedRootCertificates accepts a PEM bundle as a string.

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.

ModeRustPythonNode
No bearerOmit with_token_providerOmit authorizationOmit authorization
Static bearerArc::new(StaticToken::new(token)?) passed to with_token_providerauthorization=tokenauthorization: { static: token }
Private-key JWTPrivateKeyJwt passed as a TokenProviderauthorization={"private_key_jwt": config}authorization: { privateKeyJwt: config }
Custom providerImplement the public TokenProvider traitNot exposedNot exposed

The built-in private-key-JWT configuration requires a token endpoint, client identifier, and private signing JWK. It also accepts audience, assertion lifetime, refresh margin, token request and connection timeouts, user agent, and a PEM certificate bundle for the token endpoint. Relay and token-endpoint certificate bundles are independent.

Python wraps the configuration as {"private_key_jwt": {...}} and 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: {...} }. Its required members are tokenEndpoint, clientId, and clientKey; optional members are audience, assertionLifetimeSeconds, refreshMarginSeconds, requestTimeoutMilliseconds, connectTimeoutMilliseconds, userAgent, and string-valued trustedRootCertificates. Rust constructs PrivateKeyJwtConfig, builds PrivateKeyJwt, and passes it through with_token_provider.

The built-in token exchange sends only grant_type, client_assertion_type, and client_assertion. It does not send scope, an RFC 8707 resource, a body client_id, or deployment-defined form members. Use a pre-acquired short-lived static bearer when an issuer requires those members. Rust callers can implement a custom TokenProvider instead.

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

Every method performs at most one Relay 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 a Relay exchange, fetches referenced schemas, or advances pagination automatically.

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. Node also accepts geo-json-rfc7946 as an alias for geojson. 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, dataflow, datastructure, or data-structure in 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 optional etag, or kind: "not_modified", etag, and trace_id.
  • Node returns an object with kind: 'complete', value, traceId, and optional etag, or kind: 'notModified', etag, and traceId.

Deployment-defined records and collections stay dynamic. Python returns nested mappings, lists, and scalars. Node returns plain JSON values. Rust exposes fixed envelopes around a dynamic JSON record body. Fixed service metadata, resource metadata, page, trace, and ETag envelopes remain typed.

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. 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, wrong-route handoff, 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 throws 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/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 canonical Rust client owns product-neutral outbound policy, bearer acquisition, prefix-safe route construction, bounded reads, OAuth response decoding, trace validation, and Problem validation. Relay-specific request models and fixed route semantics stay in the Relay client. Deployment-defined data stays dynamic at the SDK boundary.

The Python and Node bindings convert native values, construct the Rust client, run one SDK method, and map the validated result or error. They do not implement Python or JavaScript HTTP, route, authentication, Problem, redirect, retry, cache, or pagination policy.