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

# Relay client API reference

> Construction, authentication, methods, outcomes, continuations, caching, raw documents, and errors for the Rust, Python, and Node Relay V2 clients.

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.

## Availability and compatibility

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.

## Install a client

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:

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

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

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

## Construct a client

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.

### Rust

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

### Python

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

### Node

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

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.

| Mode | Rust | Python | Node |
| --- | --- | --- | --- |
| No bearer | Omit `with_token_provider` | Omit `authorization` | Omit `authorization` |
| Static bearer | `Arc::new(StaticToken::new(token)?)` passed to `with_token_provider` | `authorization=token` | `authorization: { static: token }` |
| Private-key JWT | `PrivateKeyJwt` passed as a `TokenProvider` | `authorization={"private_key_jwt": config}` | `authorization: { privateKeyJwt: config }` |
| Custom provider | Implement the public `TokenProvider` trait | Not exposed | Not 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.

## Operation matrix

| Operation | Rust | Python | Node |
| --- | --- | --- | --- |
| Process probes | `health`, `ready` | `health`, `ready` | `health`, `ready` |
| Discovery documents | `openapi`, `service_metadata` | `openapi`, `service_metadata` | `openapi`, `serviceMetadata` |
| Resource discovery | `resources`, `continue_resources`, `resource` | `resources`, `continue_resources`, `resource` | `resources`, `continueResources`, `resource` |
| Record list and search | `list_records`, `search_records`, `continue_collection` | `list_records`, `search`, `continue_list_records`, `continue_search` | `listRecords`, `search`, `continueListRecords`, `continueSearch` |
| Record read | `read_record` | `read_record` | `readRecord` |
| Governed lookup | `lookup_record` | `lookup` | `lookup` |
| Generated artifact | `artifact` | `artifact` | `artifact` |
| SDMX documents | `sdmx_data`, `sdmx_structure` | `sdmx_data`, `sdmx_structure` | `sdmxData`, `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.

## Request arguments

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.

| Operation | Required arguments | Optional arguments | Rust request type |
| --- | --- | --- | --- |
| OpenAPI, service metadata | None | ETag | `Option<&StrongEtag>` |
| Resource list | None | Page size from 1 through 100; ETag | `ResourceListRequest` |
| Resource detail | Resource identifier | ETag | Resource identifier as `&str` |
| Resource continuation | Complete resource continuation | ETag | `ResourceContinuation` |
| Record list | Resource identifier | Page size, record options, filters, ETag | `ListRequest` |
| Record search | Resource identifier, search identifier, bbox | Page size, record options, ETag | `SearchRequest` |
| Record or search continuation | Complete matching continuation | ETag | `CollectionContinuation` |
| Record read | Resource and record identifiers | Record options; ETag | `RecordOptions` |
| Governed lookup | Resource and lookup identifiers; selectors | Record options; ETag | `LookupRequest` |
| Artifact | Artifact identifier | ETag | Identifier as `&str` |
| SDMX data | Agency, resource, three-part version | Key, constraints, offset, limit, dimension at observation, format, ETag | `SdmxDataRequest` |
| SDMX structure | Kind, agency, resource, three-part version | ETag | `SdmxStructureRequest` |

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

## Response outcomes

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.

## Continuations

Resource discovery returns this complete continuation projection:

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

A record-list continuation has this shape:

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

## Conditional cache contract

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.

## Raw documents

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

| Language | Complete raw response |
| --- | --- |
| Rust | `RawDocument` with `media_type()` and `as_bytes()` |
| Python | `body: bytes` and `media_type: str` |
| Node | `body: 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.

## Error contract

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.

| Meaning | Python | Node |
| --- | --- | --- |
| Closed failure category | `kind` | `kind` |
| Registered Problem code | `code` | `code` |
| Public HTTP status | `status` | `status` |
| Validated trace identifier | `trace_id` | `traceId` |
| Bounded `429` delay | `retry_after_seconds` | `retryAfterSeconds` |
| Transport subcategory | `transport_kind` | `transportKind` |
| Token subcategory | `token_kind` | `tokenKind` |

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.

## Shared implementation boundary

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.

## Related reference

- [Relayctl command reference](../relayctl/) for project authoring and package commands.
- [Instance API references](../apis/) for the deployment-generated Relay OpenAPI boundary.
- [Errors and status codes](../errors/) for the wider Registry Stack error vocabulary.