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/Observability

    Observability

    See what your integration did and prove it. Correlate with X-Correlation-Id — see Observability & audit.

    GET/api/v2/audit-logsAudit logs
    GET/api/v2/metricsMetrics counters
    GET/api/v2/metrics/prometheusPrometheus exposition
    POST/api/v2/audit/chain/verifyVerify the audit chain
    GET/api/v2/openapi.jsonAuthenticated OpenAPI spec
    KEY

    Security-relevant actions in typed classes. Correlate with X-Correlation-Id — it shows up as requestId.

    Request
    GET {{baseUrl}}/api/v2/audit-logs
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/audit-logs" \
      -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/audit-logs`, {
      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/audit-logs",
        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/audit-logs", 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
    Observability & audit
    OPS TOKENHEALTHCHECK_TOKEN (min 32 chars) for JSON mode, METRICS_SCRAPE_TOKEN for ?format=prometheus — both sent as Authorization: Bearer

    Two modes on one path. Default (no query) returns the JSON business-metrics summary and needs HEALTHCHECK_TOKEN; ?format=prometheus returns text exposition and needs METRICS_SCRAPE_TOKEN. Both as Authorization: Bearer. Tenant API keys get 401 by design.

    ▲
    X-Health-Token corrected out 2026-08-09: HealthCheckAuthService does read that header, but /api/v2/** is edge-authenticated and the edge recognises only x-api-key or Authorization: Bearer — so a request carrying X-Health-Token alone answers 401 before the route runs. A HEALTHCHECK_TOKEN shorter than 32 characters is rejected as "configured but invalid" and every call 401s, including one carrying the right token; .env.local.example ships a compliant value. In ?format=prometheus mode, METRICS_SCRAPE_TOKEN unset means 503 outside development (fail-closed).
    Request
    GET {{baseUrl}}/api/v2/metrics
    Authorization: Bearer {{apiKey}}
    Authorization: Bearer {{opsToken}}
    curl -X GET "$BASE_URL/api/v2/metrics" \
      -H "Authorization: Bearer $NORRUVA_API_KEY" \
      -H "Authorization: Bearer $NORRUVA_OPS_TOKEN"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    
    const res = await fetch(`${baseUrl}/api/v2/metrics`, {
      method: "GET",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "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/metrics",
        headers={
            "Authorization": f"Bearer {api_key}",
            "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/metrics", baseURL)
    
    	req, _ := http.NewRequest("GET", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    	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
    401503
    Related
    Observability & audit
    OPS TOKENMETRICS_SCRAPE_TOKEN, sent as Authorization: Bearer. A collector that cannot set that header cannot scrape this path.

    Prometheus text exposition, 80+ KPIs (PRD F14) — the scrape target for Prometheus/Datadog agents. Edge-authenticated: every scrape MUST send Authorization: Bearer <METRICS_SCRAPE_TOKEN>.

    Request
    GET {{baseUrl}}/api/v2/metrics/prometheus
    Authorization: Bearer {{apiKey}}
    Authorization: Bearer {{opsToken}}
    curl -X GET "$BASE_URL/api/v2/metrics/prometheus" \
      -H "Authorization: Bearer $NORRUVA_API_KEY" \
      -H "Authorization: Bearer $NORRUVA_OPS_TOKEN"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    
    const res = await fetch(`${baseUrl}/api/v2/metrics/prometheus`, {
      method: "GET",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "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/metrics/prometheus",
        headers={
            "Authorization": f"Bearer {api_key}",
            "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/metrics/prometheus", baseURL)
    
    	req, _ := http.NewRequest("GET", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    	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
    401503
    Related
    Observability & audit
    KEYaudit:createIdempotency-Key requiredGET needs only audit:view

    Audit chain verification. The two verbs are NOT interchangeable: GET reads the chain on audit:view, while POST is a mutation — it needs scope audit:create AND an Idempotency-Key header (400 IDEMPOTENCY_KEY_REQUIRED without one). Beyond the core journey.

    Request
    POST {{baseUrl}}/api/v2/audit/chain/verify
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    Idempotency-Key: {{uuid}}
    curl -X POST "$BASE_URL/api/v2/audit/chain/verify" \
      -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/audit/chain/verify`, {
      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/audit/chain/verify",
        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/audit/chain/verify", 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}}
    }
    Errors you can branch on
    400 VALIDATION_ERROR403 API_SCOPE_DENIED
    Related
    Observability & audit
    KEY

    Authenticated machine-readable spec.

    Request
    GET {{baseUrl}}/api/v2/openapi.json
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/openapi.json" \
      -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/openapi.json`, {
      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/openapi.json",
        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/openapi.json", 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
    Integration playbook
    Was this page helpful?
    Thanks — noted.Feedback goes to the docs team by email.
    ← PreviousImport / bulkNext →Beyond happy path
    On this page
    GET /audit-logsGET /metricsGET /metrics/prometheusPOST /audit/chain/verifyGET /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