Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.26.1.
Issue registered-parent evidence from OpenCRVS
For the assertion provider
Complete Assert a role-bound relationship before starting this tutorial. You will apply that pattern to the public OpenCRVS Farajaland demo and answer two questions from the same registered birth. First verify, “Is this candidate registered as a parent of this child?” Then return the recorded mother and father national IDs for a caller authorized to receive them.
This tutorial uses synthetic data from a public demonstration system. Do not substitute a real person. The demo maintainers can reset its data, accounts, and integrations.
Understand the two disclosure paths
Section titled “Understand the two disclosure paths”The tutorial uses this provider-published synthetic registration:
| Field | Value | Use |
|---|---|---|
| Child | Josh Hoeger | Find the registration in the web application |
| Child national ID | 3617402568 | Select the registered birth through the Events API |
| Tracking ID | YADU1N | Cross-check the registration in the web application |
| Registration number | CJWULMP0B6Z5 | Cross-check the registration in the web application |
The registered birth contains an authenticated national ID for Josh’s mother. It reports that the father’s details are unavailable. You will copy the mother’s national ID from the web application and use it as the candidate-parent selector.
The relationship question has two distinct subject roles:
child, selected by the child’s national IDcandidate-parent, selected by the candidate’s national ID
The source searches for one registered birth by child.nid and reads the bounded mother.nid and
father.nid fields when present. One question compares those values with a candidate and returns
only a boolean. A second question returns the parent identifiers in a closed, role-labeled value.
The two questions use separate purposes and disclosure rules.
Inspect the synthetic registration
Section titled “Inspect the synthetic registration”Open the Farajaland registration application and sign in with the provider-published Registrar account:
Username: k.mweenePassword: testTwo-factor code: 000000Search for 3617402568, YADU1N, or CJWULMP0B6Z5. Open Josh Hoeger’s registered birth and note
the mother’s national ID. Keep it outside the tracked project. You can also use this account to
create another synthetic registration.
Create your Record Search client
Section titled “Create your Record Search client”The human account cannot authorize the Events API. Create a separate OAuth client for the tutorial instead of sharing an existing client ID or secret.
In your own OpenCRVS deployment, sign in with a National System Admin account. OpenCRVS publishes this account for the Farajaland demo:
Username: j.campbellPassword: testTwo-factor code: 000000In Configuration, open Integrations, select Create client, and choose Record Search. Give the client a name that identifies your tutorial run. Copy the client ID and secret when OpenCRVS displays them. The secret is shown once.
The client can search records. It cannot create, update, or delete registrations. OpenCRVS audits its searches and applies a daily request limit. Review the OpenCRVS guidance for creating a client and Record Search clients before using this pattern outside the demo.
Do not put the OAuth credentials, access token, or a live response in source control. The private credentials used to validate this page are not part of the tutorial or repository.
Create the authoring project
Section titled “Create the authoring project”Create a working directory and download the reviewed tutorial subset of the OpenCRVS Events API:
mkdir opencrvs-parent-relationshipcd opencrvs-parent-relationshipcurl -fsSLo opencrvs-events.openapi.yaml \ https://docs.registrystack.org/examples/evidence/opencrvs-events-search.openapi.yamlOpenCRVS does not publish this bounded subset at a stable URL. Registry Stack maintains it for the tutorial and records its upstream review revision in the file. Review it against the target deployment before production use.
Create an editable Evidence Gateway project and disposable local keys:
evidencectl init registered-parent \ --openapi opencrvs-events.openapi.yaml \ --profile localcd registered-parentThe command retains the OpenAPI document and creates empty authoring directories. It does not invent the source policy, relationship semantics, or question.
Draft the OpenCRVS source
Section titled “Draft the OpenCRVS source”Select only the fields needed to establish cardinality, validate the birth, and compare its registered parents:
evidencectl source suggest \ --project . \ --source-id opencrvs-birth-parents \ --operation 'POST /events/search' \ --select /total \ --select '/results/*/type' \ --select '/results/*/status' \ --select '/results/*/declaration/child.nid' \ --select '/results/*/declaration/mother.nid' \ --select '/results/*/declaration/mother.verified' \ --select '/results/*/declaration/father.nid' \ --select '/results/*/declaration/father.verified'The command writes one source, two scripts, and three schemas. They remain editable and cannot run until you supply the OpenCRVS-specific decisions.
Define the national-ID selector
Section titled “Define the national-ID selector”Create selectors/opencrvs-national-id-v1.yaml:
maximumAggregateBytes: 128fields: national_id: type: string minimumBytes: 1 maximumBytes: 128The same bounded field can identify the child and candidate parent, but their roles remain distinct. A caller cannot omit a role or substitute another selector field.
Configure the bounded source
Section titled “Configure the bounded source”Replace sources/opencrvs-birth-parents.yaml with:
transport: http-jsonbaseUrl: https://events.farajaland-integration.opencrvs.devposture: record-transformedauthentication: kind: oauth2-client-credentials tokenEndpoint: https://auth.farajaland-integration.opencrvs.dev/token clientIdRef: secret:file/opencrvs-client-id clientSecretRef: secret:file/opencrvs-client-secret scope: recordsearch credentialPlacement: form-body maximumCacheSeconds: 300 assumedLifetimeSeconds: 600request: method: POST path: /events/search fixedHeaders: - name: Accept value: application/json selectorInputs: - role: child alternatives: - profile: opencrvs-national-id-v1 fields: [national_id] prepareScript: adapters/opencrvs-birth-parents-prepare.rhai adapterParameters: childSelectorRole: child eventType: birth registeredStatus: REGISTERED childNationalIdField: child.nid motherNationalIdField: mother.nid motherVerificationField: mother.verified fatherNationalIdField: father.nid fatherVerificationField: father.verified authenticatedValue: authenticated resultLimit: 2 resultOffset: 0 adapterParametersSchema: schemas/opencrvs-birth-parents-parameters.schema.yaml preparationLimits: query: forbidden jsonBody: required maximumJsonDepth: 12 maximumCollectionItems: 32 maximumStringBytes: 512 maximumNormalizedBytes: 8192 projection: - /total - /results/*/type - /results/*/status - /results/*/declaration/child.nid - /results/*/declaration/mother.nid - /results/*/declaration/mother.verified - /results/*/declaration/father.nid - /results/*/declaration/father.verified redirects: deny timeoutMilliseconds: 10000 maximumResponseBytes: 262144 concurrencyLimit: 8responseSchema: schemas/opencrvs-birth-parents-response.schema.yamlextractScript: adapters/opencrvs-birth-parents-extract.rhaifactSchema: schemas/opencrvs-birth-parents-facts.schema.yamlrecord-transformed states that OpenCRVS returns a bounded birth record and Evidence Gateway reduces it
before disclosure. The fixed event type and status prevent the caller from turning this source
into a general search proxy. A two-result ceiling lets extraction distinguish one match from an
ambiguous national ID without paging through the registry.
The token endpoint accepts credentials in the form body. OpenCRVS omits an expiry from this demo’s token response, so the source assumes 600 seconds and clamps caching to 300 seconds.
Store your client credentials
Section titled “Store your client credentials”Read the two values without echoing them and write owner-only files without a trailing newline:
umask 077printf 'OpenCRVS client ID: ' >&2IFS= read -rs OPENCRVS_CLIENT_IDprintf '\n' >&2printf '%s' "$OPENCRVS_CLIENT_ID" > secrets/opencrvs-client-idunset OPENCRVS_CLIENT_ID
printf 'OpenCRVS client secret: ' >&2IFS= read -rs OPENCRVS_CLIENT_SECRETprintf '\n' >&2printf '%s' "$OPENCRVS_CLIENT_SECRET" > secrets/opencrvs-client-secretunset OPENCRVS_CLIENT_SECRETPaste each value copied from OpenCRVS at its prompt and press Enter. The generated .gitignore
excludes the secrets directory.
Evidence Gateway resolves these logical secret references beneath that directory. It does not read the credentials from source YAML, requests, command arguments, or logs.
Prepare the fixed birth search
Section titled “Prepare the fixed birth search”Replace adapters/opencrvs-birth-parents-prepare.rhai with:
fn prepare(selectors, context) { let parameters = context["parameters"]; let child = selectors[parameters["childSelectorRole"]]; let data = #{}; data[parameters["childNationalIdField"]] = #{ type: "exact", term: child["values"]["national_id"] };
#{ query: [], body: #{ query: #{ type: "and", clauses: [#{ eventType: parameters["eventType"], status: #{ type: "exact", term: parameters["registeredStatus"] }, data: data }] }, limit: parameters["resultLimit"], offset: parameters["resultOffset"] } }}The script can read the authorized child selector and fixed parameters. It cannot read the candidate parent, credentials, caller identity, purpose, or signing keys.
Replace schemas/opencrvs-birth-parents-parameters.schema.yaml with:
type: objectadditionalProperties: falserequired: - childSelectorRole - eventType - registeredStatus - childNationalIdField - motherNationalIdField - motherVerificationField - fatherNationalIdField - fatherVerificationField - authenticatedValue - resultLimit - resultOffsetproperties: childSelectorRole: {const: child} eventType: {const: birth} registeredStatus: {const: REGISTERED} childNationalIdField: {const: child.nid} motherNationalIdField: {const: mother.nid} motherVerificationField: {const: mother.verified} fatherNationalIdField: {const: father.nid} fatherVerificationField: {const: father.verified} authenticatedValue: {const: authenticated} resultLimit: {const: 2} resultOffset: {const: 0}The schema closes every parameter around its reviewed value. A later edit cannot silently change the event, status, search field, parent fields, verification value, or page size.
Extract authenticated parent identifiers
Section titled “Extract authenticated parent identifiers”The generated response schema closes the projected response around total, at most two results,
and the selected declaration fields. Replace adapters/opencrvs-birth-parents-extract.rhai with:
fn extract(source_response, context) { let parameters = context["parameters"]; let total = source_response["total"]; let results = source_response["results"]; if results.len > parameters["resultLimit"] { throw("source_protocol_error"); }
if total == 0 { if results.len != 0 { throw("source_protocol_error"); } return #{outcome: "no_match"}; } if total > 1 { if results.len < 2 { throw("source_protocol_error"); } return #{outcome: "ambiguous"}; } if results.len != 1 { throw("source_protocol_error"); }
let result = results[0]; if result["type"] != parameters["eventType"] || result["status"] != parameters["registeredStatus"] || !result.contains("declaration") { throw("source_protocol_error"); }
let declaration = result["declaration"]; let child_field = parameters["childNationalIdField"]; if !declaration.contains(child_field) { throw("source_protocol_error"); }
let parents = []; let mother_field = parameters["motherNationalIdField"]; if declaration.contains(mother_field) { let verification = parameters["motherVerificationField"]; if !declaration.contains(verification) || declaration[verification] != parameters["authenticatedValue"] { throw("source_protocol_error"); } parents.push(#{ role: "mother", national_id: declaration[mother_field] }); }
let father_field = parameters["fatherNationalIdField"]; if declaration.contains(father_field) { let verification = parameters["fatherVerificationField"]; if !declaration.contains(verification) || declaration[verification] != parameters["authenticatedValue"] { throw("source_protocol_error"); } let father_id = declaration[father_field]; if parents.len == 1 && parents[0]["national_id"] == father_id { throw("source_protocol_error"); } parents.push(#{role: "father", national_id: father_id}); }
if parents.len == 0 || parents.len > 2 { throw("source_protocol_error"); }
#{ outcome: "match", facts: #{ child_national_id: declaration[child_field], registered_parents: parents } }}The script refuses inconsistent cardinality, incomplete births, and parent identifiers that the record does not mark as authenticated. An absent parent identifier is not converted into an invented parent. The source must establish at least one authenticated parent before the question can run.
Replace schemas/opencrvs-birth-parents-facts.schema.yaml with:
type: objectadditionalProperties: falserequired: [child_national_id, registered_parents]properties: child_national_id: type: string minLength: 1 maxLength: 128 registered_parents: type: array minItems: 1 maxItems: 2 uniqueItems: true items: type: object additionalProperties: false required: [role, national_id] properties: role: type: string enum: [mother, father] national_id: type: string minLength: 1 maxLength: 128These facts are the complete input to both questions. Names, dates, addresses, and other birth fields cannot reach either derivation.
Author the relationship question
Section titled “Author the relationship question”Create questions/registered-parent.yaml:
id: registered-parentquestion: Is the candidate registered as a parent of the child?purpose: relationship-checksubjects: - role: child selector: national_id profile: opencrvs-national-id-v1 derivation: true - role: candidate-parent selector: national_id profile: opencrvs-national-id-v1 derivation: truesource: ref: opencrvs-birth-parentsanswers: - concept: relationship_confirmed type: booleanderivation: derivations/registered-parent.rhaidisclosure: allow: [relationship_confirmed]The question declares the complete role set. derivation: true gives the script the child needed
to verify the returned birth and the candidate needed for the comparison. The source search itself
uses only the child. The question governs the one allowed purpose, answer, and disclosure. Sharing
a selector profile does not make the two roles interchangeable.
Create derivations/registered-parent.rhai:
fn answer(facts, selectors, context) { if facts.child_national_id != selectors.child.values.national_id { throw("derivation_input_error"); }
let candidate = selectors["candidate-parent"]["values"]["national_id"]; let confirmed = false; for parent in facts.registered_parents { if parent["national_id"] == candidate { confirmed = true; } } #{relationship_confirmed: confirmed}}The first check binds the returned birth to the requested child. The comparison checks the candidate against at most two authenticated parent identifiers, then returns only the declared boolean.
Author the parent-identifier question
Section titled “Author the parent-identifier question”Create schemas/registered-parent-national-ids.schema.yaml:
$id: urn:registrystack:evidence:local:schema:registered-parent-national-ids:v1type: objectadditionalProperties: falserequired: [parents]properties: parents: type: array minItems: 1 maxItems: 2 uniqueItems: true items: type: object additionalProperties: false required: [role, national_id] properties: role: type: string enum: [mother, father] national_id: type: string minLength: 1 maxLength: 128This closed schema preserves each parent’s role and rejects extra fields. The bounded array holds one or two role-labeled identifiers. The extraction script refuses to produce facts unless the source establishes at least one authenticated parent identifier.
Create questions/registered-parent-identifiers.yaml:
id: registered-parent-identifiersquestion: Which national IDs are recorded for the child's parents?purpose: family-case-recordsubject: role: child selector: national_id profile: opencrvs-national-id-v1 derivation: truesource: ref: opencrvs-birth-parentsanswers: - concept: registered_parent_national_ids type: reviewed-structured-value schema: schemas/registered-parent-national-ids.schema.yaml maximumSerializedBytes: 512derivation: derivations/registered-parent-identifiers.rhaidisclosure: allow: [registered_parent_national_ids]This question has no candidate-parent subject. The caller asks for the registered identifiers and
must be authorized for the separate family-case-record purpose. In production, grant this
question only to relying parties with a justified need for the identifiers. The boolean question
remains available to callers that need only a relationship decision.
Create derivations/registered-parent-identifiers.rhai:
fn answer(facts, selectors, context) { if facts.child_national_id != selectors.child.values.national_id { throw("derivation_input_error"); }
#{ registered_parent_national_ids: #{ form: "reviewed-structured-value", schema: "urn:registrystack:evidence:local:schema:registered-parent-national-ids:v1", fields: #{parents: facts.registered_parents} } }}Evidence Gateway validates the returned object against the reviewed schema before signing it. The derivation cannot add names, dates, addresses, or any other birth-registration field.
Start the project
Section titled “Start the project”Compile the editable artifacts into a private local generation and start Evidence Gateway with its local issuer:
evidencectl dev start .Evidence ready at http://127.0.0.1:8080Issuer ready at http://127.0.0.1:8081dev runs the real Evidence Gateway configuration check before either service becomes ready. Unresolved
draft markers, invalid schemas, inconsistent roles, or missing credentials stop the start.
Send a real Evidence Gateway request
Section titled “Send a real Evidence Gateway request”Read the mother’s national ID without leaving its value in shell history:
printf 'Mother national ID: ' >&2IFS= read -rs OPENCRVS_MOTHER_NIDprintf '\n' >&2export OPENCRVS_MOTHER_NIDWrite both subjects into an owner-only request file. Josh’s national ID is the child, and the value you typed is the candidate parent:
python3 - <<'PY'import jsonimport os
selection = { "subjects": [ {"role": "child", "field": "national_id", "value": "3617402568"}, { "role": "candidate-parent", "field": "national_id", "value": os.environ["OPENCRVS_MOTHER_NID"], }, ]}descriptor = os.open( "../opencrvs-parent-subjects.json", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)with os.fdopen(descriptor, "w", encoding="utf-8") as destination: json.dump(selection, destination, separators=(",", ":")) destination.write("\n")
print("Subject file: ready")PYunset OPENCRVS_MOTHER_NIDPrepare a request from that file:
evidencectl request prepare registered-parent \ --purpose relationship-check \ --subjects-file ../opencrvs-parent-subjects.json \ --name opencrvs-parentUse this only with the public synthetic record.
evidencectl takes either --subject or --subjects-file and refuses both, so the value you
typed never reaches a command line. It reads the file only when the file is a regular file you own
with exactly one link and mode 0600.
Preparation obtains short-lived local authorization and records verification expectations before a response exists. It does not contact OpenCRVS.
Send the request across the Evidence Gateway HTTP boundary:
curl -fsS \ --config .evidence/requests/opencrvs-parent/authorization.curl \ --request POST \ --url http://127.0.0.1:8080/v1/evidence \ --header 'Content-Type: application/json' \ --header 'Accept: application/jose+json' \ --data-binary @.evidence/requests/opencrvs-parent/request.json \ --output opencrvs-parent.jws.jsonEvidence Gateway exchanges its client credentials for an OpenCRVS token, performs the fixed child search, projects the bounded parent fields, derives the relationship, and returns a signed flattened JWS. The caller cannot alter the event type, status, projection, or parent verification rule.
Verify before reading
Section titled “Verify before reading”Verify the response against the expectations created before the request:
evidencectl verify opencrvs-parent.jws.json \ --context .evidence/requests/opencrvs-parent/verification.json \ --output opencrvs-parent.verified.jsonVERIFIEDInspect the verified payload:
python3 -m json.tool opencrvs-parent.verified.jsonUsing the mother recorded on Josh’s birth produces this supported value:
{ "providesValueFor": "urn:registrystack:evidence:local:concept:registered-parent:relationship_confirmed", "value": true}The assertion contains pseudonymous child and candidate-parent bindings. It does not contain
either national ID, the tracking ID, registration number, names, birth date, parent list, or exact
source response. The civil-registration record stayed behind the source boundary.
Return the registered parent identifiers
Section titled “Return the registered parent identifiers”Prepare the second question. This request needs only the child because the answer returns the recorded parent identifiers instead of comparing one with a candidate:
evidencectl request prepare registered-parent-identifiers \ --purpose family-case-record \ --subject child:national_id=3617402568 \ --name opencrvs-parent-identifiersSend the request across the same HTTP boundary:
curl -fsS \ --config .evidence/requests/opencrvs-parent-identifiers/authorization.curl \ --request POST \ --url http://127.0.0.1:8080/v1/evidence \ --header 'Content-Type: application/json' \ --header 'Accept: application/jose+json' \ --data-binary @.evidence/requests/opencrvs-parent-identifiers/request.json \ --output opencrvs-parent-identifiers.jws.jsonVerify the signature and the expectations recorded for this question before reading its value:
evidencectl verify opencrvs-parent-identifiers.jws.json \ --context .evidence/requests/opencrvs-parent-identifiers/verification.json \ --output opencrvs-parent-identifiers.verified.jsonVERIFIEDInspect the verified payload:
python3 -m json.tool opencrvs-parent-identifiers.verified.jsonJosh’s public registration has an authenticated mother identifier but reports the father’s details as unavailable. Its supported value therefore contains the mother field only:
{ "providesValueFor": "urn:registrystack:evidence:local:concept:registered-parent-identifiers:registered_parent_national_ids", "value": { "form": "reviewed-structured-value", "schema": "urn:registrystack:evidence:local:schema:registered-parent-national-ids:v1", "fields": { "parents": [ { "role": "mother", "national_id": "<mother-national-id>" } ] } }}For a synthetic registration with two authenticated parent identifiers, parents contains a
second item with role father. You can create such a registration in the demo and prepare the
same question with that child’s national ID.
This assertion intentionally discloses identifiers. Store and transmit the verified result as sensitive civil-registration data. Callers that need only a relationship decision must use the boolean question instead.
Inspect the audit and clean up
Section titled “Inspect the audit and clean up”Stop the services:
evidencectl dev stopInspect the last operation:
evidencectl audit show --last-operationACCESS AUTHORIZED registered-parent-identifiers family-case-record requester=<pseudonym>DISCLOSURE RELEASED registered_parent_national_idsThe requester value changes on each fresh project. This view covers the last operation only, so it shows the identifier question rather than the boolean one you asked first.
Read those two lines for what they leave out. They name the requester pseudonym, the question, the purpose it was authorized under, and the one concept released. That concept is a list of parent identifiers, and the audit still carries no national ID: not the child’s, not the mother’s, and not the candidate value you typed at the prompt. Neither line carries the OpenCRVS token, the search response, the tracking ID, or the registration number. That gap is the point of the trail. An operator can establish that a caller was authorized to receive registered parent identifiers under a stated purpose, and cannot read those identifiers out of the audit.
Remove the sealed local generation and the request file holding the two national IDs:
evidencectl dev cleanrm -f ../opencrvs-parent-subjects.jsonKeep the project, Record Search client, and two local credential files if you are continuing to the birth-certificate tutorial. Otherwise, delete the client in OpenCRVS and remove the credential files when you finish.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Resolution |
|---|---|---|
| The OpenCRVS token request fails with 401 | The Record Search client was deleted, its secret was rotated, or the demo reset its integrations. | Create a new Record Search client and rewrite secrets/opencrvs-client-id and secrets/opencrvs-client-secret. Keep both values out of shell history and tracked files. |
| Signing in to the demo fails | The demo rotated its published logins, or the environment was rebuilt. | Take the current logins from the OpenCRVS Farajaland integration demo documentation. Do not use a real OpenCRVS account to complete this tutorial. |
Writing the subject file fails with FileExistsError | os.open refuses O_EXCL when the file is already there, so a second run cannot overwrite it. | Remove ../opencrvs-parent-subjects.json and run the step again. |
| The request returns no assertion | The demo was reset and the synthetic registration no longer exists under that national ID, so the fixed search matched nothing. | Confirm the record is still in the demo. Leave the extractor’s no_match path alone; a missing registration is an answer. |
| Evidence Gateway reports a source protocol error | A parent identifier appears in the declaration without the record marking it authenticated, or the result set contradicts total. | Read the record in the demo. Leave the authentication check in place: an unverified parent entry is not a registered parent. |
The relationship question answers false for a parent you believe is recorded | The candidate national ID does not match the identifier the registration records for that role. | Compare it with the record in the demo. false is the source’s current answer, not a defect to tune away. |
| Requests start failing after several runs | OpenCRVS audits its searches and applies a daily request limit on the integration demo. | Wait for the limit to reset before running the tutorial again. Do not add retries around the source. |
| Evidence Gateway reports a source dependency failure | The demo host was unreachable or slow, or the response exceeded timeoutMilliseconds or maximumResponseBytes. | Retry later. Treat the bounds as reviewed source policy, and raise one only after deciding it is right for the deployment. |
| The token request or the search fails with a certificate error | An intercepting proxy or an out-of-date trust store on your machine. | Repair the trust store, or exempt the demo hosts from interception. Do not reach for --insecure: the source denies redirects and reaches OpenCRVS over HTTPS only. |