Skip to content
Registry Stack Docs Latest

Client SDK guide

View as Markdown

Registry Notary ships a typed Rust client plus Python and Node.js wrappers. Use these clients instead of hand-written HTTP calls when application code needs the Notary wire contract, purpose handling, bounded response reads, route-aware retry behavior, JWKS refresh behavior, and redacted error mapping.

This guide starts with the first integration path: discover a claim, evaluate it for one target, read the result, and handle errors safely. Later sections cover batch evaluation, rendering, credential issuance, OID4VCI, federation, and Rust-only verifier helpers.

Before the first SDK call, collect these values from the deployment operator or from your own application workflow.

NeedExampleWhere It Comes From
Service base URLhttps://agriculture-notary.lab.registrystack.orgHosted lab manifest or Registry Notary operator
Auth credentialAGRI_EVIDENCE_CLIENT_BEARERHosted lab manifest or Registry Notary operator
Claim ideligible-for-climate-smart-input-voucherlist_claims or operator docs
Target identifierfarmer_id and FARMER-1001Your application or case record
Purposehttps://demo.example.gov/purpose/nagdi/climate-smart-input-supportYour product, policy, or workflow

Use list_claims first when you are unsure which claim ids your credential can see. A later 403 on evaluation usually means the credential lacks the required_scope for one of the requested claims.

The quickstart uses the public hosted lab backend so the examples are runnable as written. The lab publishes current demo service URLs and caller credentials at https://lab.registrystack.org/api/lab.json. These are public demo credentials, not production secret-handling guidance.

Terminal window
eval "$(registryctl lab env --credential agri-evidence)"

The helper emits only the requested credential from the hosted lab manifest: REGISTRY_NOTARY_BASE_URL, REGISTRY_NOTARY_BEARER_TOKEN, and REGISTRY_NOTARY_PURPOSE. If you are not using registryctl, copy the same values from the lab UI or from the agri-evidence entry in lab.json.

  • Claim: A named question Notary can evaluate, such as eligible-for-climate-smart-input-voucher.
  • Target: The person, organization, or record being evaluated.
  • Requester: The actor asking for the evaluation.
  • Relationship: Why the requester may ask about the target, such as self.
  • Purpose: The reason for processing, sent in request metadata and usually recorded in audit evidence.
  • Disclosure: The detail level requested in the response. predicate asks for a minimized true, false, or unknown style result.
RuntimePackageSurface
Rustregistry-notary-clientPrimary typed client and optional JSON facade
Pythonregistry-notaryDictionary-friendly sync and async wrapper
Node.js@registry-notary/clientPromise client with TypeScript declarations

The Rust crate is the source of truth. Python and Node expose the main application, discovery, OID4VCI, and federation helpers, but keep JSON as dictionaries or plain objects. Rust additionally exposes operational, admin, format, and explicit SD-JWT VC verifier helpers.

The packages are not currently published to public package registries. Pin integrations to a release tag or commit.

[dependencies]
registry-notary-client = { git = "https://github.com/jeremi/registry-notary", tag = "vX.Y.Z" }
registry-notary-core = { git = "https://github.com/jeremi/registry-notary", tag = "vX.Y.Z" }
Terminal window
python -m pip install "git+https://github.com/jeremi/registry-notary.git@vX.Y.Z#subdirectory=bindings/python"
Terminal window
pnpm add "github:jeremi/registry-notary#vX.Y.Z&path:bindings/node"

npm does not support installing from a subdirectory of a git repository. With npm, install the Node package from a checkout pinned to a release tag or commit:

Terminal window
npm install ./bindings/node

From an app that depends on a local Registry Notary checkout, use path installs instead:

[dependencies]
registry-notary-client = { path = "/path/to/registry-notary/crates/registry-notary-client" }
registry-notary-core = { path = "/path/to/registry-notary/crates/registry-notary-core" }
Terminal window
python -m pip install -e /path/to/registry-notary/bindings/python
pnpm add /path/to/registry-notary/bindings/node

