Products
Create and manage products and advance them toward passport generation. Update with PATCH (canonical, PRD F1) 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 |
List the tenant's products.
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}}
}Create a tenant-owned commercial item. Creating a product does not make it public and does not register a resolvable identifier.
POST {{baseUrl}}/api/v2/products
Authorization: Bearer {{apiKey}}
Content-Type: application/json
Idempotency-Key: {{uuid}}
{
// electronics, not battery: battery compliance is quarantined (ATLAS-F017),
// 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}}
}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.
{
"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"
}Read one product. Cross-tenant reads fail closed with 404 — authz runs before existence.
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}}
}Update a product. PATCH is canonical (PRD F1); PUT is an accepted alias — both run the same full-update handler.
PATCH {{baseUrl}}/api/v2/products/{id}
Authorization: Bearer {{apiKey}}
Content-Type: application/jsoncurl -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}}
}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.
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}}
}204 No Content. 409 PRODUCT_REFERENCED when print_jobs still reference one of the product's serials — the product stays addressable and voidable.
Create 1–1000 products in one call — same item contract as single create, per-index error isolation.
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}}
}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.
{
"created": 998,
"failed": 2,
"errors": [ { "index": 14, "error": "VALIDATION_ERROR: productDescription: Description must be at least 10 characters." } ]
}Product version list (PRD F1).
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}}
}Move draft → validated → ready_for_dpp. Two transitions are required before passport generation. Illegal transitions are typed errors, not silent no-ops.
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}}
}Domain events for one product — the per-product observability surface.
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}}
}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.
POST {{baseUrl}}/api/v2/products/{id}/void
Authorization: Bearer {{apiKey}}
Content-Type: application/jsoncurl -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}}
}The tenant's own categories with usage counts.
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}}
}Every category with its schema versions, jurisdiction, effective window, and effectiveNow flag. Works with the read-only TSC key.
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}}
}The schema effective at an instant for a jurisdiction — exactly what creates and imports validate against (the same T101 resolution the create path runs). Links straight to the category's CSV import template.
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}}
}{
"schemaId": "…", "schemaRegistryId": "…", "version": "…",
"effectiveFrom": "…", "effectiveUntil": null,
"fieldSchema": { /* full field definitions */ },
"_links": { "importTemplate": "/api/v2/schemas/categories/battery/import-template?jurisdiction=EU" }
}text/csv template rendered from that SAME effective schema — the columns round-trip by construction.
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}}
}