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/Resolver & public

    Resolver & public

    How the outside world reaches a published passport. See Digital Link & resolution for audience tiers and the two documented deviations.

    GET/api/v2/01/{gtin}/21/{serial}Resolve a Digital Link
    GET/api/v2/public/gs1/{…segments}Machine resolver (GS1 DL)
    GET/api/v2/.well-known/gs1resolverResolver discovery document
    GET/api/v2/public/passport/{uid}Public passport JSON
    GET/api/v2/public/passport/{uid}/verifyVerify a public passport
    GET/api/v2/public/openapi.jsonPublic OpenAPI spec
    HEADER-SHAPEDheader-shaped — see deviation

    GS1 01/21 resolution → 302 to /p/{uid} (the public uid = the product id), measured 0.16s. Content-negotiated linkset via Accept: application/linkset+json (RFC 9264).

    ▲
    Deviation: currently returns 401 when no Authorization header is present — send Authorization: Bearer edge-gate-placeholder until fixed.
    Parameters
    gtinrequiredpath · stringGS1 application identifier 01 — the GTIN.
    serialrequiredpath · stringGS1 application identifier 21 — the serial.
    Request
    GET {{baseUrl}}/api/v2/01/{gtin}/21/{serial}
    Authorization: Bearer edge-gate-placeholder
    curl -X GET "$BASE_URL/api/v2/01/{gtin}/21/{serial}" \
      -H "Authorization: Bearer edge-gate-placeholder"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const gtin = "…";
    const serial = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/01/${gtin}/21/${serial}`, {
      method: "GET",
      headers: {
        "Authorization": `Bearer edge-gate-placeholder`
      }
    });
    
    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"]
    gtin = "…"
    serial = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/01/{gtin}/21/{serial}",
        headers={
            "Authorization": f"Bearer edge-gate-placeholder"
        },
    )
    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")
    	gtin := "…"
    	serial := "…"
    	url := fmt.Sprintf("%s/api/v2/01/%s/21/%s", baseURL, gtin, serial)
    
    	req, _ := http.NewRequest("GET", url, nil)
    	req.Header.Set("Authorization", "Bearer edge-gate-placeholder")
    
    	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
    400 INVALID_GTIN400 INVALID_SERIAL404 PRODUCT_NOT_FOUND410 SERIAL_VOIDED401
    Related
    Digital Link & resolutionDeviations & gotchas
    PUBLIC

    Catch-all GS1 Digital Link segments: browsers → 302 /p/{uid}; machine Accept → JSON linkset.

    Request
    GET {{baseUrl}}/api/v2/public/gs1/{…segments}
    
    curl -X GET "$BASE_URL/api/v2/public/gs1/{…segments}"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const segments = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/public/gs1/${segments}`, {
      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"]
    segments = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/public/gs1/{segments}",
        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")
    	segments := "…"
    	url := fmt.Sprintf("%s/api/v2/public/gs1/%s", baseURL, segments)
    
    	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}}
    }
    Related
    Digital Link & resolution
    PUBLIC

    GS1 resolver discovery: supported keys, qualifiers, link types.

    Request
    GET {{baseUrl}}/api/v2/.well-known/gs1resolver
    
    curl -X GET "$BASE_URL/api/v2/.well-known/gs1resolver"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    
    const res = await fetch(`${baseUrl}/api/v2/.well-known/gs1resolver`, {
      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"]
    
    resp = requests.get(
        f"{base_url}/api/v2/.well-known/gs1resolver",
        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")
    	url := fmt.Sprintf("%s/api/v2/.well-known/gs1resolver", baseURL)
    
    	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}}
    }
    Related
    Digital Link & resolution
    PUBLIC

    Audience-filtered passport JSON. Anonymous callers get the consumer minimum; only a cryptographically verified X-DPP-Access-Token elevates disclosure. Every public read is logged with its audience.

    ▲
    Draft passports are publicly indistinguishable from nonexistent (404 "not found or not published").
    RequestTry it
    GET {{baseUrl}}/api/v2/public/passport/{uid}
    
    curl -X GET "$BASE_URL/api/v2/public/passport/{uid}"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const uid = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/public/passport/${uid}`, {
      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"]
    uid = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/public/passport/{uid}",
        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")
    	uid := "…"
    	url := fmt.Sprintf("%s/api/v2/public/passport/%s", baseURL, uid)
    
    	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}}
    }
    Errors you can branch on
    404
    Related
    Digital Link & resolution
    PUBLIC

    Signature + anchor verification for a published passport.

    RequestTry it
    GET {{baseUrl}}/api/v2/public/passport/{uid}/verify
    
    curl -X GET "$BASE_URL/api/v2/public/passport/{uid}/verify"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const uid = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/public/passport/${uid}/verify`, {
      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"]
    uid = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/public/passport/{uid}/verify",
        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")
    	uid := "…"
    	url := fmt.Sprintf("%s/api/v2/public/passport/%s/verify", baseURL, uid)
    
    	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}}
    }
    Related
    Digital Link & resolution
    PUBLIC

    Public machine-readable spec — pull it for discovery in your smoke tests.

    RequestTry it
    GET {{baseUrl}}/api/v2/public/openapi.json
    
    curl -X GET "$BASE_URL/api/v2/public/openapi.json"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    
    const res = await fetch(`${baseUrl}/api/v2/public/openapi.json`, {
      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"]
    
    resp = requests.get(
        f"{base_url}/api/v2/public/openapi.json",
        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")
    	url := fmt.Sprintf("%s/api/v2/public/openapi.json", baseURL)
    
    	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}}
    }
    Related
    Integration playbook
    ▲
    Deviation. The resolver returns 401 to anonymous scanners sending no Authorization header; send Authorization: Bearer edge-gate-placeholder until fixed. Draft passports return 404 "not found or not published".
    Was this page helpful?
    Thanks — noted.Feedback goes to the docs team by email.
    ← PreviousPassportsNext →Webhooks
    On this page
    GET /01/{gtin}/21/{serial}GET /public/gs1/{…segments}GET /.well-known/gs1resolverGET /public/passport/{uid}GET /public/passport/{uid}/verifyGET /public/openapi.json
    Norruva DPP API · sandbox developer docsGenerated 2026-07-29 · PRD-aligned (TSC roadmap rev 2) · statuses reflect E2E-verified sandbox behaviour — not marketing