This quickstart checks whether the lab farmer FARMER-1001 satisfies the eligible-for-climate-smart-input-voucher claim. Use it when your application already knows the target identifier and wants a minimized claim result from Notary.

The examples set default_purpose or defaultPurpose on the client. You can also set purpose per call when different workflows share the same client.

import os
from registry_notary import RegistryNotaryClient
from registry_notary.errors import NotaryProblemError, NotaryTransportError
client = RegistryNotaryClient(
base_url=os.environ["REGISTRY_NOTARY_BASE_URL"],
bearer_token=os.environ["REGISTRY_NOTARY_BEARER_TOKEN"],
default_purpose=os.environ["REGISTRY_NOTARY_PURPOSE"],
user_agent="benefits-api/1.0",
)
claims = client.list_claims()
print([claim["id"] for claim in claims.get("data", [])])
try:
result = client.evaluate_request({
"target": {
"type": "Farmer",
"identifiers": [{
"scheme": "farmer_id",
"value": "FARMER-1001",
}],
},
"relationship": {"type": "self"},
"claims": ["eligible-for-climate-smart-input-voucher"],
"disclosure": "predicate",
"purpose": os.environ["REGISTRY_NOTARY_PURPOSE"],
})
except NotaryProblemError as exc:
print(exc.status, exc.code, exc.request_id)
except NotaryTransportError:
print("transport failure")
else:
first = result["results"][0]
print(first["claim_id"], first.get("satisfied"))

What this does:

  • list_claims confirms which claim ids this credential can see.
  • target identifies the entity being evaluated.
  • relationship tells Notary why this requester may ask about the target.
  • claims names the claim ids to evaluate.
  • disclosure asks for a minimized claim answer.
  • purpose must match the configured default purpose when both are present.

Python also has a shorter helper for the common one-target case:

result = client.evaluate(
target_id="FARMER-1001",
identifier_scheme="farmer_id",
target_type="Farmer",
claims=["eligible-for-climate-smart-input-voucher"],
)

Use evaluate_request when you need the full canonical wire shape, including relationship context, version-pinned claim references, disclosure, format, or explicit purpose.

import {
NotaryProblemError,
NotaryTransportError,
RegistryNotaryClient,
} from "@registry-notary/client";
const client = new RegistryNotaryClient({
baseUrl: process.env.REGISTRY_NOTARY_BASE_URL,
bearerToken: process.env.REGISTRY_NOTARY_BEARER_TOKEN,
defaultPurpose: process.env.REGISTRY_NOTARY_PURPOSE,
userAgent: "benefits-api/1.0",
});
const claims = await client.listClaims();
console.log(claims.data?.map((claim) => claim.id) ?? []);
try {
const result = await client.evaluate({
target: {
type: "Farmer",
identifiers: [{ scheme: "farmer_id", value: "FARMER-1001" }],
},
relationship: { type: "self" },
claims: ["eligible-for-climate-smart-input-voucher"],
disclosure: "predicate",
purpose: process.env.REGISTRY_NOTARY_PURPOSE,
});
const first = result.results[0];
console.log(first.claimId, first.satisfied);
} catch (error) {
if (error instanceof NotaryProblemError) {
console.log(error.status, error.code, error.requestId);
} else if (error instanceof NotaryTransportError) {
console.log("transport failure");
} else {
throw error;
}
}

The high-level Node helpers accept camelCase object keys and return camelCase response keys. Use evaluateRequest when you want to send canonical snake_case wire JSON and receive snake_case response JSON.

