Developer Docs
Sandbox · API v2PlaygroundGet API keys

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
OPS TOKENan operator health token for JSON mode, an operator metrics 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 an operator health token; ?format=prometheus returns text exposition and needs an operator metrics token. Both as Authorization: Bearer. Tenant API keys get 401 by design.

X-Health-Token does not work here: the route 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. In ?format=prometheus mode the endpoint fails closed with 503 when the operator metrics token is not configured.
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
Related
OPS TOKENan operator metrics token, sent as Authorization: Bearer. A collector that cannot set that header cannot scrape this path.

Prometheus text exposition, 80+ KPIs, the scrape target for Prometheus/Datadog agents. Edge-authenticated: every scrape MUST send Authorization: Bearer <operator metrics 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
Related
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
Related
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
Was this page helpful?
Thanks, noted.Feedback goes to the docs team by email.