Skip to content
Registry Stack Docs Latest

Verify a claim with Registry Notary

View as Markdown

Use this tutorial after you publish the spreadsheet registry API. You will add Registry Notary to the same local project, start Relay and Notary together, prove anonymous claim access is denied, and evaluate one claim backed by the registry API. A final section, Run Notary standalone for an API you operate, shows the same flow with Notary in its own project pointed at a source API you choose.

This tutorial uses synthetic data and local demo credentials. Do not use the generated local keys in production. Estimated time: about 10 minutes after the first tutorial passes.

Complete Publish a spreadsheet as a secured registry API first:

Terminal window
registryctl init relay my-first-api --sample benefits
cd my-first-api
registryctl start
registryctl smoke

The Relay smoke test must pass before you add Notary.

If you already completed that tutorial and my-first-api is still on disk, do not rerun the init line: registryctl init refuses to overwrite a directory that is not empty. From my-first-api, run registryctl start and registryctl smoke to confirm the project is healthy, then continue.

From the Relay project directory, add Notary:

Terminal window
registryctl add notary --from local-relay

The command updates the local project:

my-first-api/
registryctl.yaml
compose.yaml
README.md
relay/
config.yaml
metadata.yaml
notary/
config.yaml
data/
benefits_casework.xlsx
bruno/
registry-api/
secrets/
local.env
output/
.gitignore

registryctl keeps Relay and Notary credentials in secrets/local.env. The Relay and Notary runtime configs contain only fingerprint references, commitments, and environment variable names. This local demo uses generated API keys only; it does not require OIDC, eSignet, or an assisted-access service.

The generated Compose file also starts a local Redis replay store for Notary readiness. It is part of the local demo runtime and does not require manual configuration.

Notary reads from Relay through the Compose network:

http://registry-relay:8080

registry-relay is the service name registryctl writes into the generated compose.yaml; it is fixed by the generator, not derived from your project directory name.

Local browser and curl examples use the host URL:

http://127.0.0.1:4255

Start the project again:

Terminal window
registryctl start

The command starts both services and, after the Docker Compose progress lines, waits for both health and readiness checks:

Relay API: http://127.0.0.1:4242
API docs: http://127.0.0.1:4242/docs
Notary API: http://127.0.0.1:4255
API docs: http://127.0.0.1:4255/docs

Check the project status:

Terminal window
registryctl status

The Relay and Notary services report healthy and ready when startup is complete.

Run the Notary smoke checks:

Terminal window
registryctl notary smoke

The smoke test passes with these checks:

PASS notary healthz is public
PASS notary ready is public
PASS anonymous claims request is denied
PASS anonymous caller can open Notary API docs
PASS anonymous caller can fetch Notary OpenAPI
PASS notary evaluator can list claims
PASS notary evaluator can verify benefits person exists

registryctl writes detailed results to:

output/notary-smoke-results.json

The smoke result must not contain raw API keys, the Relay source token, local env values, Relay source rows, or sensitive sample column values.

Load the generated local keys into your shell:

Terminal window
set -a
. secrets/local.env
set +a

The Notary tutorial adds these local values:

ValueEnvironment variableWhat it is for
Notary evaluator keyREGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAWLets you call Notary evaluation routes
Notary evaluator fingerprintREGISTRY_NOTARY_TUTORIAL_EVALUATOR_HASHLets Notary verify the local evaluator key
Notary audit hash secretREGISTRY_NOTARY_AUDIT_HASH_SECRETLets Notary hash audit subjects without logging raw values
Relay source token for NotaryEVIDENCE_SOURCE_REGISTRY_RELAY_TOKENLets Notary read the Relay source API
Notary demo issuer JWKREGISTRY_NOTARY_ISSUER_JWKLocal demo signing key used only to make Notary readiness pass
Notary replay Redis URLREGISTRY_NOTARY_REPLAY_REDIS_URLPoints Notary at the local demo Redis replay store

The Notary evaluator key is for you calling Notary. The Relay source token is for Notary calling Relay. They are intentionally separate.

The same secrets/local.env still carries the keys from the Relay tutorial, including the row-reader key ROW_READER_RAW, which this page uses later to compare a row read with a claim result.

Call the claim list without a credential:

Terminal window
curl -i http://127.0.0.1:4255/v1/claims

Notary returns 401 Unauthorized.

Call the same route with the Notary evaluator key:

Terminal window
curl -sS \
-H "x-api-key: $REGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAW" \
http://127.0.0.1:4255/v1/claims

The response is a JSON list of the configured claim definitions. The starter claim appears in it with "id": "benefits-person-exists".

Evaluate whether the synthetic person per-2001 exists in the Relay-backed benefits dataset:

Terminal window
curl -sS -X POST \
-H "x-api-key: $REGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAW" \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.registry-notary.claim-result+json" \
-d '{
"target": {
"type": "person",
"id": "per-2001"
},
"claims": ["benefits-person-exists"],
"disclosure": "predicate",
"purpose": "https://example.local/purpose/tutorial"
}' \
http://127.0.0.1:4255/v1/evaluations