const result = await client.evaluateRequest({
target: {
type: "Farmer",
identifiers: [{ scheme: "farmer_id", value: "FARMER-1001" }],
},
relationship: { type: "self" },
claims: ["eligible-for-climate-smart-input-voucher"],
disclosure: "predicate",
purpose: process.env.REGISTRY_NOTARY_PURPOSE,
});
use registry_notary_client::RegistryNotaryClient;
let base_url = std::env::var("REGISTRY_NOTARY_BASE_URL")?;
let bearer_token = std::env::var("REGISTRY_NOTARY_BEARER_TOKEN")?;
let purpose = std::env::var("REGISTRY_NOTARY_PURPOSE")?;
let client = RegistryNotaryClient::builder(base_url)
.bearer_token(bearer_token)
.default_purpose(purpose.clone())
.user_agent("benefits-api/1.0")
.build()?;
let claims = client.list_claims(Default::default()).await?;
let response = client
.evaluate_target("Farmer")
.target_identifier("farmer_id", "FARMER-1001")
.relationship("self")
.claims(["eligible-for-climate-smart-input-voucher"])
.disclosure("predicate")
.purpose(purpose.clone())
.send()
.await?;
if let Some(result) = response.body.result_for("eligible-for-climate-smart-input-voucher") {
println!("satisfied: {:?}", result.satisfied);
}

Rust returns NotaryResponse<T>, so response metadata is available next to the decoded body:

println!("status: {}", response.status);
println!("request id: {:?}", response.request_id);

The SDKs protect the same wire model. Most integration confusion comes from the request fields, not from the language syntax.

The target is the entity being evaluated. Most application calls include a target with one or more identifiers:

{
"target": {
"type": "Person",
"identifiers": [
{
"scheme": "national_id",
"value": "person-1",
"issuer": "civil_registry"
}
]
}
}

Requester context is optional. Include it when the caller is asking about a different target or acting on behalf of someone else. Use relationship to explain the permission context, such as self, guardian, case_worker, or a deployment-specific value.

For citizen self-attestation flows, omit identity fields and let the server derive the requester, target, and self relationship from the verified token binding:

result = client.evaluate_request({
"claims": [{"id": "person-is-alive", "version": "2026-05"}],
"disclosure": "predicate",
})

Claim references may be plain strings or pinned objects with id and version. The same versioned claim reference shape is supported for single evaluation and batch evaluation.

{
"claims": [
"person-is-alive",
{ "id": "age-over-18", "version": "2026-05" }
]
}

The credential behind your auth mode carries a scopes list, and every claim declares a required_scope on its source bindings. Scope strings are operator-defined <namespace>:<operation> values, for example civil_registry:evidence_verification or registry_notary:credential_issue. There is no fixed global registry of scope names.

When you connect to a deployment you do not operate, ask the operator which scopes the claims you need require. GET /v1/claims, exposed as list_claims or listClaims, confirms which claims your credential can see before the first evaluation.

Evaluation routes can carry a data purpose in the Data-Purpose header and in the request body purpose field. If both are present, they must match exactly. The client rejects mismatches before sending the request.

Set a client default purpose when most calls share one purpose. Override per call through request options when needed.

Use disclosure to request the amount of detail your workflow is allowed to receive. predicate is the common minimized answer for policy gates. Other disclosure modes and formats are deployment-specific and should be selected from operator guidance or claim metadata.

Successful Python and Node helpers return the decoded response body directly. Rust returns NotaryResponse<T> with the decoded body plus metadata.

A single evaluation response is an object with results. Each result represents one claim answer. This example is abbreviated because real responses include provenance and matching metadata.

{
"results": [
{
"evaluation_id": "eval-1",
"claim_id": "person-is-alive",
"claim_version": "2026-05",
"target_ref": {
"type": "Person",
"handle": "target-1",
"identifier_schemes": ["national_id"]
},
"value": null,
"satisfied": true,
"disclosure": "predicate",
"format": "application/vnd.registry-notary.claim-result+json",
"issued_at": "2026-05-29T00:00:00Z",
"expires_at": null,
"provenance": {}
}
]
}

Inspect these fields first:

  • evaluation_id: save this if you plan to render or issue a credential later.
  • claim_id: identifies which requested claim this result belongs to.
  • satisfied: true, false, or absent when the claim does not map to a boolean predicate.
  • value: a minimized value when the requested disclosure and claim format allow it.
  • provenance: evidence metadata. Do not log it blindly.
