Developer Docs
Sandbox · API v2PlaygroundGet API keys

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