Developer Docs
Sandbox · API v2PlaygroundGet API keys

Products

Create and manage products and advance them toward passport generation. Update with PATCH (canonical) or PUT (alias), identical semantics.

GET/api/v2/productsList products
POST/api/v2/productsCreate a product
GET/api/v2/products/{id}Get a product
PATCH/api/v2/products/{id}Update a product
DELETE/api/v2/products/{id}Delete a product
POST/api/v2/products/batchBatch create products
GET/api/v2/products/{id}/versionsProduct versions
POST/api/v2/products/{id}/lifecycle/transitionAdvance product lifecycle
GET/api/v2/products/{id}/eventsProduct domain events
POST/api/v2/products/{id}/voidVoid a serialized item
GET/api/v2/products/categoriesTenant categories
GET/api/v2/schemas/categoriesSchema catalog
GET/api/v2/schemas/categories/{category}Effective category schema
GET/api/v2/schemas/categories/{category}/import-templateCategory import template
KEYproducts:readproducts:view is an accepted legacy alias

List the tenant's products.

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

Create a tenant-owned commercial item. Creating a product does not make it public and does not register a resolvable identifier.

A second model-granularity product with the same GTIN in the same tenant is a 409 conflict, (company, gtin) is unique.
Parameters
namerequiredbody · stringProduct name.
descriptionrequiredbody · stringMust be ≥ 10 characters, else 400 VALIDATION_ERROR.
categoryrequiredbody · stringResolves to an effective schema that governs extensions. No effective schema → 422 SCHEMA_NOT_FOUND.
gtinrequiredbody · stringCheck-digit-validated; a valid GTIN-8/12/13 is normalized to 14 digits at write time.
serialNumberrequiredbody · string≤ 20 chars (SGTIN-96 bound). Numeric serials must fit ≤ 2^38 for EPC carrier generation.
extensionsrequiredbody · objectCategory-specific fields (electronics ≈ 8, battery ≈ 30). Missing → 422 EXTENSIONS_REQUIRED.
Request
POST {{baseUrl}}/api/v2/products
Authorization: Bearer {{apiKey}}
Content-Type: application/json
Idempotency-Key: {{uuid}}

{
  // electronics, not battery: battery compliance is quarantined,
  // so a battery product cannot complete validate → publish-dpp → resolve today.
  "name": "EcoCell Charger XR-2024",
  "description": "Fast charger with certified recycled content",
  "category": "electronics",
  "gtin": "04012345000016",
  "serialNumber": "SN-ELEC-2024-000001",
  "extensions": {
    "energyEfficiencyClass": "A",
    "annualEnergyConsumptionKwh": 42,
    "repairabilityScore": 8.5,
    "sparePartsAvailabilityYears": 10,
    "hazardousSubstances": [],
    "recycledContentPercentage": 35,
    "productLifetimeYears": 12,
    // enum, NOT a WEEE number: temperature_exchange | screens_monitors | lamps
    //  | large_equipment | small_equipment | small_it_telecom
    "weeeCategory": "small_it_telecom"
  }
}
curl -X POST "$BASE_URL/api/v2/products" \
  -H "Authorization: Bearer $NORRUVA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "name": "EcoCell Charger XR-2024",
  "description": "Fast charger with certified recycled content",
  "category": "electronics",
  "gtin": "04012345000016",
  "serialNumber": "SN-ELEC-2024-000001",
  "extensions": {
    "energyEfficiencyClass": "A",
    "annualEnergyConsumptionKwh": 42,
    "repairabilityScore": 8.5,
    "sparePartsAvailabilityYears": 10,
    "hazardousSubstances": [],
    "recycledContentPercentage": 35,
    "productLifetimeYears": 12,
    "weeeCategory": "small_it_telecom"
  }
}'
const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;

const res = await fetch(`${baseUrl}/api/v2/products`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": `application/json`,
    "Idempotency-Key": `${crypto.randomUUID()}`
  },
  body: JSON.stringify({
    "name": "EcoCell Charger XR-2024",
    "description": "Fast charger with certified recycled content",
    "category": "electronics",
    "gtin": "04012345000016",
    "serialNumber": "SN-ELEC-2024-000001",
    "extensions": {
      "energyEfficiencyClass": "A",
      "annualEnergyConsumptionKwh": 42,
      "repairabilityScore": 8.5,
      "sparePartsAvailabilityYears": 10,
      "hazardousSubstances": [],
      "recycledContentPercentage": 35,
      "productLifetimeYears": 12,
      "weeeCategory": "small_it_telecom"
    }
  })
});

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/products",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": f"application/json",
        "Idempotency-Key": f"{uuid.uuid4()}"
    },
    json={
      "name": "EcoCell Charger XR-2024",
      "description": "Fast charger with certified recycled content",
      "category": "electronics",
      "gtin": "04012345000016",
      "serialNumber": "SN-ELEC-2024-000001",
      "extensions": {
        "energyEfficiencyClass": "A",
        "annualEnergyConsumptionKwh": 42,
        "repairabilityScore": 8.5,
        "sparePartsAvailabilityYears": 10,
        "hazardousSubstances": [],
        "recycledContentPercentage": 35,
        "productLifetimeYears": 12,
        "weeeCategory": "small_it_telecom"
      }
    },
)
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/products", baseURL)

	payload := []byte(`{
  "name": "EcoCell Charger XR-2024",
  "description": "Fast charger with certified recycled content",
  "category": "electronics",
  "gtin": "04012345000016",
  "serialNumber": "SN-ELEC-2024-000001",
  "extensions": {
    "energyEfficiencyClass": "A",
    "annualEnergyConsumptionKwh": 42,
    "repairabilityScore": 8.5,
    "sparePartsAvailabilityYears": 10,
    "hazardousSubstances": [],
    "recycledContentPercentage": 35,
    "productLifetimeYears": 12,
    "weeeCategory": "small_it_telecom"
  }
}`)
	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 product is returned UNWRAPPED; capture id (not data.id). GET /api/v2/products does use a data envelope, and the write path renames name → productName.