TaskRustPythonNode.js
See claims available to my credentiallist_claimslist_claimslistClaims
Evaluate one target with helper fieldsevaluate_target(...).send()evaluateevaluate
Send canonical wire JSONevaluate_requestevaluate_requestevaluateRequest
Evaluate many targets safelybatch_evaluate_requestbatch_evaluate_requestbatchEvaluate or batchEvaluateRequest
Render an evaluationrender_requestrender_requestrenderRequest
Issue a credentialissue_credential_requestissue_credential_requestissueCredentialRequest
Check credential statuscredential_statuscredential_statuscredentialStatus
Read issuer JWKSissuer_jwksissuer_jwksissuerJwks
Verify SD-JWT VC materialverifier featurenot exposednot exposed

Use the high-level helpers when your app is doing the common thing. Use the raw or request-shaped helpers when you need exact wire JSON, versioned claim references, route-shaped request bodies, or language binding code.

Configure exactly one auth mode:

  • Bearer token, sent as Authorization: Bearer <token>.
  • API key, sent as X-Api-Key.
  • Rust only: dynamic AuthProvider.

Supplying more than one auth mode is a client construction error. Debug output redacts configured auth material.

let client = RegistryNotaryClient::builder("https://notary.example.gov")
.bearer_token("access-token")
.build()?;
let client = RegistryNotaryClient::builder("https://notary.example.gov")
.api_key("service-key")
.build()?;
client = RegistryNotaryClient(
base_url="https://notary.example.gov",
bearer_token="access-token",
)
const client = new RegistryNotaryClient({
baseUrl: "https://notary.example.gov",
bearerToken: "access-token",
});

Use the canonical request shape when the high-level helper hides fields your workflow cares about. The Python and Node raw helpers preserve canonical snake_case JSON.

result = client.evaluate_request(
{
"target": {
"type": "Person",
"identifiers": [{
"scheme": "national_id",
"value": "person-1",
"issuer": "civil_registry",
}],
},
"relationship": {"type": "self"},
"claims": [{"id": "person-is-alive", "version": "2026-05"}],
"disclosure": "predicate",
"purpose": "benefits_eligibility",
},
request_id="req-123",
traceparent="00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
)
const result = await client.evaluateRequest(
{
target: {
type: "Person",
identifiers: [{ scheme: "national_id", value: "person-1", issuer: "civil_registry" }],
},
relationship: { type: "self" },
claims: [{ id: "person-is-alive", version: "2026-05" }],
disclosure: "predicate",
purpose: "benefits_eligibility",
},
{
requestId: "req-123",
traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
},
);

Rust callers can also construct core DTOs directly. This is most useful inside the Registry Notary workspace or when the application already depends on registry-notary-core.

use registry_notary_client::RequestOptions;
use registry_notary_core::{
ClaimRef, EvidenceEntity, EvidenceIdentifier, EvidenceRelationship, EvaluateRequest,
};
let request = EvaluateRequest {
requester: None,
target: Some(EvidenceEntity {
entity_type: "Person".to_string(),
id: None,
identifiers: vec![EvidenceIdentifier {
scheme: "national_id".to_string(),
value: "person-1".to_string(),
issuer: Some("civil_registry".to_string()),
country: None,
}],
attributes: Default::default(),
assurance: None,
profile: None,
}),
relationship: Some(EvidenceRelationship {
relationship_type: "self".to_string(),
attributes: Default::default(),
}),
on_behalf_of: None,
claims: vec![ClaimRef::new("person-is-alive")],
disclosure: Some("predicate".to_string()),
format: None,
purpose: Some("benefits_eligibility".to_string()),
};
let options = RequestOptions::builder()
.purpose("benefits_eligibility")
.request_id("req-123")
.traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
.build();
let response = client.evaluate_request(request, options).await?;

Use batch evaluation when several targets share the same claim set. Batch evaluation is the only POST route that accepts Idempotency-Key through the client. Retries are allowed for this route only when an idempotency key is supplied.

