Passports
Generate a passport from a product, publish it, register the resolvable Digital Link, and drive its post-publish lifecycle through the command envelope.
| POST | /api/v2/dpp/generateGenerate a passport |
| GET | /api/v2/dpp/status/{requestId}Generation status |
| POST | /api/v2/dpp/{productId}/publishPublish passport content |
| POST | /api/v2/products/{id}/publish-dppRegister the Digital Link |
| POST | /api/v2/passports/{id}/publishBroadcast to external registries |
| POST | /api/v2/passports/commandsPassport lifecycle command |
| GET | /api/v2/passports/commandsCommand catalog |
| GET | /api/v2/passports/{id}/historyVersion history |
| GET | /api/v2/passports/{id}/validationsValidation chain + readyToPrint |
| POST | /api/v2/passports/{id}/validationsAnchor a validation result |
| GET | /api/v2/passports/{id}/as-ofTemporal snapshot (as-of) |
Run the 6-stage generation pipeline from a ready_for_dpp product. Async — poll GET /api/v2/dpp/status/{requestId}. In sandbox, anchorToBlockchain is always effectively false.
POST {{baseUrl}}/api/v2/dpp/generate
Authorization: Bearer {{apiKey}}
Content-Type: application/json
Idempotency-Key: {{uuid}}
{
"productId": "{{productId}}",
"strictness": "advisory",
"region": "EU",
"lifecycleStage": "production",
"dppOptions": {
"requestId": "dpp-gen-001",
"manufacturerDid": "did:ebsi:example-gmbh",
"anchorToBlockchain": false
}
}curl -X POST "$BASE_URL/api/v2/dpp/generate" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"productId": "{{productId}}",
"strictness": "advisory",
"region": "EU",
"lifecycleStage": "production",
"dppOptions": {
"requestId": "dpp-gen-001",
"manufacturerDid": "did:ebsi:example-gmbh",
"anchorToBlockchain": false
}
}'const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const res = await fetch(`${baseUrl}/api/v2/dpp/generate`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`,
"Idempotency-Key": `${crypto.randomUUID()}`
},
body: JSON.stringify({
"productId": "{{productId}}",
"strictness": "advisory",
"region": "EU",
"lifecycleStage": "production",
"dppOptions": {
"requestId": "dpp-gen-001",
"manufacturerDid": "did:ebsi:example-gmbh",
"anchorToBlockchain": false
}
})
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
resp = requests.post(
f"{base_url}/api/v2/dpp/generate",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json",
"Idempotency-Key": f"{uuid.uuid4()}"
},
json={
"productId": "{{productId}}",
"strictness": "advisory",
"region": "EU",
"lifecycleStage": "production",
"dppOptions": {
"requestId": "dpp-gen-001",
"manufacturerDid": "did:ebsi:example-gmbh",
"anchorToBlockchain": False
}
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"bytes"
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
url := fmt.Sprintf("%s/api/v2/dpp/generate", baseURL)
payload := []byte(`{
"productId": "{{productId}}",
"strictness": "advisory",
"region": "EU",
"lifecycleStage": "production",
"dppOptions": {
"requestId": "dpp-gen-001",
"manufacturerDid": "did:ebsi:example-gmbh",
"anchorToBlockchain": false
}
}`)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "REPLACE-WITH-UUIDv4")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}202 Accepted — poll /api/v2/dpp/status/{requestId}.
Async generation status for a dpp/generate request.
GET {{baseUrl}}/api/v2/dpp/status/{requestId}
Authorization: Bearer {{apiKey}}curl -X GET "$BASE_URL/api/v2/dpp/status/{requestId}" \
-H "Authorization: Bearer $NORRUVA_API_KEY"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const requestId = "…";
const res = await fetch(`${baseUrl}/api/v2/dpp/status/${requestId}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
request_id = "…"
resp = requests.get(
f"{base_url}/api/v2/dpp/status/{request_id}",
headers={
"Authorization": f"Bearer {api_key}"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
requestId := "…"
url := fmt.Sprintf("%s/api/v2/dpp/status/%s", baseURL, requestId)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}Mints the public passport content and an immutable version; returns {{passportUid}}. Publish ≠ register — the Digital Link registration is the separate publish-dpp call.
POST {{baseUrl}}/api/v2/dpp/{productId}/publish
Authorization: Bearer {{apiKey}}
Content-Type: application/json
Idempotency-Key: {{uuid}}
{
"amendmentReason": "Initial publication",
"updates": { "batteryPassport": { "capacity": 85, "cycleLife": 1500 } }
}curl -X POST "$BASE_URL/api/v2/dpp/{productId}/publish" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amendmentReason": "Initial publication",
"updates": {
"batteryPassport": {
"capacity": 85,
"cycleLife": 1500
}
}
}'const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const productId = "…";
const res = await fetch(`${baseUrl}/api/v2/dpp/${productId}/publish`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`,
"Idempotency-Key": `${crypto.randomUUID()}`
},
body: JSON.stringify({
"amendmentReason": "Initial publication",
"updates": {
"batteryPassport": {
"capacity": 85,
"cycleLife": 1500
}
}
})
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
product_id = "…"
resp = requests.post(
f"{base_url}/api/v2/dpp/{product_id}/publish",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json",
"Idempotency-Key": f"{uuid.uuid4()}"
},
json={
"amendmentReason": "Initial publication",
"updates": {
"batteryPassport": {
"capacity": 85,
"cycleLife": 1500
}
}
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"bytes"
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
productId := "…"
url := fmt.Sprintf("%s/api/v2/dpp/%s/publish", baseURL, productId)
payload := []byte(`{
"amendmentReason": "Initial publication",
"updates": {
"batteryPassport": {
"capacity": 85,
"cycleLife": 1500
}
}
}`)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "REPLACE-WITH-UUIDv4")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}The deliberate step that makes the item resolvable (dpp_identifiers 0 → 1). Returns the verifiable credential plus a merkleRoot for later verification. passportType is optional and NOT defaulted — omit it and the type is derived from the product (battery-marked → battery, otherwise generic). A battery-marked product returns 503 COMPLIANCE_EVALUATION_UNAVAILABLE while the battery pack is quarantined (ATLAS-F017); non-battery products publish normally.
POST {{baseUrl}}/api/v2/products/{id}/publish-dpp
Authorization: Bearer {{apiKey}}
Content-Type: application/json
Idempotency-Key: {{uuid}}
{ "publishToWallet": false }curl -X POST "$BASE_URL/api/v2/products/{id}/publish-dpp" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"publishToWallet": false
}'const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/products/${id}/publish-dpp`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`,
"Idempotency-Key": `${crypto.randomUUID()}`
},
body: JSON.stringify({
"publishToWallet": false
})
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.post(
f"{base_url}/api/v2/products/{id}/publish-dpp",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json",
"Idempotency-Key": f"{uuid.uuid4()}"
},
json={
"publishToWallet": False
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"bytes"
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/products/%s/publish-dpp", baseURL, id)
payload := []byte(`{
"publishToWallet": false
}`)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "REPLACE-WITH-UUIDv4")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}Broadcast an approved/published passport to external registries (Catena-X / GS1 / EBSI). A third, distinct publish operation — optional. In sandbox/local none of the three connectors is configured, so the honest answer there is 503 REGISTRY_UNAVAILABLE, not a success.
POST {{baseUrl}}/api/v2/passports/{id}/publish
Authorization: Bearer {{apiKey}}
Content-Type: application/jsoncurl -X POST "$BASE_URL/api/v2/passports/{id}/publish" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/passports/${id}/publish`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.post(
f"{base_url}/api/v2/passports/{id}/publish",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/passports/%s/publish", baseURL, id)
req, _ := http.NewRequest("POST", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}All post-publish state changes use one self-documenting envelope. Guards are real: EDIT on a published passport → 403 GUARD_REJECTED.
POST {{baseUrl}}/api/v2/passports/commands
Authorization: Bearer {{apiKey}}
Content-Type: application/json
Idempotency-Key: {{uuid}}
{
"type": "PASSPORT.SUSPEND",
"tenantId": "{{tenantId}}",
"actorId": "{{userId}}",
"idempotencyKey": "{{uuid}}",
"correlationId": "{{uuid}}",
"requestedAt": "2026-07-25T12:00:00.000Z",
"payload": { "passportId": "{{passportUid}}" }
}curl -X POST "$BASE_URL/api/v2/passports/commands" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"type": "PASSPORT.SUSPEND",
"tenantId": "{{tenantId}}",
"actorId": "{{userId}}",
"idempotencyKey": "{{uuid}}",
"correlationId": "{{uuid}}",
"requestedAt": "2026-07-25T12:00:00.000Z",
"payload": {
"passportId": "{{passportUid}}"
}
}'const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const res = await fetch(`${baseUrl}/api/v2/passports/commands`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`,
"Idempotency-Key": `${crypto.randomUUID()}`
},
body: JSON.stringify({
"type": "PASSPORT.SUSPEND",
"tenantId": "{{tenantId}}",
"actorId": "{{userId}}",
"idempotencyKey": "{{uuid}}",
"correlationId": "{{uuid}}",
"requestedAt": "2026-07-25T12:00:00.000Z",
"payload": {
"passportId": "{{passportUid}}"
}
})
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
resp = requests.post(
f"{base_url}/api/v2/passports/commands",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json",
"Idempotency-Key": f"{uuid.uuid4()}"
},
json={
"type": "PASSPORT.SUSPEND",
"tenantId": "{{tenantId}}",
"actorId": "{{userId}}",
"idempotencyKey": "{{uuid}}",
"correlationId": "{{uuid}}",
"requestedAt": "2026-07-25T12:00:00.000Z",
"payload": {
"passportId": "{{passportUid}}"
}
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"bytes"
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
url := fmt.Sprintf("%s/api/v2/passports/commands", baseURL)
payload := []byte(`{
"type": "PASSPORT.SUSPEND",
"tenantId": "{{tenantId}}",
"actorId": "{{userId}}",
"idempotencyKey": "{{uuid}}",
"correlationId": "{{uuid}}",
"requestedAt": "2026-07-25T12:00:00.000Z",
"payload": {
"passportId": "{{passportUid}}"
}
}`)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "REPLACE-WITH-UUIDv4")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}The self-documenting catalog of passport lifecycle commands.
GET {{baseUrl}}/api/v2/passports/commands
Authorization: Bearer {{apiKey}}curl -X GET "$BASE_URL/api/v2/passports/commands" \
-H "Authorization: Bearer $NORRUVA_API_KEY"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const res = await fetch(`${baseUrl}/api/v2/passports/commands`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
resp = requests.get(
f"{base_url}/api/v2/passports/commands",
headers={
"Authorization": f"Bearer {api_key}"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
url := fmt.Sprintf("%s/api/v2/passports/commands", baseURL)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}The immutable version chain: version number, previous_version_id hash-chain link, current flag. Republishing mints v(N+1); v(N) is superseded, never edited.
GET {{baseUrl}}/api/v2/passports/{id}/history
Authorization: Bearer {{apiKey}}curl -X GET "$BASE_URL/api/v2/passports/{id}/history" \
-H "Authorization: Bearer $NORRUVA_API_KEY"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/passports/${id}/history`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.get(
f"{base_url}/api/v2/passports/{id}/history",
headers={
"Authorization": f"Bearer {api_key}"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/passports/%s/history", baseURL, id)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}The anchored validation chain plus the derived readyToPrint flag (= validated ∧ published, PRD F6) — the go/no-go signal before printing carriers.
GET {{baseUrl}}/api/v2/passports/{id}/validations
Authorization: Bearer {{apiKey}}curl -X GET "$BASE_URL/api/v2/passports/{id}/validations" \
-H "Authorization: Bearer $NORRUVA_API_KEY"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/passports/${id}/validations`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.get(
f"{base_url}/api/v2/passports/{id}/validations",
headers={
"Authorization": f"Bearer {api_key}"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/passports/%s/validations", baseURL, id)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}{
"upi": "…",
"validations": [
{ "eventId": "…", "occurredAt": "…", "digest": "…",
"signatureAlgorithm": "…", "jrcAnchor": "…" }
],
"readyToPrint": true // = at least one anchored validation AND currently published
}The write half of readyToPrint: anchor a validation-result Verifiable Credential into the lifecycle log (VALIDATION_RESULT_ANCHORED, JRC Annex 9). Tenant-guarded (a passport you don't own is a 404). The credential is cryptographically verified before anything is written: it needs a single DataIntegrityProof with cryptosuite eddsa-jcs-2022 and proofPurpose assertionMethod, whose verificationMethod DID equals the issuer DID and whose Ed25519 signature checks out — otherwise a typed 422 (VALIDATION_CREDENTIAL_NOT_VERIFIED) BEFORE any write. The issuer DID method must also be EUDIW-compatible — did:web, did:ebsi or did:key (422 EUDIW_INCOMPATIBLE_DID_METHOD); note that only did:key resolves offline today, so did:web and did:ebsi are currently refused rather than waved through.
POST {{baseUrl}}/api/v2/passports/{id}/validations
Authorization: Bearer {{apiKey}}
Content-Type: application/json
Idempotency-Key: {{uuid}}
{
"vc": {
"@context": ["https://www.w3.org/ns/credentials/v2"],
"type": ["VerifiableCredential", "ValidationResultCredential"],
"issuer": "did:web:validator.example.com",
"issuanceDate": "2026-07-29T00:00:00Z",
"credentialSubject": { "result": "pass" }
}
}curl -X POST "$BASE_URL/api/v2/passports/{id}/validations" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"vc": {
"@context": [
"https://www.w3.org/ns/credentials/v2"
],
"type": [
"VerifiableCredential",
"ValidationResultCredential"
],
"issuer": "did:web:validator.example.com",
"issuanceDate": "2026-07-29T00:00:00Z",
"credentialSubject": {
"result": "pass"
}
}
}'const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/passports/${id}/validations`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`,
"Idempotency-Key": `${crypto.randomUUID()}`
},
body: JSON.stringify({
"vc": {
"@context": [
"https://www.w3.org/ns/credentials/v2"
],
"type": [
"VerifiableCredential",
"ValidationResultCredential"
],
"issuer": "did:web:validator.example.com",
"issuanceDate": "2026-07-29T00:00:00Z",
"credentialSubject": {
"result": "pass"
}
}
})
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.post(
f"{base_url}/api/v2/passports/{id}/validations",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json",
"Idempotency-Key": f"{uuid.uuid4()}"
},
json={
"vc": {
"@context": [
"https://www.w3.org/ns/credentials/v2"
],
"type": [
"VerifiableCredential",
"ValidationResultCredential"
],
"issuer": "did:web:validator.example.com",
"issuanceDate": "2026-07-29T00:00:00Z",
"credentialSubject": {
"result": "pass"
}
}
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"bytes"
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/passports/%s/validations", baseURL, id)
payload := []byte(`{
"vc": {
"@context": [
"https://www.w3.org/ns/credentials/v2"
],
"type": [
"VerifiableCredential",
"ValidationResultCredential"
],
"issuer": "did:web:validator.example.com",
"issuanceDate": "2026-07-29T00:00:00Z",
"credentialSubject": {
"result": "pass"
}
}
}`)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "REPLACE-WITH-UUIDv4")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}201 Created. The digest is deterministic over the VC — re-anchoring the same VC converges on the same digest.
{ "upi": "…", "digest": "9f2c…", "anchoredAt": "2026-07-29T10:00:00.000Z" }The passport projection valid at a given instant (PRD F4). CURRENTLY 404s FOR EVERY INSTANT: publishing writes passport_versions but nothing writes passport_projections, which is the table this route reads, so the projection is empty even straight after a successful publish. Use GET /api/v2/passports/{id}/history for the version chain until the bi-temporal projector decision is ratified.
GET {{baseUrl}}/api/v2/passports/{id}/as-of
Authorization: Bearer {{apiKey}}curl -X GET "$BASE_URL/api/v2/passports/{id}/as-of" \
-H "Authorization: Bearer $NORRUVA_API_KEY"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/passports/${id}/as-of`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.get(
f"{base_url}/api/v2/passports/{id}/as-of",
headers={
"Authorization": f"Bearer {api_key}"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/passports/%s/as-of", baseURL, id)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}The command envelope
All post-publish state changes use one self-documenting envelope; GET /api/v2/passports/commands returns the catalog.
POST /api/v2/passports/commands
{ "type": "PASSPORT.SUSPEND", // dotted wire format: PASSPORT.SUBMIT | PASSPORT.APPROVE |
// PASSPORT.REJECT | PASSPORT.EDIT | PASSPORT.PUBLISH | PASSPORT.SUSPEND |
// PASSPORT.REINSTATE | PASSPORT.REVOKE | PASSPORT.ARCHIVE | PASSPORT.RECYCLE …
"tenantId": "<uuid>",
"idempotencyKey": "<uuid>",
"payload": { "passportId": "<uuid>", /* commandSpecificFields */ } }Guards are real: EDIT on a published passport → 403 GUARD_REJECTED. Replaying with the same idempotencyKey returns the first result. A field at the wrong level → 400 naming the field.