Preview release.These docs are a work in progress. Pages are still being written, links may break, and structure may shift without notice. Treat everything here as a draft and report issues onGitHub.
This guide describes the V1 HTTP contract from a client and operator point of view. It is the practical reference for calling a running gateway.
The V1 route shape is dataset-scoped and entity-oriented. Storage table ids stay out of public URLs; callers use dataset ids, entity names, record ids, relationships, aggregate ids, and standards adapter roots.
Listeners and surfaces
Section titled “Listeners and surfaces”The data-plane listener is server.bind. It serves health probes, docs, catalog metadata, dataset metadata, entity reads, evidence-offering discovery, aggregates, OpenAPI, and optional standards adapters.
The admin listener is optional and only exists when server.admin_bind is configured. Admin routes must stay on a private network. They are never mounted on the public data-plane listener.
The public URL space is structured as follows:
/v1/datasets/{dataset_id}/entities/{entity}/...and related aggregate, measure, and dimension routes are the entity-oriented data-plane surface./v1/attribute-releasesand/v1/attribute-releases/{profile_id}/versions/{version}/resolve(feature:attribute-release, off by default for 1.0) resolve governed identity attribute-release profiles to minimized claim bundles./metadata/*is the standards-facing metadata surface: catalog, DCAT, SHACL, policies, evidence offerings, and dataset/entity descriptors./.well-known/api-catalogis the public well-known discovery entry point./ogc/v1/*(feature:ogcapi-features) exposes spatial entities as OGC API Features collections./ogc/v1/records/*(feature:ogcapi-records) exposes a metadata-only catalog view./ogc/edr/v1/*(feature:ogcapi-edr) exposes spatial aggregates as OGC EDR area collections./dci/{registry}/registry/sync/*(feature:spdci-api-standards) provides SP DCI standards adapter routes./healthz,/ready,/docs, and/docs/scalar.jsare unauthenticated./openapi.jsonand/docsserve the machine-readable and human-readable API surface respectively.
The curated OpenAPI artifact, including documented request methods, path parameters, and security requirements, is in openapi/registry-relay.openapi.json. Runtime OpenAPI is generated separately from the running data-plane config at /openapi.json and browsable at /docs. Admin-route exposure is verified by the project’s security test suite.
For SP DCI, sync/search is the generic path for any configured
standards.spdci.registries entry. The disability-status, details, and support
paths are Disability Registry-specific and return unknown-resource errors unless
the named {registry} points at the same dataset/entity as
standards.spdci.disability_registry.
Most admin routes are documented only in this guide because they are served on the separate server.admin_bind listener, not the public data-plane. The committed OpenAPI artifact also includes the table-specific ingest reload route because it is part of the documented operator contract.
Admin routes on server.admin_bind:
GET /healthzGET /readyGET /metricsGET /admin/v1/capabilitiesGET /admin/v1/posturePOST /admin/v1/datasets/{dataset_id}/tables/{table_id}/reloadPOST /admin/v1/reloadGET /metrics returns Prometheus-style text/plain metrics for operators. It is intentionally admin-listener only, requires registry_relay:metrics_read, and is not mounted on server.bind.
GET /admin/v1/capabilities returns redacted admin capability metadata for callers with registry_relay:ops_read. Use it before invoking product-specific reload operations.
GET /admin/v1/posture returns a redacted operations posture document for callers with registry_relay:ops_read. Pass ?tier=restricted only to trusted operations users who need the restricted posture projection.
POST /admin/v1/reload reloads every configured source resource and returns a compact status plus counts summary. It requires registry_relay:admin, does not reload startup runtime config, and has a table-specific companion route when you need to reload only one source. Reload-all publishes as one coherent generation: if any source resource cannot prepare, Relay keeps the previous generation active and returns 500 with status: "failed".
Relay no longer exposes admin config verify, dry-run, or apply routes. Signed
config bundles are verified only at boot from the local paths in
config_trust. Operators can use registry-relay config verify-bundle for an
offline local bundle check before deploying the bundle and restarting Relay.
The audit record stores the approval reference, emergency change class, expiry, rate-limit identity, and hashes of approver identity and free-text reason. It does not store raw reason text or raw approver identity.
Authentication
Section titled “Authentication”The gateway runs in one of two auth modes, fixed at startup by auth.mode:
api_key: high-entropy shared secret with a SHA-256 fingerprint loaded from an environment variable.oidc: bearer JWT verified against an external OpenID Connect / OAuth2 IdP’s JWKS.
API key
Section titled “API key”Clients send either header:
Authorization: Bearer <api-key>or:
x-api-key: <api-key>When both are present, Authorization wins. The gateway hashes the presented raw key with SHA-256 and compares it to fingerprints resolved from auth.api_keys[].fingerprint.
OIDC bearer JWT
Section titled “OIDC bearer JWT”Clients send:
Authorization: Bearer <jwt>The OIDC mode does not accept x-api-key. The gateway validates the standard claims (iss, aud, exp, optional nbf) against the configured auth.oidc block, looks up the signing key in the cached JWKS (refreshed on unknown kid), and verifies the signature. The Principal’s principal_id is taken from the token’s sub (preferred), then client_id, then azp. Token verification failures map to granular auth.* codes, including token_expired, audience_mismatch, and kid_unknown, so audit pipelines can distinguish IdP outages from policy denials; see docs/configuration.md for the full table.
Scopes are independent. Grant the narrowest scope that lets the caller do its job:
| Scope suffix | Allows |
|---|---|
metadata | Catalog, dataset summaries, entity schema, and OpenAPI visibility for that dataset |
rows | Entity collection, single-record, and relationship reads |
evidence_verification | Evidence-oriented standards adapter calls and integrations that must stay separate from row reads |
aggregate | Aggregate discovery and configured aggregate execution |
Global admin-listener scopes are independent of dataset scopes: registry_relay:admin for reload and configuration mutation, registry_relay:metrics_read for metrics, and registry_relay:ops_read for read-only posture and capability discovery.
Entity reads
Section titled “Entity reads”Entity routes use configured entity names, not storage table ids. For example:
GET /v1/datasets/social_registry/entities/individual/records?municipality_code=riverbend&limit=50GET /v1/datasets/social_registry/entities/individual/records/ind-123GET /v1/datasets/social_registry/entities/household/records/hh-42/relationships/membersOnly fields exposed through entities[].fields appear in responses. If fields is omitted, every table column is exposed under its declared name. Storage-only columns should therefore be hidden by explicitly listing the public fields.
Filters
Section titled “Filters”Filters are opt-in. A query parameter is accepted only when its field appears in entities[].api.allowed_filters.
Common forms:
?id=ind-123?id.eq=ind-123?id.in=ind-123,ind-456?payment_amount.gte=100?payment_amount.lte=500Operators are configured per field with ops: [eq, in, gte, lte, between]. Arbitrary SQL is never exposed.
Some entities declare required_filters. Protected reads for those entities must carry a principal-bound equality filter for at least one listed field or the gateway returns 400 entity.filter_required; caller-supplied filters can narrow results but do not satisfy the gate. The list is an OR gate, so configure multiple fields only when each field is an acceptable row boundary.
Collection reads accept at most 20 filter parameters. The cap applies before query planning so overly broad or machine-generated requests fail predictably.
Pagination and conditional requests
Section titled “Pagination and conditional requests”Collection routes support limit up to the entity’s configured max_limit. Responses may include an opaque cursor for the next page. Treat cursors as server-owned tokens and pass them back unchanged. A cursor is bound to the query shape and the current snapshot generation; malformed, tampered, or stale cursors fail with query.cursor_invalid.
Entity collection and record responses include validators where supported. Clients can use If-None-Match to avoid re-downloading unchanged content. A matching validator returns 304 Not Modified.
Purpose headers
Section titled “Purpose headers”Entities can require a Data-Purpose header for row reads and OGC feature reads.
Data-Purpose: https://data.example.gov/purposes/service-intake-checkFrozen semantics:
- Header presence can be required per entity via
require_purpose_header: true. A missing header when required returns400 auth.purpose_required. - When the header is present, the value is always recorded verbatim in the audit trail.
- Without an entity
governed_policy, purpose values are not enforced at the consultation layer. Relay records the value but does not validate, compare, or allowlist it. - With an entity
governed_policy, governed evidence-gateway routes evaluate the configured PDP purpose allowlist and return a stablepdp.*denial when the purpose is not permitted. - Registry Notary is the purpose-certification layer.
- Value-level allowlists remain additive opt-in configuration and do not change the default
require_purpose_headerbehavior.
Use stable, reviewable purpose IRIs. Do not put secrets, bearer tokens, or personal data in this header; it is recorded in audit logs.
Governed request context
Section titled “Governed request context”Registry Relay treats client-supplied Policy Decision Point (PDP) context as
untrusted. The supported client-supplied trust-context headers are listed
below. Relay passes each one to the PDP only when the authenticated principal
has the exact registry:trust:<scope_field>:<header_value> scope.
| Header | Scope field | Classification |
|---|---|---|
x-registry-trust-jurisdiction | jurisdiction | Scope-gated |
x-registry-trust-assurance | assurance | Scope-gated |
x-registry-trust-legal-basis | legal_basis | Scope-gated |
x-registry-trust-consent | consent | Scope-gated |
x-registry-subject-ref | subject_ref | Scope-gated |
x-registry-relationship | relationship | Scope-gated |
x-registry-on-behalf-of | on_behalf_of | Scope-gated |
x-registry-credential-format | requested_credential_format | Scope-gated |
x-registry-source-observed-age-seconds | source_observed_age_seconds | Scope-gated |
x-registry-source-observed-at-unix-seconds | source_observed_at_unix_seconds | Scope-gated |
Audit records retain ordinary route scopes unchanged in scopes_used. Each
value-bearing trust scope is replaced by its canonical field-bound form,
registry:trust:<field>:hmac-sha256:<digest>, computed under the deployment
audit key. This preserves pseudonymous authorization evidence without storing
the raw value. pdp_trust_provenance separately records authenticated field
names without their values, including a malformed authenticated freshness
field when parsing denies the request before PDP evaluation.
An absent or nonmatching scope makes the header absent from PDP context. A
policy that requires the field or matches its value then denies the request.
Data-Purpose remains a caller-stated purpose, not proof of identity,
delegation, legal basis, consent, or source freshness.
Policy authors must not make a Permit decision depend on unauthenticated request context. Use only server-derived context or values authenticated by an exact-value trust scope. An adapter that adds another client-supplied trust-context field must apply the same guard before the PDP evaluates it.
Metadata, catalog, and OpenAPI
Section titled “Metadata, catalog, and OpenAPI”GET /metadata/catalog and GET /metadata/dcat/bregdcat-ap return only datasets visible to the authenticated principal’s metadata scopes.
The metadata catalog is route-neutral and does not inline Relay-specific runtime
adapters. Standards routes remain canonical at their protocol roots, such as
/ogc/v1/records, /ogc/v1, and /dci/{registry}/registry/sync/....
/v1/datasets/{dataset_id} acts as the Relay-native discovery surface that
connects them back to the native dataset model.
GET /metadata/* is the canonical standards-facing metadata surface. When the runtime config points at a split metadata manifest, these routes render from the compiled portable manifest and filter the compiled view to the caller’s metadata scopes. They expose catalog JSON, base DCAT, application-profile DCAT, SHACL, dataset/entity metadata, evidence-offering metadata, Draft 2020-12 JSON Schemas, and link-free OGC Records bodies. Visible aggregate distributions advertise native JSON, SDMX JSON 2.1, CSV, and, for configured spatial aggregates when built with ogcapi-edr, the OGC EDR /area endpoint. They do not grant row, evidence verification, aggregate, or admin access.
Relay-native discovery remains under /v1/datasets and runtime entity routes. Portable metadata consumers should use /metadata/* or static publication.
Metadata responses include private validators for the authenticated view. Clients can send If-None-Match; unchanged metadata returns 304 Not Modified. The gateway also sets Cache-Control: private, no-store and Vary: Authorization so shared caches do not reuse one principal’s scoped catalog for another caller.
GET /openapi.json is auth-gated and metadata-filtered by default. The generated document includes only the operations and dataset/entity tags visible to the caller. Local demos and controlled tooling can set server.openapi_requires_auth: false; in that mode, unauthenticated callers receive the full configured OpenAPI surface. GET /docs serves the local Scalar viewer and can load the document with or without a bearer token depending on that setting.
Static publication uses the same portable metadata model without starting Relay. See metadata.md for the manifest model, CLI commands, publication layout, and split metadata startup errors.
When built with ogcapi-features, OGC API Features exposes spatial entities as read-only GeoJSON Features:
GET /ogc/v1GET /ogc/v1/conformanceGET /ogc/v1/collectionsGET /ogc/v1/datasets/{dataset_id}/collectionsGET /ogc/v1/datasets/{dataset_id}/collections/{collection_id}GET /ogc/v1/datasets/{dataset_id}/collections/{collection_id}/itemsGET /ogc/v1/datasets/{dataset_id}/collections/{collection_id}/items/{feature_id}Spatial collections are configured per entity with spatial. An entity is only exposed as an OGC collection when that block is present.
Auth and scope requirements for OGC API Features routes:
| Route | Required scope |
|---|---|
GET /ogc/v1 | metadata scope on at least one spatial entity |
GET /ogc/v1/conformance | metadata scope on at least one spatial entity |
GET /ogc/v1/collections | metadata scope for each listed dataset |
GET /ogc/v1/datasets/{dataset_id}/collections | metadata scope for the dataset |
GET /ogc/v1/datasets/{dataset_id}/collections/{collection_id} | metadata scope for the collection’s dataset |
GET /ogc/v1/datasets/{dataset_id}/collections/{collection_id}/items | read scope for the collection’s entity |
GET /ogc/v1/datasets/{dataset_id}/collections/{collection_id}/items/{feature_id} | read scope for the collection’s entity |
Required filters, purpose headers, and field projection all apply on item routes. bbox alone does not satisfy a non-spatial required_filters constraint; item-by-id reads also enforce required filters to prevent filter-bypass via direct feature links. When multiple required_filters are configured, any one principal-bound equality filter on a listed field satisfies the gate. Item links in FeatureCollections preserve active required filters so they remain valid when followed.
OGC item routes return application/geo+json; landing, conformance, and collection routes return application/json. Errors return application/problem+json with a stable code extension member.
Error codes for OGC and spatial failures:
| Code | Condition |
|---|---|
ogc.collection_not_found | Dataset or collection not registered, not spatially exposed, or not visible |
ogc.feature_not_found | Feature not registered, not visible, or outside required filter context |
ogc.record_not_found | OGC Records item not registered or not visible |
spatial.geometry_invalid | Geometry field is malformed at runtime |
spatial.geometry_too_large | Geometry exceeds the configured vertex limit |
spatial.bbox_invalid | bbox parameter is malformed, uses an unsupported shape, or crosses the antimeridian |
spatial.filter_unsupported | A named parameter cannot be evaluated for this collection (carries parameter field) |
spatial.crs_unsupported | Requested bbox-crs is not CRS84 |
query.cursor_invalid | OGC after cursor is malformed, expired, or bound to a different query context |
Pagination on item routes uses limit (capped by the entity’s max_limit) and the opaque signed after cursor. The cursor binds dataset id, collection id, normalized filters, bbox, bbox-crs, datetime, limit, and caller identity; any change invalidates it. numberMatched is omitted; numberReturned and links (self, first, next) are always present on FeatureCollections.
Spatial query parameter behavior: bbox is parsed as minx,miny,maxx,maxy in CRS84 longitude-latitude order; six-value 3D bbox values are rejected. bbox-crs accepts http://www.opengis.net/def/crs/OGC/1.3/CRS84 only. datetime requires a configured datetime_field; open-open intervals (../..) are rejected. Broad bbox queries that exceed max_bbox_degrees are rejected before data access.
Conformance boundaries for the current OGC Features surface: it claims http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core, http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson, and http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30. It supports point fields and existing GeoJSON geometry fields; WKT and WKB are not yet supported. Antimeridian bboxes, reprojection, and formal Queryables are not yet supported. For operator rollout details and configuration of the spatial entity block, see standards-adapter-operator-guide.md.
When built with ogcapi-records, OGC API Records exposes a metadata-only catalog view:
GET /ogc/v1/recordsGET /ogc/v1/records/conformanceGET /ogc/v1/records/collectionsGET /ogc/v1/records/collections/datasetsGET /ogc/v1/records/collections/datasets/itemsGET /ogc/v1/records/collections/datasets/items/{dataset_id}The initial Records surface has one collection, datasets, where each item is a record describing a visible dcat:Dataset. It uses the same metadata scopes as /metadata/catalog; it does not expose row data, bypass required filters, or claim searchable-catalog conformance.
GET /ogc/v1/records/collections/datasets/items supports metadata-side discovery parameters:
q: case-insensitive text search across visible dataset and entity metadata.limit: maximum records to return, from 1 to 1000.after: opaque signed pagination cursor from arel=nextlink.
Dataset Records do not currently support bbox or datetime filtering because the catalog metadata only carries spatial_coverage IRIs and no temporal coverage field. The endpoint rejects unsupported spatial/search parameters instead of pretending those fields are filterable.
Evidence verification and aggregates
Section titled “Evidence verification and aggregates”Evidence offerings are declared metadata resources that describe what kind of evidence a registry can check and the authority, assurance, purpose, and request shape for that check:
GET /metadata/evidence-offeringsGET /metadata/evidence-offerings/individual_name_evidenceMetadata reads require the caller’s metadata scope for the owning dataset. They do not execute a check or disclose row data. Relay publishes discovery records only; Registry Notary executes verification. Evidence offerings carry access.kind: registry-notary metadata with the advertised Notary endpoint or discovery URL; clients submit claims and evidence to Registry Notary, not Relay. For the complete client handoff sequence, see client-integration.md.
The list endpoint accepts metadata-only filters:
GET /metadata/evidence-offerings?evidence_type=BirthCertificateGET /metadata/evidence-offerings?country=THGET /metadata/evidence-offerings?procedure_context=https://example.gov/procedure/benefit-intakeAggregates are predeclared in config. Clients can list available aggregates and execute one by id:
GET /v1/datasets/social_registry/aggregatesGET /v1/datasets/social_registry/measuresGET /v1/datasets/social_registry/dimensionsGET /v1/datasets/social_registry/aggregates/by_municipalityGET /v1/datasets/social_registry/aggregates/by_municipality/structurePOST /v1/datasets/social_registry/aggregates/by_municipality/queryMeasure and dimension discovery is dataset-scoped and generated from aggregate declarations. Reused measure or dimension ids are merged into one discovery record with queryable_via, valid_dimensions for measures, and links back to the aggregate routes. GET /v1/datasets/{dataset_id}/aggregates/{aggregate_id}/metadata remains a deprecated compatibility alias for /structure while the runtime still mounts it.
Aggregate JSON results use observations for rows and structure for dimensions/measures.
JSON
- Query bodies should use
measures;indicatorsis accepted only as a deprecated compatibility alias. - Disclosure control is configured per aggregate.
- Suppressed or masked groups are normal results, not errors.
- Temporal query bounds are supported for aggregates that declare a
temporal_field; requests with temporal bounds against aggregates without one are rejected instead of guessing. - POST queries may set
max_rows; truncated results are marked withcompleteness.complete: falseandcompleteness.truncated: trueso a partial cube is not confused with a complete one.
CSV
- Available with
?f=csv, request"format": "csv", orAccept: text/csvand carriesX-Registry-Relay-*andX-SPDCI-*disclosure/freshness headers plus aLink: rel="describedby"header to aggregate structure.
SDMX JSON 2.1
- Available with
?f=sdmx-json, request"format": "sdmx-json", orAccept: application/vnd.sdmx.data+json;version=2.1; messages declare the official schema athttps://json.sdmx.org/2.1/sdmx-json-data-schema.json.
OGC EDR
- When built with
ogcapi-edr, configuredadmin_areaspatial aggregates are also exposed as OGC EDR/areacollections under/ogc/edr/v1.
Attribute release
Section titled “Attribute release”registry-relay can resolve a governed identity attribute-release profile: an
exactly-one-subject lookup that maps configured source fields (or CEL
expressions) into a minimized, OIDC/UserInfo-style claim bundle for a named
profile. This beta surface is compiled only when Relay is built with the
off-by-default attribute-release feature.
GET /v1/attribute-releasesPOST /v1/attribute-releases/{profile_id}/versions/{version}/resolveGET /v1/attribute-releases lists the release profiles visible to the
authenticated caller: a profile appears only when the caller holds that
profile’s release_scope. The response never includes source internals
(table ids, source field names); it is private, no-store and
Vary: Authorization.
POST /v1/attribute-releases/{profile_id}/versions/{version}/resolve resolves
one subject against the named (profile_id, version) pair, which is globally
unique and has no “latest” alias. Each profile declares its own
release_scope, a dataset-bound scope that must differ from the entity’s
read_scope; the scope suffix convention for this capability is
<dataset_id>:identity_release, one of the scope levels an API key can be
granted alongside metadata, aggregate, rows, verify, and
evidence_verification. A profile may also declare a purpose: when it does,
the request’s Data-Purpose header must equal it, or the gateway returns
400 auth.purpose_required (header absent) or 403 auth.purpose_denied
(header present but mismatched), before any source read. A profile that omits
purpose carries no such gate.
Request body:
{ "subject": { "id_type": "NATIONAL_ID", "value": "NID-1" }, "claims": ["given_name"]}claims is optional: absent resolves the profile’s full default claim set; an
explicit empty list is rejected with 400 filter.invalid_value; a name
outside the profile’s configured claims is denied. subject.value accepts
only a non-blank scalar (string, number, or boolean); subject.id_type must
match the profile’s configured type when one is set. Either failure returns
400 release.subject_invalid.
Successful response (200):
{ "profile_id": "civil_identity", "profile_version": "v1", "claims": { "given_name": "Ada", "full_name": "Ada Lovelace" }}claims carries only the released, minimized claim bundle: never the raw
registry row, never a raw or hashed subject value. A source block
(dataset, entity, subject_id_type, cardinality, checked_at) is added
only when the profile sets response.include_source_metadata: true; it is
absent by default.
Every denial after profile resolution collapses to one public code, so a caller cannot distinguish “no such subject” from “subject exists but was denied”:
| Condition | Code | Status |
|---|---|---|
Unknown (profile_id, version) | release.profile_not_found | 404 |
| Invalid subject id_type or value | release.subject_invalid | 400 |
| No matching row, more than one matching row, a false release-condition predicate, a required claim unavailable, or a requested claim name outside the profile’s configured set | release.subject_denied | 403 |
| Backing source unavailable | release.source_unavailable | 503 |
A successful release is cacheable only when the profile sets
response.max_age_seconds, which yields private, max-age=N; the default is
private, no-store. Every response carries Vary: Authorization. Denials are
always private, no-store, regardless of the profile’s caching setting.
Problem details
Section titled “Problem details”Errors use RFC 9457 Problem Details with a stable code field:
{ "type": "https://id.registrystack.org/problems/registry-relay/auth/scope_denied", "title": "Scope denied", "status": 403, "code": "auth.scope_denied", "detail": "required scope: social_registry:rows"}The exact text in detail is operator-facing but intentionally scrubbed. Do not depend on it programmatically. Use the HTTP status and code.
Startup-only split metadata failures use stable metadata.manifest.* and runtime.binding.* codes logged to stderr; see metadata.md for the full table.
Credential issuance
Section titled “Credential issuance”Relay does not issue response credentials, host DID documents, or publish credential-support schemas and contexts. Use Registry Notary for credential issuance and verification workflows. See provenance.md for migration notes.