from registry_notary import RetryPolicy
client = RegistryNotaryClient(
base_url="https://notary.example.gov",
bearer_token="access-token",
retry_policy=RetryPolicy(
max_attempts=3,
base_delay=0.1,
max_delay=2.0,
retry_transport_errors=True,
retry_rate_limited=True,
retry_unavailable=True,
),
)
result = client.batch_evaluate_request(
{
"items": [{
"target": {
"type": "Person",
"identifiers": [{
"scheme": "national_id",
"value": "person-1",
"issuer": "civil_registry",
}],
},
"relationship": {"type": "self"},
}],
"claims": ["person-is-alive"],
"purpose": "benefits_eligibility",
},
idempotency_key="batch-2026-05-29-001",
)
const controller = new AbortController();
const client = new RegistryNotaryClient({
baseUrl: "https://notary.example.gov",
bearerToken: "access-token",
retryPolicy: {
maxAttempts: 3,
baseDelayMs: 100,
maxDelayMs: 2000,
retryTransportErrors: true,
retryRateLimited: true,
retryUnavailable: true,
},
});
const result = await client.batchEvaluate(
{
items: [{
target: {
type: "Person",
identifiers: [{ scheme: "national_id", value: "person-1", issuer: "civil_registry" }],
},
relationship: { type: "self" },
}],
claims: ["person-is-alive"],
purpose: "benefits_eligibility",
},
{
idempotencyKey: "batch-2026-05-29-001",
signal: controller.signal,
},
);
use registry_notary_client::{RequestOptions, RetryPolicy};
use registry_notary_core::{
BatchEvaluateItemRequest, BatchEvaluateRequest, ClaimRef, EvidenceEntity,
EvidenceIdentifier, EvidenceRelationship,
};
use std::time::Duration;
let client = RegistryNotaryClient::builder("https://notary.example.gov")
.retry_policy(RetryPolicy {
max_attempts: 3,
base_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(2),
retry_transport_errors: true,
retry_rate_limited: true,
retry_unavailable: true,
})
.build()?;
let request = BatchEvaluateRequest {
items: vec![BatchEvaluateItemRequest {
requester: None,
target: EvidenceEntity {
entity_type: "Person".to_string(),
id: None,
identifiers: vec![EvidenceIdentifier {
scheme: "national_id".to_string(),
value: "person-1".to_string(),
issuer: Some("civil_registry".to_string()),
country: None,
}],
attributes: Default::default(),
assurance: None,
profile: None,
},
relationship: Some(EvidenceRelationship {
relationship_type: "self".to_string(),
attributes: Default::default(),
}),
on_behalf_of: None,
purpose: None,
}],
claims: vec![ClaimRef::new("person-is-alive")],
disclosure: None,
format: None,
purpose: Some("benefits_eligibility".to_string()),
};
let options = RequestOptions::builder()
.purpose("benefits_eligibility")
.idempotency_key("batch-2026-05-29-001")
.build();
let response = client.batch_evaluate_request(request, options).await?;
println!("succeeded: {}", response.body.summary.succeeded);

Render when you already have an evaluation_id and need a configured evidence format for display, export, or downstream exchange.

Rust accepts the core RenderRequest DTO and moves evaluation_id into /v1/evaluations/{evaluation_id}/render before sending the body. Python and Node raw helpers accept canonical snake_case JSON, require a mapping or object, extract evaluation_id, and send the remaining fields as the route body.

rendered = client.render_request({
"evaluation_id": "eval-1",
"format": "application/vnd.registry-notary.claim-result+json",
"disclosure": "predicate",
})
const rendered = await client.renderRequest({
evaluation_id: "eval-1",
format: "application/vnd.registry-notary.claim-result+json",
disclosure: "predicate",
});
use registry_notary_core::RenderRequest;
let rendered = client
.render_request(
RenderRequest {
evaluation_id: "eval-1".to_string(),
format: "application/vnd.registry-notary.claim-result+json".to_string(),
disclosure: Some("predicate".to_string()),
claims: None,
purpose: Some("benefits_eligibility".to_string()),
},
RequestOptions::default(),
)
.await?;