Notary returns a successful claim result for benefits-person-exists. The response shows the claim outcome, not the Relay source row. It has this shape (identifiers, timestamps, and the target handle vary per run):

{
"results": [
{
"claim_id": "benefits-person-exists",
"claim_version": "2026-06",
"disclosure": "predicate",
"evaluation_id": "01KTS319VPVAJWKT8SZF4R8ZVN",
"expires_at": null,
"format": "application/vnd.registry-notary.claim-result+json",
"issued_at": "2026-06-10T15:39:53Z",
"provenance": {
"schema_version": "registry-notary-claim-provenance/v1",
"generated_by": {
"type": "claim_evaluation",
"service_id": "registryctl.benefits.notary",
"evaluation_id": "01KTS319VPVAJWKT8SZF4R8ZVN",
"claim_id": "benefits-person-exists",
"claim_version": "2026-06"
},
"used": {
"source_count": 1,
"source_versions": {},
"source_runtimes": []
},
"derived_from": []
},
"satisfied": true,
"subject_type": "person",
"target_ref": {
"handle": "rnref:v1:hmac-sha256:...",
"type": "person"
},
"value": true
}
]
}

What happened:

  • You called Notary with the Notary evaluator key.
  • Notary checked that the key can evaluate the configured claim.
  • Notary used its internal Relay source token to query Relay.
  • Relay enforced its row-read scope and purpose-header rules.
  • Notary returned the configured claim result without exposing the spreadsheet row.

Run the same evaluation for an id that is not in the sample workbook:

Terminal window
curl -sS -X POST \
-H "x-api-key: $REGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAW" \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.registry-notary.claim-result+json" \
-d '{
"target": { "type": "person", "id": "per-9999" },
"claims": ["benefits-person-exists"],
"disclosure": "predicate",
"purpose": "https://example.local/purpose/tutorial"
}' \
http://127.0.0.1:4255/v1/evaluations

Notary does not return satisfied: false for a missing person. It returns a 409 problem document, because there is no evidence to evaluate:

{
"code": "evidence.not_available",
"detail": "the evidence is not available",
"request_id": "01KTS31QC7184F5487VW0P7XM3",
"status": 409,
"title": "Evidence not available",
"type": "https://docs.registry-notary.dev/problems/evidence/not_available"
}

This distinction matters to integrators: a claim result answers the configured question about a known subject, while a missing subject is an evidence error, not a negative answer.

Relay still exposes the source-facing consultation API:

Terminal window
curl -sS -G \
-H "Authorization: Bearer $ROW_READER_RAW" \
-H "Data-Purpose: https://example.local/purpose/tutorial" \
--data-urlencode "id=per-2001" \
http://127.0.0.1:4242/v1/datasets/benefits_casework/entities/person/records

Notary exposes a claim API:

Terminal window
curl -sS -X POST \
-H "x-api-key: $REGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAW" \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.registry-notary.claim-result+json" \
-d '{
"target": { "type": "person", "id": "per-2001" },
"claims": ["benefits-person-exists"],
"disclosure": "predicate",
"purpose": "https://example.local/purpose/tutorial"
}' \
http://127.0.0.1:4255/v1/evaluations

Use Relay when a caller is allowed to consult configured records. Use Notary when a caller receives a narrow claim result.

registryctl add notary --from local-relay refreshes the generated Bruno collection with Notary requests. Bruno is optional. The tutorial and API work without it.

Open the generated collection:

Terminal window
registryctl bruno open

If Bruno is installed, the collection opens with Relay and Notary folders. If Bruno is not installed, the command prints the collection path and an install link.

If the Bruno CLI is installed, you can run the collection:

Terminal window
registryctl bruno run

If bru is not installed, the command prints a fallback and exits without blocking Relay or Notary.

Open the Notary API surface:

Terminal window
registryctl notary open

The command opens the docs page in your default browser. In an environment that cannot launch one, such as a remote shell, it prints the URLs instead:

Notary API docs: http://127.0.0.1:4255/docs
OpenAPI JSON: http://127.0.0.1:4255/openapi.json

Run Notary standalone for an API you operate

Section titled “Run Notary standalone for an API you operate”

Everything so far ran Notary inside the Relay project. When you already operate an API and only want claim verification, create a standalone Notary project instead and point it at your source API. The source must expose the Registry Data API-shaped record route Notary is configured to call.

If you arrived here directly, for example from the routing table on the docs homepage, note that the reproducible demo below uses the local registry API from the sections above as its stand-in source, so it needs that project on disk and running (the Before you start block creates it in about two minutes). If you already have a live source API of your own, skim the demo for the shape of the flow, then start from For an API you operate below and substitute your own URL, dataset, and token.

