Developer Docs
Sandbox · API v2PlaygroundGet API keys

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: 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
Related
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

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
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
Related
KEYepcis:captureIdempotency-Key recommendedproduction: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
Emits events
print.confirmedprint.failed
Related
KEYepcis:capture

Capture an EPCIS 2.0 document. Idempotent per eventId; invalid events are quarantined. Label verification read-back also lands here (same 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
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
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.