Issue a credential when a successful evaluation should become a credential artifact, such as an SD-JWT VC. Credential bodies are sensitive. Do not log them, and note that Rust redacts credential bodies from Debug.

credential = client.issue_credential_request({
"evaluation_id": "eval-1",
"credential_profile": "person_is_alive_sd_jwt",
})
const credential = await client.issueCredentialRequest({
evaluation_id: "eval-1",
credential_profile: "person_is_alive_sd_jwt",
});
use registry_notary_core::CredentialIssueRequest;
let credential = client
.issue_credential_request(
CredentialIssueRequest {
evaluation_id: "eval-1".to_string(),
credential_profile: None,
format: None,
claims: None,
disclosure: None,
holder: None,
},
RequestOptions::builder()
.purpose("benefits_eligibility")
.build(),
)
.await?;

Use discovery for claim catalogs, service metadata, and issuer keys. The JWKS helpers use a short in-process cache when called without request options. raw_issuer_jwks or rawIssuerJwks bypasses that cache.

claims = client.list_claims()
claim = client.get_claim("person-is-alive")
service = client.service_document()
jwks = client.issuer_jwks()
client.refresh_jwks()
key = client.get_jwk("key-1")
const claims = await client.listClaims();
const claim = await client.getClaim("person-is-alive");
const service = await client.serviceDocument();
const jwks = await client.issuerJwks();
await client.refreshJwks();
const key = await client.getJwk("key-1");
let service = client.service_document(RequestOptions::default()).await?;
let jwks = client.issuer_jwks(RequestOptions::default()).await?;
// Force refresh after an unknown kid during verification.
let refreshed = client.refresh_jwks(RequestOptions::default()).await?;
status = client.credential_status("credential-1")
const status = await client.credentialStatus("credential-1");
let status = client
.credential_status("credential-1", RequestOptions::default())
.await?;
let updated = client
.update_credential_status("credential-1", "revoked", RequestOptions::default())
.await?;

The client wraps endpoints only. It does not generate holder proofs or manage holder keys.

metadata = client.oid4vci_issuer_metadata()
offer = client.oid4vci_credential_offer("person_is_alive_sd_jwt")
nonce = client.oid4vci_nonce()
credential = client.oid4vci_credential({
"credential_configuration_id": "person_is_alive_sd_jwt",
"proof": {"proof_type": "jwt", "jwt": "eyJ..."},
})
const metadata = await client.oid4vciIssuerMetadata();
const offer = await client.oid4vciCredentialOffer("person_is_alive_sd_jwt");
const nonce = await client.oid4vciNonce();
const credential = await client.oid4vciCredential({
credential_configuration_id: "person_is_alive_sd_jwt",
proof: { proof_type: "jwt", jwt: "eyJ..." },
});

In Rust, enable the oid4vci feature:

registry-notary-client = {
git = "https://github.com/jeremi/registry-notary",
tag = "vX.Y.Z",
features = ["oid4vci"]
}
let metadata = client
.oid4vci_issuer_metadata(RequestOptions::default())
.await?;
let offer = client
.oid4vci_credential_offer(Some("person_is_alive_sd_jwt"), RequestOptions::default())
.await?;

The client submits an already-signed JWT. It does not mint or sign federation requests.

response_jws = client.federation_evaluate_jws("eyJ...")
const responseJws = await client.federationEvaluateJws("eyJ...");

In Rust, enable the federation feature:

registry-notary-client = {
git = "https://github.com/jeremi/registry-notary",
tag = "vX.Y.Z",
features = ["federation"]
}
let compact_response_jws = client
.federation_evaluate_jws("eyJ...", RequestOptions::default())
.await?;

Use the service root URL. A path prefix is allowed and is preserved when routes are joined.

Clients require HTTPS for non-loopback hosts. Rust allows HTTP loopback only in debug or test-support builds. Python and Node local workflows may use http://127.0.0.1, http://localhost, or http://[::1].

Python also exposes an explicit lab/internal escape hatch for Docker Compose and private service-network deployments:

