Developer Docs
Sandbox · API v2PlaygroundGet API keys

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)
KEYdpp:editIdempotency-Key recommendedmint the key with dpp:edit; passport:write does not satisfy it

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.

Parameters
productIdrequiredbody · stringThe product to generate from (must be ready_for_dpp).
strictnessoptionalbody · stringe.g. "advisory".
regionoptionalbody · stringe.g. "EU".
lifecycleStageoptionalbody · stringe.g. "production".
dppOptionsoptionalbody · objectrequestId, manufacturerDid, anchorToBlockchain (ignored in sandbox, never anchors).
Request
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}}
}
Response

202 Accepted, poll /api/v2/dpp/status/{requestId}.

Errors you can branch on
Related
KEY

Async generation status for a dpp/generate request.

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}}
}
Related
KEYIdempotency-Key recommended

Mints the public passport content and an immutable version; returns {{passportUid}}. Publish ≠ register, the Digital Link registration is the separate publish-dpp call.

amendmentReason has a 10-character minimum (400 VALIDATION_ERROR below it). The product must already be dpp_issued, draft or archived, calling this straight after the ready_for_dpp transition, before POST /dpp/generate, is 422 INVALID_STATE_TRANSITION.
Request
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}}
}
Errors you can branch on
Emits events
product.published
Related
KEYproducts:editIdempotency-Key required

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; non-battery products publish normally.

Deviation: an out-of-scope call returns 403 FORBIDDEN, not 403 API_SCOPE_DENIED, a different auth wrapper. Branch on both.
Request
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}}
}
Errors you can branch on
Related
KEYpassports:publishmid_market tier and up

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.

Parameters
registriesoptionalbody · string[]Subset of CATENA_X | GS1 | EBSI, UPPER CASE; a lower-case value is a 400 VALIDATION_ERROR. Not yet honoured for selection: the router broadcasts to all configured registries.
passportDataoptionalbody · objectPayload override; omitted, the stored passport is used.
Request
POST {{baseUrl}}/api/v2/passports/{id}/publish
Authorization: Bearer {{apiKey}}
Content-Type: application/json
curl -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}}
}
Errors you can branch on
Related
KEYIdempotency-Key required

All post-publish state changes use one self-documenting envelope. Guards are real: EDIT on a published passport → 403 GUARD_REJECTED.

The command model keeps its OWN lifecycle (draft → submitted → approved → published), advanced ONLY by PASSPORT.* commands. POST /api/v2/dpp/{productId}/publish mints a passport version but does not move it, so a passport reached through the documented journey is still `draft` here and PASSPORT.SUSPEND is a correct 409 INVALID_TRANSITION. Drive SUBMIT → APPROVE → PUBLISH first; PASSPORT.PUBLISH additionally requires payload.disclosureTier (public | partner | internal). ⚠ THE TWO LIFECYCLES SHARE ONE STATE: sending PASSPORT.SUBMIT against a passport already published through the dpp/publish path moves it to `submitted`, and the public resolver reads that same state, GET /api/v2/public/passport/{uid} goes 200 → 404. Do not send SUBMIT to a live passport.
Parameters
typerequiredbody · stringDotted wire format: PASSPORT.SUBMIT | APPROVE | REJECT | EDIT | PUBLISH | SUSPEND | REINSTATE | REVOKE | ARCHIVE | RECYCLE …
tenantIdrequiredbody · uuidYour tenant id.
idempotencyKeyrequiredbody · uuidReplaying the same key returns the first result.
actorIdrequiredbody · uuidThe user id executing the command. Envelope metadata only, authorisation is decided from the authenticated credential, never from this field.
correlationIdrequiredbody · uuidRequest-tracing id.
requestedAtrequiredbody · ISO 8601When the command was requested.
causationIdoptionalbody · uuidId of the causing command/event.
payloadrequiredbody · objectpassportId + command-specific fields. A field at the wrong level → 400 naming the field.
Request
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}}
}
Errors you can branch on
Related
KEY

The self-documenting catalog of passport lifecycle commands.

Request
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}}
}
Related
KEY

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.

Request
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}}
}
Related
KEY

The anchored validation chain plus the derived readyToPrint flag (= validated ∧ published), the go/no-go signal before printing carriers.

Request
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}}
}
Response
JSON
{
  "upi": "…",
  "validations": [
    { "eventId": "…", "occurredAt": "…", "digest": "…",
      "signatureAlgorithm": "…", "jrcAnchor": "…" }
  ],
  "readyToPrint": true   // = at least one anchored validation AND currently published
}
Related
KEYdpp:editIdempotency-Key recommendedsupply_chain:edit also accepted

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.

Parameters
vcrequiredbody · objectW3C Verifiable Credential (@context, type, issuer, issuanceDate, credentialSubject).
signatureAlgorithmoptionalbody · stringDefaults to ECDSA-P-256.
Request
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}}
}
Response

201 Created. The digest is deterministic over the VC, re-anchoring the same VC converges on the same digest.

JSON
{ "upi": "…", "digest": "9f2c…", "anchoredAt": "2026-07-29T10:00:00.000Z" }
Errors you can branch on
Related
KEY

The passport projection valid at a given instant. CURRENTLY 404s FOR EVERY INSTANT: publishing does not populate the projection this route reads, so it 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.

Blocked on a reserved bi-temporal projector decision: the write-zone semantics are irreversible-if-wrong, so the projection is not being filled in ahead of ratification.
Parameters
daterequiredquery · ISO 8601The instant to project at.
Request
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}}
}
Errors you can branch on
Related

The command envelope

All post-publish state changes use one self-documenting envelope; GET /api/v2/passports/commands returns the catalog.

JSON
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.

Was this page helpful?
Thanks, noted.Feedback goes to the docs team by email.