Skip to main content
    Skip to content
    NorruvaDeveloper Docs
    Sandbox · verified 2026-07-29
    This page
    Whole docs
    Machine formats

    PlaygroundGet API keys
    IntroductionQuickstartAuthenticationErrors & conventionsSandbox & environments
    Entity modelProducts & categoriesPassports & versionsDigital Link & resolutionCompliance & regulationsWebhooks & eventsAutoID print loopImport jobsObservability & audit
    OverviewAuth & API keysProductsCompliance & regulationsPassportsResolver & publicWebhooksPrint jobs & devicesImport / bulkObservabilityBeyond happy path
    Integration playbookWebhook receiver guideRun a print deviceDeviations & gotchas
    EN 18222 API methodsAnnex ZA — ESPR correspondence
    Docs/API reference/Beyond happy path

    Beyond happy path

    These surfaces exist in the route tree but sit outside the core integration journey and are not yet E2E-verified. Treat them as previews and check status before building.

    Anchoring & credentials

    POST/api/v2/passports/{id}/anchorAnchor a passport
    GET/api/v2/blockchain/anchor-status/{requestId}Anchor transaction status
    POST/api/v2/credentials/issueIssue a credential
    POST/api/v2/credentials/verifyVerify a credential
    POST/api/v2/credentials/{id}/revokeRevoke a credential
    GET/api/v2/credentialsList credentials
    GET/api/v2/credentials/status/{listId}Credential status list
    POST/api/v2/credentials/commandsCredential command envelope
    KEYpassports:anchor

    Anchor a published passport on-chain. Sandbox never anchors: with ENABLE_BLOCKCHAIN=false the chain is unconfigured and the call returns 503 ANCHORING_UNAVAILABLE naming the network. A 500 ANCHORING_FAILED means a real anchoring fault, not an unconfigured environment — branch on the two separately.

    Request
    POST {{baseUrl}}/api/v2/passports/{id}/anchor
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/passports/{id}/anchor" \
      -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}/anchor`, {
      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}/anchor",
        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/anchor", 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
    503 ANCHORING_UNAVAILABLEANCHORING_FAILED404
    Related
    Sandbox & environments
    KEY

    Anchor tx status for an anchor request.

    Request
    GET {{baseUrl}}/api/v2/blockchain/anchor-status/{requestId}
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/blockchain/anchor-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/blockchain/anchor-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/blockchain/anchor-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/blockchain/anchor-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}}
    }
    KEYcompliance:manage

    Verifiable-credential issue. Scope compliance:manage — NOT credential:write. The credentials surface is split: issue/list/commands are gated on compliance:*, while verify/revoke use credential:*. The two families are not aliases of each other, so a key minted only with credential:write gets 403 here. See the Deviations page.

    Request
    POST {{baseUrl}}/api/v2/credentials/issue
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/credentials/issue" \
      -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 res = await fetch(`${baseUrl}/api/v2/credentials/issue`, {
      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"]
    
    resp = requests.post(
        f"{base_url}/api/v2/credentials/issue",
        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")
    	url := fmt.Sprintf("%s/api/v2/credentials/issue", baseURL)
    
    	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}}
    }
    KEYcredential:verify

    Verifiable-credential verify. Dual-mode: anonymous callers (no Authorization header) are allowed, but a presented Bearer key MUST carry credential:verify — a presented-but-unscoped key is 403, not silently anonymous. credential:verify is NOT implied by read/write.

    Request
    POST {{baseUrl}}/api/v2/credentials/verify
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/credentials/verify" \
      -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 res = await fetch(`${baseUrl}/api/v2/credentials/verify`, {
      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"]
    
    resp = requests.post(
        f"{base_url}/api/v2/credentials/verify",
        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")
    	url := fmt.Sprintf("%s/api/v2/credentials/verify", baseURL)
    
    	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}}
    }
    KEYcredential:write

    Verifiable-credential revoke.

    Request
    POST {{baseUrl}}/api/v2/credentials/{id}/revoke
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/credentials/{id}/revoke" \
      -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/credentials/${id}/revoke`, {
      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/credentials/{id}/revoke",
        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/credentials/%s/revoke", 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}}
    }
    KEYcompliance:view

    Credential list. Scope compliance:view — NOT credential:read (see /credentials/issue).

    Request
    GET {{baseUrl}}/api/v2/credentials
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/credentials" \
      -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/credentials`, {
      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/credentials",
        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/credentials", 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}}
    }
    PUBLIC

    StatusList2021 revocation-status read. Unauthenticated by design — a StatusList2021 list must be dereferenceable by any verifier holding the credential, per the spec.

    Request
    GET {{baseUrl}}/api/v2/credentials/status/{listId}
    
    curl -X GET "$BASE_URL/api/v2/credentials/status/{listId}"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const listId = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/credentials/status/${listId}`, {
      method: "GET",
      headers: {
    
      }
    });
    
    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"]
    list_id = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/credentials/status/{list_id}",
        headers={
    
        },
    )
    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")
    	listId := "…"
    	url := fmt.Sprintf("%s/api/v2/credentials/status/%s", baseURL, listId)
    
    	req, _ := http.NewRequest("GET", url, nil)
    
    	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}}
    }
    KEYcompliance:view

    Mirror of the passport command envelope for credentials. Base scope compliance:view gates the envelope; each command then demands its own scope (compliance:manage / compliance:edit / compliance:view) inside the handler, so a compliance:view key can open the envelope but be refused the command.

    Request
    POST {{baseUrl}}/api/v2/credentials/commands
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/credentials/commands" \
      -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 res = await fetch(`${baseUrl}/api/v2/credentials/commands`, {
      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"]
    
    resp = requests.post(
        f"{base_url}/api/v2/credentials/commands",
        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")
    	url := fmt.Sprintf("%s/api/v2/credentials/commands", baseURL)
    
    	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}}
    }
    Related
    Passports

    Scope families on the credentials surface are split, and they are not aliases. /credentials/issue, GET /credentials and /credentials/commands are gated on compliance:*; /credentials/verify and /credentials/{id}/revoke are gated on credential:*. The alias table resolves plural forms (credentials:read → credential:read) and credential:manage → credential:write, but it does not bridge credential:* and compliance:*. A key minted only with credential:read/credential:write therefore gets 403 API_SCOPE_DENIED on the compliance:* routes above. Mint keys covering both families until this is reconciled.

    Auto-ID · registry · customs

    POST/api/v2/carriers/batchAllocate carrier serials
    GET/api/v2/carriers/exportExport the print CSV
    POST/api/v2/carriers/print-statusReport print outcomes
    POST/api/v2/epcis/captureCapture EPCIS events
    POST/api/v2/registry/submitEU registry submission
    GET/api/v2/customs/dpp-checkCustoms DPP check
    KEYproduction:createIdempotency-Key recommended

    Allocate up to 10k numeric, SGTIN-96-safe serials for a product. Fires carrier.generated so subscribers pull instead of poll.

    Request
    POST {{baseUrl}}/api/v2/carriers/batch
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    Idempotency-Key: {{uuid}}
    
    { "productId": "{{productId}}", "count": 1000 }
    curl -X POST "$BASE_URL/api/v2/carriers/batch" \
      -H "Authorization: Bearer $NORRUVA_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
      "productId": "{{productId}}",
      "count": 1000
    }'
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    
    const res = await fetch(`${baseUrl}/api/v2/carriers/batch`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": `application/json`,
        "Idempotency-Key": `${crypto.randomUUID()}`
      },
      body: JSON.stringify({
        "productId": "{{productId}}",
        "count": 1000
      })
    });
    
    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/carriers/batch",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": f"application/json",
            "Idempotency-Key": f"{uuid.uuid4()}"
        },
        json={
          "productId": "{{productId}}",
          "count": 1000
        },
    )
    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/carriers/batch", baseURL)
    
    	payload := []byte(`{
      "productId": "{{productId}}",
      "count": 1000
    }`)
    	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 — webhook carrier.generated carries exportPath; no polling.

    JSON
    { "allocated": 1000, "serials": ["100001", "100002", "…"] }
    Emits events
    carrier.generated
    Related
    AutoID print loop
    KEYproduction:viewidentifiers:read satisfies it

    The print contract (CSV or format=json): gtin, serial, digital_link, sgtin96_epc. digital_link IS the QR payload; sgtin96_epc feeds the RFID encoder. printState=failed is exactly the re-print file.

    Parameters
    productIdrequiredquery · stringProduct whose carriers to export.
    printStateoptionalquery · stringprinted | failed | pending — narrows to the reported outcome; an unknown value refuses with 422.
    formatoptionalquery · stringcsv (default) or json.
    Request
    GET {{baseUrl}}/api/v2/carriers/export
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/carriers/export" \
      -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/carriers/export`, {
      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/carriers/export",
        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/carriers/export", 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}}
    }
    Errors you can branch on
    422
    Related
    AutoID print loop
    KEYepcis:captureIdempotency-Key recommendedthe TSC key — production:create also accepted

    Per-serial outcomes from the print layer, ≤ 1000 per call. Outcomes land on metadata.print (last write wins); unknown serials come back in unknown[] with a 200; fires one aggregate print.confirmed and/or print.failed per accepted request. Retries replay rather than re-execute.

    Request
    POST {{baseUrl}}/api/v2/carriers/print-status
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    Idempotency-Key: {{uuid}}
    
    {
      "results": [
        { "serial": "100001", "status": "printed", "deviceId": "PEX-2000-01" },
        { "serial": "100002", "status": "failed",  "reason": "ribbon jam" }
      ]
    }
    curl -X POST "$BASE_URL/api/v2/carriers/print-status" \
      -H "Authorization: Bearer $NORRUVA_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
      "results": [
        {
          "serial": "100001",
          "status": "printed",
          "deviceId": "PEX-2000-01"
        },
        {
          "serial": "100002",
          "status": "failed",
          "reason": "ribbon jam"
        }
      ]
    }'
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    
    const res = await fetch(`${baseUrl}/api/v2/carriers/print-status`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": `application/json`,
        "Idempotency-Key": `${crypto.randomUUID()}`
      },
      body: JSON.stringify({
        "results": [
          {
            "serial": "100001",
            "status": "printed",
            "deviceId": "PEX-2000-01"
          },
          {
            "serial": "100002",
            "status": "failed",
            "reason": "ribbon jam"
          }
        ]
      })
    });
    
    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/carriers/print-status",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": f"application/json",
            "Idempotency-Key": f"{uuid.uuid4()}"
        },
        json={
          "results": [
            {
              "serial": "100001",
              "status": "printed",
              "deviceId": "PEX-2000-01"
            },
            {
              "serial": "100002",
              "status": "failed",
              "reason": "ribbon jam"
            }
          ]
        },
    )
    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/carriers/print-status", baseURL)
    
    	payload := []byte(`{
      "results": [
        {
          "serial": "100001",
          "status": "printed",
          "deviceId": "PEX-2000-01"
        },
        {
          "serial": "100002",
          "status": "failed",
          "reason": "ribbon jam"
        }
      ]
    }`)
    	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

    Retries are REPLAYED, not re-executed. Print controllers rarely send an Idempotency-Key, so one is derived from the batch itself: re-posting an identical batch returns the first response and dispatches no further webhooks — the events[] counts in a replay describe the original call, not the retry. A batch carrying different outcomes hashes to a different key and executes normally. If you do send your own Idempotency-Key it takes precedence over the derived one, so reusing a key with a changed body is a 409.

    JSON
    {
      "updated": ["100001", "100002"], "unknown": [],
      "printed": ["100001"], "failed": ["100002"],
      "reportedAt": "2026-07-22T17:20:00.000Z",
      "events": [
        { "type": "print.confirmed", "webhooksNotified": 1 },
        { "type": "print.failed",    "webhooksNotified": 1 }
      ]
    }
    Errors you can branch on
    400 VALIDATION_ERROR403 API_SCOPE_DENIED409
    Emits events
    print.confirmedprint.failed
    Related
    AutoID print loopWebhooks & events
    KEYepcis:capture

    Capture an EPCIS 2.0 document. Idempotent per eventId; invalid events are quarantined. Label verification read-back also lands here (same TSC key).

    Request
    POST {{baseUrl}}/api/v2/epcis/capture
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/epcis/capture" \
      -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 res = await fetch(`${baseUrl}/api/v2/epcis/capture`, {
      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"]
    
    resp = requests.post(
        f"{base_url}/api/v2/epcis/capture",
        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")
    	url := fmt.Sprintf("%s/api/v2/epcis/capture", baseURL)
    
    	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}}
    }
    Related
    AutoID print loop
    KEY

    Submit to the EU registry.

    Request
    POST {{baseUrl}}/api/v2/registry/submit
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/registry/submit" \
      -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 res = await fetch(`${baseUrl}/api/v2/registry/submit`, {
      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"]
    
    resp = requests.post(
        f"{base_url}/api/v2/registry/submit",
        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")
    	url := fmt.Sprintf("%s/api/v2/registry/submit", baseURL)
    
    	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}}
    }
    Related
    Passports & versions
    KEYproducts:customs_inspect

    Customs check surface. GET with required query params uid + eori. Gated on products:customs_inspect (NOT a generic dpp:read key) so arbitrary passport UIDs cannot be probed across tenants.

    Request
    GET {{baseUrl}}/api/v2/customs/dpp-check
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/customs/dpp-check" \
      -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/customs/dpp-check`, {
      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/customs/dpp-check",
        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/customs/dpp-check", 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}}
    }
    Was this page helpful?
    Thanks — noted.Feedback goes to the docs team by email.
    ← PreviousObservabilityNext →Integration playbook
    On this page
    Anchoring & credentialsAuto-ID · registry · customsPOST /passports/{id}/anchorGET /blockchain/anchor-status/{requestId}POST /credentials/issuePOST /credentials/verifyPOST /credentials/{id}/revokeGET /credentialsGET /credentials/status/{listId}POST /credentials/commandsPOST /carriers/batchGET /carriers/exportPOST /carriers/print-statusPOST /epcis/capturePOST /registry/submitGET /customs/dpp-check
    Norruva DPP API · sandbox developer docsGenerated 2026-07-29 · PRD-aligned (TSC roadmap rev 2) · statuses reflect E2E-verified sandbox behaviour — not marketing