client = RegistryNotaryClient(
base_url="http://registry-notary:8080",
default_purpose="benefits_eligibility",
allow_insecure_internal_http=True,
)

Use allow_insecure_internal_http=True only when transport is already protected by the deployment boundary or a local development network. Production service URLs should remain HTTPS.

Request options support:

  • purpose, mapped to Data-Purpose.
  • request_id or requestId, mapped to X-Request-Id.
  • traceparent, mapped to W3C trace context.
  • idempotency_key or idempotencyKey, mapped to Idempotency-Key only for batch evaluation.
  • accept, for Rust, JSON facade, and selected Python request helpers that need an explicit Accept. Node does not expose a public accept override.

Retries are disabled by default. When enabled, they are route-aware:

  • GET routes may retry transport errors, 429, or 503 according to the policy.
  • POST /v1/batch-evaluations may retry only when an Idempotency-Key is supplied.
  • Evaluation, render, credential issuance, OID4VCI credential, and federation submission are never retried because those POST routes are not deduplicated by the server.

Retry-After seconds are honored. Rust, Python, and Node also handle HTTP-date Retry-After by using the response Date header as the reference clock when it is present.

All typed Rust methods return NotaryResponse<T> with:

  • body: decoded response body.
  • status: HTTP status returned by the server.
  • request_id: server X-Request-Id, when present.
  • retry_after: server Retry-After, when present.

Python and Node expose equivalent fields on errors. Successful Python and Node helpers return the response body directly.

Rust returns NotaryClientError. Python and Node expose:

  • NotaryError
  • NotaryTransportError
  • NotaryProblemError

Safe fields for logs are status, code, title, retryable, and request_id. Do not log raw request bodies, requester or target identifiers, holder proofs, credential bodies, SD-JWT disclosures, nonces, Authorization, X-Api-Key, or Problem Details detail.

Problem detail strings are not exposed by the Python wrapper. The Rust portable() error envelope is intended for language bindings and FFI. It intentionally excludes sensitive detail strings.

The stable application problem code values for policy mapping live in the problem code registry in the API reference.

SymptomLikely CauseCheck
401Missing, expired, or wrong auth credentialToken/API key source and configured auth mode
403Credential lacks a claim’s required_scopelist_claims, operator scope mapping
Client rejects before sendBody purpose and header/default purpose differRequest body and client options
Batch did not retryMissing idempotency key or retry policy disabledidempotency_key/idempotencyKey, retry settings
Render request failsevaluation_id missing or emptyUse an id from a previous evaluation result
Unknown signing keyJWKS cache does not have the kidrefresh_jwks/refreshJwks or Rust verifier refresh path

Enable optional routes only when needed:

registry-notary-client = {
git = "https://github.com/jeremi/registry-notary",
tag = "vX.Y.Z",
features = ["oid4vci", "federation", "json-facade"]
}

In a workspace checkout, the same feature selection can use path dependencies:

registry-notary-client = {
path = "crates/registry-notary-client",
features = ["oid4vci", "federation", "json-facade"]
}

Enable json-facade when building language wrappers. The facade accepts and returns canonical wire JSON with snake_case fields.

use registry_notary_client::facade::NotaryClientHandle;
let handle = NotaryClientHandle::new(client);
let response = handle
.evaluate_json(
serde_json::json!({
"target": {
"type": "Person",
"identifiers": [{
"scheme": "national_id",
"value": "person-1",
"issuer": "civil_registry"
}]
},
"relationship": { "type": "self" },
"claims": ["person-is-alive"],
"purpose": "benefits_eligibility"
}),
serde_json::json!({})
)
.await?;

Enable the Rust verifier feature when relying-party or wallet code needs to verify SD-JWT VC credential material. Verification is explicit and opt-in: transport methods continue to return decoded response bodies without hidden network refreshes or trust-policy decisions.

registry-notary-client = {
git = "https://github.com/jeremi/registry-notary",
tag = "vX.Y.Z",
features = ["verifier"]
}
use registry_notary_client::{HolderBindingPolicy, VerifyOptions};
let options = VerifyOptions::new("did:web:notary.example")
.expected_vct("https://credentials.example/vct/person-is-alive")
.holder_binding(HolderBindingPolicy::Required);
let verified = client
.verify_credential_response(&credential.body, options)
.await?;