To keep the steps reproducible without external credentials, this section reuses the local registry API as the stand-in source. Leave the Relay project running, and load its demo keys in your current shell (the set -a block in Load local demo keys); the project-creation step reads ROW_READER_RAW from your shell.

From the parent directory of my-first-api, create the Notary project:

Terminal window
cd ..
registryctl init notary my-standalone-notary \
--source-url http://registry-relay:8080 \
--source-network my-first-api_default \
--source-token-from-env ROW_READER_RAW
cd my-standalone-notary

--source-token-from-env ROW_READER_RAW reads the token value from your current shell once, at project-creation time, and writes it into the new project’s secrets/local.env. my-first-api_default is the Compose network name Docker derives from the Relay project directory name; if you named that directory differently, use that name with the _default suffix. The generated Notary config contains the source API URL, source dataset, entity, lookup field, environment variable names, and credential commitments. The raw keys and tokens live only in this project’s own secrets/local.env.

Change the source options at project creation time. The three <...> values are placeholders for what your source API exposes; the demo values above (benefits_casework, person, id) are also the CLI defaults, so leaving these flags off wires Notary to a dataset that does not exist on your API:

Terminal window
registryctl init notary my-notary \
--source-url https://api.example.com \
--source-token-env EVIDENCE_SOURCE_API_TOKEN \
--source-dataset <your-dataset-id> \
--source-entity <your-entity> \
--source-lookup-field <your-lookup-field>

--source-token-env differs from --source-token-from-env above: it does not read a value from your shell now, it names the environment variable the Notary container resolves at runtime. Supply the value afterwards by editing secrets/local.env and setting EVIDENCE_SOURCE_API_TOKEN to the source token. If the source API is another Compose service, pass --source-network <compose-network> so Notary can join that network.

If you already have a FHIR source-adapter sidecar running, start from the FHIR profile instead of the Registry Data API defaults:

Terminal window
registryctl init notary my-fhir-notary --source-kind fhir-sidecar

This generates a starter patient-record-exists claim, points Notary at the sidecar URL http://host.docker.internal:4360, and uses FHIR_SIDECAR_TOKEN in secrets/local.env. Change --source-url, --source-token-env, --source-dataset, --source-entity, or --source-lookup-field if your sidecar uses different local names.

From the standalone project directory, the rest of the flow is the same as the co-located sections earlier on this page: registryctl start, registryctl notary smoke, load this project’s keys with the set -a block, and evaluate the claim against http://127.0.0.1:4255/v1/evaluations. The standalone project has its own secrets/local.env; load it from this directory, not the Relay project’s.

When you are done:

Terminal window
registryctl stop

This stops both local services. It does not delete your workbook, generated configs, local keys, or smoke results.

If you created the standalone Notary project, stop it from its own directory, then stop the source registry API from my-first-api.

SymptomCauseResolution
registryctl add notary --from local-relay cannot find a Relay projectThe current directory does not contain a generated registryctl.yaml with a Relay section.Run the command from the Relay tutorial project directory.
registryctl add notary --from local-relay cannot find a source tokensecrets/local.env is missing or does not contain the Relay row-reader key.Recreate the Relay project or restore the generated local env file.
registryctl start starts Relay but Notary is not readyNotary config, source token, or Compose service wiring is invalid.Run registryctl status, then registryctl logs and check the Notary service errors.
The Notary container log shows failed to parse config YAML ... unknown fieldThe locally cached container image does not match the digest-pinned image in the generated compose.yaml.Run docker compose pull in the project directory to refresh the pinned images, then registryctl start again.
Notary /ready is degraded while /healthz is healthyThe local replay store or demo issuer key is not available to Notary.Run registryctl stop, then registryctl start; if it persists, inspect registryctl logs.
registryctl notary smoke returns 401 for authorized callsThe Notary evaluator key was not loaded or does not match the generated Notary fingerprint.Run . secrets/local.env, then retry.
Claim evaluation returns a source auth errorNotary cannot authenticate to Relay with EVIDENCE_SOURCE_REGISTRY_RELAY_TOKEN.Confirm secrets/local.env has the source token and Relay is running.
Claim evaluation returns 409 Evidence not availableThe target id is not in the sample workbook or the Relay entity lookup changed.Use per-2001 for the tutorial target, or inspect the Relay person entity.
registryctl init notary fails with failed to read source token from $ROW_READER_RAW: environment variable not foundThe Relay keys were loaded in a different shell session, or not at all.Run the set -a; . secrets/local.env; set +a block from the Relay project directory in this shell, then rerun the command.
Standalone registryctl start fails and the Notary log shows network ... not foundThe Relay project is not running, or the --source-network name does not match the Relay project directory.Start the Relay project, and pass the Compose network named after that directory with the _default suffix.
Standalone registryctl notary smoke returns 401 for authorized callsThe shell still holds the Relay project’s keys, not the standalone Notary keys.Run set -a; . secrets/local.env; set +a from the standalone project directory, then retry.