JSON
{
  "id": "…uuid…",              // capture as {{productId}}, NOT under a "data" envelope
  "productName": "EcoCell Battery Pack XR-2024",   // request field is "name"
  "productDescription": "…",   // request field is "description"
  "category": "battery",
  "status": "Draft",
  "gtin": "04012345000016"
}
Errors you can branch on
Emits events
product.created
Related
KEYproducts:read

Read one product. Cross-tenant reads fail closed with 404, authz runs before existence.

Request
GET {{baseUrl}}/api/v2/products/{id}
Authorization: Bearer {{apiKey}}
curl -X GET "$BASE_URL/api/v2/products/{id}" \
  -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/products/${id}`, {
  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/products/{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")
	id := "…"
	url := fmt.Sprintf("%s/api/v2/products/%s", 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
KEYproducts:edit

Update a product. PATCH is canonical; PUT is an accepted alias, both run the same full-update handler.

Request
PATCH {{baseUrl}}/api/v2/products/{id}
Authorization: Bearer {{apiKey}}
Content-Type: application/json
curl -X PATCH "$BASE_URL/api/v2/products/{id}" \
  -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/products/${id}`, {
  method: "PATCH",
  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.patch(
    f"{base_url}/api/v2/products/{id}",
    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/products/%s", baseURL, id)

	req, _ := http.NewRequest("PATCH", 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
Emits events
product.updated
Related
KEYproducts:delete

Hard-delete a product that has no retained print-loop history. A product whose serials ever entered the AIDC print loop cannot be deleted, what physically printed is retained evidence, and answers 409 PRODUCT_REFERENCED; void it instead (POST /products/{id}/void), which is its terminal state.

Request
DELETE {{baseUrl}}/api/v2/products/{id}
Authorization: Bearer {{apiKey}}
curl -X DELETE "$BASE_URL/api/v2/products/{id}" \
  -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/products/${id}`, {
  method: "DELETE",
  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.delete(
    f"{base_url}/api/v2/products/{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")
	id := "…"
	url := fmt.Sprintf("%s/api/v2/products/%s", baseURL, id)

	req, _ := http.NewRequest("DELETE", 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

204 No Content. 409 PRODUCT_REFERENCED when print jobs still reference one of the product's serials, the product stays addressable and voidable.

Errors you can branch on
Emits events
product.deleted
Related
KEYproducts:createIdempotency-Key recommended

Create 1-1000 products in one call, same item contract as single create, per-index error isolation.

Request
POST {{baseUrl}}/api/v2/products/batch
Authorization: Bearer {{apiKey}}
Content-Type: application/json
Idempotency-Key: {{uuid}}
curl -X POST "$BASE_URL/api/v2/products/batch" \
  -H "Authorization: Bearer $NORRUVA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)"
const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;

const res = await fetch(`${baseUrl}/api/v2/products/batch`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": `application/json`,
    "Idempotency-Key": `${crypto.randomUUID()}`
  }
});

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/products/batch",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": f"application/json",
        "Idempotency-Key": f"{uuid.uuid4()}"
    },
)
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/products/batch", baseURL)

	req, _ := http.NewRequest("POST", url, nil)
	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

200, per-index isolation: good rows commit even when others fail. Each errors[] item is { index, error } with the failure code prefixed into the error string.

JSON
{
  "created": 998,
  "failed": 2,
  "errors": [ { "index": 14, "error": "VALIDATION_ERROR: productDescription: Description must be at least 10 characters." } ]
}
Errors you can branch on
Related
KEYproducts:read

Product version list.

Request
GET {{baseUrl}}/api/v2/products/{id}/versions
Authorization: Bearer {{apiKey}}
curl -X GET "$BASE_URL/api/v2/products/{id}/versions" \
  -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/products/${id}/versions`, {
  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/products/{id}/versions",
    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/products/%s/versions", 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
KEYproducts:editIdempotency-Key recommended

Move draft → validated → ready_for_dpp. Two transitions are required before passport generation. Illegal transitions are typed errors, not silent no-ops.

Parameters
stagerequiredbody · stringTarget stage, e.g. "validated" then "ready_for_dpp".
reasonoptionalbody · stringRecorded on the transition event.
Request
POST {{baseUrl}}/api/v2/products/{id}/lifecycle/transition
Authorization: Bearer {{apiKey}}
Content-Type: application/json
Idempotency-Key: {{uuid}}

{
  "stage": "validated",
  "reason": "Schema validation passed"
}
curl -X POST "$BASE_URL/api/v2/products/{id}/lifecycle/transition" \
  -H "Authorization: Bearer $NORRUVA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "stage": "validated",
  "reason": "Schema validation passed"
}'
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}/lifecycle/transition`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": `application/json`,
    "Idempotency-Key": `${crypto.randomUUID()}`
  },
  body: JSON.stringify({
    "stage": "validated",
    "reason": "Schema validation passed"
  })
});

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}/lifecycle/transition",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": f"application/json",
        "Idempotency-Key": f"{uuid.uuid4()}"
    },
    json={
      "stage": "validated",
      "reason": "Schema validation passed"
    },
)
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/lifecycle/transition", baseURL, id)

	payload := []byte(`{
  "stage": "validated",
  "reason": "Schema validation passed"
}`)
	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.updated
Related
KEYproducts:read

Domain events for one product, the per-product observability surface.

Request
GET {{baseUrl}}/api/v2/products/{id}/events
Authorization: Bearer {{apiKey}}
curl -X GET "$BASE_URL/api/v2/products/{id}/events" \
  -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/products/${id}/events`, {
  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/products/{id}/events",
    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/products/%s/events", 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
KEYproducts:edit

Void a serialized item (reason required). A voided serial resolves as 410 SERIAL_VOIDED with a tombstone and zero passport content. Beyond the core journey.

Parameters
reasonrequiredbody · stringWhy the item is voided, recorded and audited.
Request
POST {{baseUrl}}/api/v2/products/{id}/void
Authorization: Bearer {{apiKey}}
Content-Type: application/json
curl -X POST "$BASE_URL/api/v2/products/{id}/void" \
  -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/products/${id}/void`, {
  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/products/{id}/void",
    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/products/%s/void", 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
KEYproducts:read

The tenant's own categories with usage counts.

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

Every category with its schema versions, jurisdiction, effective window, and effectiveNow flag. Works with a read-only key.

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

The schema effective at an instant for a jurisdiction, exactly what creates and imports validate against. Links straight to the category's CSV import template.

A miss returns a typed 404 SCHEMA_NOT_FOUND listing availableCategories.
Parameters
categoryrequiredpath · stringCategory key, e.g. battery.
jurisdictionoptionalquery · stringe.g. "EU".
dateoptionalquery · ISO 8601Point-in-time resolution instant.
includeFieldsoptionalquery · booleanfalse skips the field definitions.
Request
GET {{baseUrl}}/api/v2/schemas/categories/{category}
Authorization: Bearer {{apiKey}}
curl -X GET "$BASE_URL/api/v2/schemas/categories/{category}" \
  -H "Authorization: Bearer $NORRUVA_API_KEY"
const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const category = "…";

const res = await fetch(`${baseUrl}/api/v2/schemas/categories/${category}`, {
  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"]
category = "…"

resp = requests.get(
    f"{base_url}/api/v2/schemas/categories/{category}",
    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")
	category := "…"
	url := fmt.Sprintf("%s/api/v2/schemas/categories/%s", baseURL, category)

	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
{
  "schemaId": "…", "schemaRegistryId": "…", "version": "…",
  "effectiveFrom": "…", "effectiveUntil": null,
  "fieldSchema": { /* full field definitions */ },
  "_links": { "importTemplate": "/api/v2/schemas/categories/battery/import-template?jurisdiction=EU" }
}
Errors you can branch on
Related
KEYproducts:read

text/csv template rendered from that SAME effective schema, the columns round-trip by construction.

Parameters
jurisdictionoptionalquery · stringe.g. "EU".
Request
GET {{baseUrl}}/api/v2/schemas/categories/{category}/import-template
Authorization: Bearer {{apiKey}}
curl -X GET "$BASE_URL/api/v2/schemas/categories/{category}/import-template" \
  -H "Authorization: Bearer $NORRUVA_API_KEY"
const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const category = "…";

const res = await fetch(`${baseUrl}/api/v2/schemas/categories/${category}/import-template`, {
  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"]
category = "…"

resp = requests.get(
    f"{base_url}/api/v2/schemas/categories/{category}/import-template",
    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")
	category := "…"
	url := fmt.Sprintf("%s/api/v2/schemas/categories/%s/import-template", baseURL, category)

	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
Was this page helpful?
Thanks, noted.Feedback goes to the docs team by email.