The verifier resolves the JWS kid only from trusted issuer JWKS, reuses the client’s short JWKS TTL cache, and forces one refresh on key.unknown. It does not loop indefinitely. VerifyOptions lets callers set expected issuer, accepted algorithms, expected vct, clock skew, and holder-binding policy.

Selective-disclosure presentations may include a subset of disclosures. Each presented disclosure must hash to a digest in the credential. When a presentation includes a key-binding JWT, the verifier separates it from disclosures and verifies its holder proof signature against the credential cnf.jwk. When required holder binding is paired with VerifyOptions::key_binding_challenge, the trailing key-binding JWT is mandatory and must match the expected audience and nonce.

Verifier errors are redacted and safe for policy mapping by code. Stable codes include signature.invalid, key.unknown, algorithm.disallowed, claim.issuer_mismatch, claim.vct_mismatch, claim.time_invalid, disclosure.digest_mismatch, holder_binding.required, holder_binding.kid_mismatch, and holder_binding.proof_invalid.

Python and Node do not expose verifier wrappers in this first phase. Callers in those runtimes should use the Rust verifier through their application boundary or perform verification in wallet-specific code.

Async evaluation helpers are prefixed with a, for example aevaluate and aevaluate_request. The package also exposes async forms for batch evaluation, rendering, and credential issuance:

result = await client.aevaluate_request({
"claims": ["person-is-alive"],
"disclosure": "predicate",
})

Node request helpers accept signal in request options. The high-level evaluate and batchEvaluate helpers also accept signal on the request object for convenience.

const controller = new AbortController();
const result = await client.evaluate({
target: { type: "Person", identifiers: [{ scheme: "national_id", value: "person-1" }] },
claims: ["person-is-alive"],
signal: controller.signal,
});
const client = new RegistryNotaryClient({
baseUrl: "https://notary.example.gov",
retryPolicy: {
maxAttempts: 3,
baseDelayMs: 100,
maxDelayMs: 2000,
retryTransportErrors: true,
retryRateLimited: true,
retryUnavailable: true,
},
});
const result = await client.batchEvaluate(
{
items: [{
target: {
type: "Person",
identifiers: [{ scheme: "national_id", value: "person-1", issuer: "civil_registry" }],
},
relationship: { type: "self" },
}],
claims: ["person-is-alive"],
purpose: "benefits_eligibility",
},
{
idempotencyKey: "batch-2026-05-29-001",
signal: controller.signal,
},
);
const claims = await client.listClaims();
const claim = await client.getClaim("person-is-alive");
const jwks = await client.issuerJwks();
await client.refreshJwks();
const key = await client.getJwk("key-1");
const status = await client.credentialStatus("credential-1");
const metadata = await client.oid4vciIssuerMetadata();
const offer = await client.oid4vciCredentialOffer("person_is_alive_sd_jwt");
const nonce = await client.oid4vciNonce();
const responseJws = await client.federationEvaluateJws("eyJ...");
import { NotaryProblemError, NotaryTransportError } from "@registry-notary/client";
try {
await client.evaluate({
target: { type: "Person", identifiers: [{ scheme: "national_id", value: "person-1" }] },
relationship: { type: "self" },
claims: ["person-is-alive"],
});
} catch (error) {
if (error instanceof NotaryProblemError) {
console.log(error.status, error.code, error.requestId);
} else if (error instanceof NotaryTransportError) {
console.log("transport failure");
}
}

The route to client method mapping for each runtime lives in the route to client method matrix in the API reference.

Terminal window
cargo test -p registry-notary-client
cargo test -p registry-notary-client --features json-facade,oid4vci,federation
cargo doc -p registry-notary-client --no-deps --all-features
python3 -m unittest discover -s bindings/python/tests
npm test --prefix bindings/node
npm run check:types --prefix bindings/node