API Reference

Vigil Platform API v1

Complete reference for the Vigil REST API and SSE streaming endpoints. All production endpoints are authenticated; public dashboard share tokens work without a key.

Authentication

All endpoints require a valid session or API key. Browser-based Vigil sessions use HTTP cookies automatically. For programmatic access, generate an API key in the dashboard.

JWT auth (interactive): POST {"email":"…","password":"…"} to /api/v1/auth/login/ — response returns {"access":"<jwt>","refresh":"…"}. Pass the access token as Authorization: Bearer <jwt>. JWTs expire; use the refresh token against /api/v1/auth/token/refresh/.

Org API key (service accounts): Generate a long-lived key in Settings → API Keys. Pass it identically: Authorization: Bearer <org-api-key>.

Gateway token (ingest only): A gw_… token scoped to data ingestion. Create one via POST /api/v1/ingest/tokens/. Pass as Authorization: Bearer gw_… on ingest requests only.

Public canvas share: /api/v1/canvas/public/{token}/ and its /query/ sub-endpoint require no auth — the share token is embedded in the URL.

Ingest

Push measurement data from gateway nodes. Ingest endpoints use a gateway token (gw_…), not a user JWT — create one via POST /api/v1/ingest/tokens/.

POST /api/v1/ingest/ Push a batch of measurements from a hardware node

Authenticated with a gateway token (Authorization: Bearer gw_…). Each measurement references a binding_id (quantity channel UUID) and a float value. Timestamps default to server receive-time if omitted.

Request body
FieldTypeDescription
hardware_idstringrequiredDevice hardware_id matching the gateway token scope
measurementsarrayrequiredArray of measurement objects (see below)
Measurement object
FieldTypeDescription
binding_idstringrequiredUUID of the quantity binding (channel → quantity mapping)
valuenumber|string|objectrequiredMeasured value in the binding's base unit
timestampstringoptionalISO-8601 UTC timestamp (default: server receive-time)
qualityinteger 0–255optionalSignal quality byte (0 = unknown)
curl -X POST https://yourdomain.com/api/v1/ingest/ \
  -H "Authorization: Bearer gw_<gateway-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "hardware_id": "SIM-001",
    "measurements": [
      {"binding_id": "<uuid>", "value": 22.5, "quality": 200},
      {"binding_id": "<uuid2>", "value": 1013.2}
    ]
  }'
from mnemos_client import MnemosClient

client = MnemosClient.login(base_url="https://yourdomain.com",
                             email="you@example.com", password="secret")
token_raw = client.tokens.create(hardware_id="SIM-001", name="Dev Token")

client.ingest(token_raw=token_raw, hardware_id="SIM-001", measurements=[
    {"binding_id": "<uuid>", "value": 22.5},
    {"binding_id": "<uuid2>", "value": 1013.2},
])
await fetch("/api/v1/ingest/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer gw_<gateway-token>"
  },
  body: JSON.stringify({
    hardware_id: "SIM-001",
    measurements: [
      { binding_id: "<uuid>",  value: 22.5 },
      { binding_id: "<uuid2>", value: 1013.2 },
    ]
  })
});
POST /api/v1/ingest/tokens/ Create a gateway token for a hardware node

Requires a user JWT or org API key. The response includes a raw_token field with the gw_… secret — store it immediately, it is never shown again.

Request body
FieldTypeDescription
namestringrequiredHuman-readable label for the token
hardware_idstringoptionalScope the token to a specific device hardware ID
Response
{"id":"...","name":"Dev Token","hardware_id":"SIM-001","raw_token":"gw_Kx9...","created_at":"2026-04-17T10:00:00Z"}
curl -X POST https://yourdomain.com/api/v1/ingest/tokens/ \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Dev Token","hardware_id":"SIM-001"}'
raw_token = client.tokens.create(hardware_id="SIM-001", name="Dev Token")
# raw_token == "gw_Kx9..." — store this securely

Devices & Sites

Manage devices, quantity bindings, and sites. All endpoints require a user JWT or org API key.

GET /api/v1/devices/ List all devices with site, status, and binding counts
ParamTypeDescription
site_idstringoptionalFilter by site UUID
searchstringoptionalSearch by display name or hardware_id
onlinebooloptionalFilter online devices (true / false)
Response
{"count":3,"results":[{"id":"...","display_name":"Pump 1","hardware_id":"SN-PS3-PUMP1","is_online":true,"site":{"id":"...","name":"Pump Station 3"},"binding_count":2}]}
curl -H "Authorization: Bearer <token>" https://yourdomain.com/api/v1/devices/
import requests
r = requests.get("https://yourdomain.com/api/v1/devices/",
    headers={"Authorization": "Bearer <token>"})
devices = r.json()["results"]
const r = await fetch("/api/v1/devices/", {
  headers: { "Authorization": "Bearer <token>" }
});
const { results: devices } = await r.json();
GET /api/v1/devices/{device_id}/ Full device detail including all active bindings and quantity metadata
Response
{"id":"...","display_name":"Pump 1","hardware_id":"SN-PS3-PUMP1","is_online":true,
 "last_seen_at":"2026-04-13T12:00:00Z","site":{...},
 "bindings":[{"id":"...","quantity":{"name":"Vibration","slug":"vibration.acceleration.rms","base_unit_symbol":"m/s²"},"is_active":true}]}
curl -H "Authorization: Bearer <token>" https://yourdomain.com/api/v1/devices/<device_id>/
r = requests.get(f"https://yourdomain.com/api/v1/devices/{device_id}/",
    headers={"Authorization": "Bearer <token>"})
device = r.json()
const device = await (await fetch(`/api/v1/devices/${deviceId}/`,
  { headers: { "Authorization": "Bearer <token>" } })).json();
GET /api/v1/devices/{device_id}/latest/ Latest measurement for every active binding — ideal for real-time widgets
Response
{"device_id":"...","readings":[{"binding_id":"...","quantity":"Temperature","unit":"K","value":295.3,"time":"2026-04-13T12:00:00Z"}]}
curl -H "Authorization: Bearer <token>" https://yourdomain.com/api/v1/devices/<id>/latest/
r = requests.get(f"https://yourdomain.com/api/v1/devices/{device_id}/latest/",
    headers={"Authorization": "Bearer <token>"})
latest = r.json()["readings"]
const { readings } = await (await fetch(`/api/v1/devices/${deviceId}/latest/`,
  { headers: { "Authorization": "Bearer <token>" } })).json();
POST /api/v1/devices/ Register a new hardware device
FieldTypeDescription
hardware_idstringrequiredUnique hardware identifier (e.g. MAC address or serial)
display_namestringrequiredHuman-readable device name
node_typestringoptionalsensor_node (default), gateway, actuator
site_idstringoptionalUUID of the site to assign the device to
curl -X POST https://yourdomain.com/api/v1/devices/ \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"hardware_id":"SIM-001","display_name":"My Sensor","node_type":"sensor_node"}'
device = client.devices.create(hardware_id="SIM-001", display_name="My Sensor")
print(device["id"])  # UUID to use in binding calls
GET /api/v1/devices/{device_id}/bindings/ List quantity bindings for a device

Each binding maps a physical channel number to a Canonical Physical Ontology (CPO) quantity. The binding_id is the key used in ingest and explorer calls.

Response
[{"id":"<binding-uuid>","channel":0,"quantity":{"slug":"temperature.celsius","name":"Temperature","base_unit_symbol":"°C"},"role":"sensor","is_active":true}]
curl -H "Authorization: Bearer <token>" \
  https://yourdomain.com/api/v1/devices/<device_id>/bindings/
bindings = client.devices.bindings(device_id="<uuid>")
for b in bindings:
    print(b["id"], b["quantity"]["slug"])
POST /api/v1/devices/{device_id}/bindings/ Add a quantity binding (channel → CPO quantity)
FieldTypeDescription
channelintegerrequiredPhysical channel index on the device (0-based)
quantity_slugstringrequiredCPO quantity slug, e.g. temperature.celsius — see Quantities
rolestringoptionalsensor (default) or actuator
sample_rate_tierstringoptionallow (default), medium, high
curl -X POST https://yourdomain.com/api/v1/devices/<device_id>/bindings/ \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"channel":0,"quantity_slug":"temperature.celsius","role":"sensor"}'
binding = client.devices.create_binding(
    device_id="<uuid>",
    channel=0,
    quantity_slug="temperature.celsius",
)
print(binding["id"])  # use this as binding_id in ingest calls
GET /api/v1/sites/ List all sites in the organization
curl -H "Authorization: Bearer <token>" https://yourdomain.com/api/v1/sites/
r = requests.get("https://yourdomain.com/api/v1/sites/",
    headers={"Authorization": "Bearer <token>"})
sites = r.json()["results"]
const { results: sites } = await (await fetch("/api/v1/sites/",
  { headers: { "Authorization": "Bearer <token>" } })).json();
GET /api/v1/sites/{site_id}/ Site detail with device list
curl -H "Authorization: Bearer <token>" https://yourdomain.com/api/v1/sites/<site_id>/
r = requests.get(f"https://yourdomain.com/api/v1/sites/{site_id}/",
    headers={"Authorization": "Bearer <token>"})
site = r.json()

Explorer — Time-Series Query

Query time-series data from TimescaleDB via GET /api/v1/explorer/query/. Binding IDs are passed as a comma-separated query parameter. Points are automatically downsampled to max_points using time-bucket averaging.

GET /api/v1/explorer/query/ Query time-series data for one or more bindings

Returns columnar time-series data. Pass binding_ids as a comma-separated list. start and end accept ISO-8601 strings or relative shorthands like -1h, -7d.

ParamTypeDescription
binding_idsstringrequiredComma-separated binding UUIDs
startstringoptionalStart time (ISO-8601 or -1h, -7d)
endstringoptionalEnd time (default: now)
max_pointsintegeroptionalMax data points per series (default 500)
Response
{
  "timestamps": ["2026-04-13T09:00:00Z", "2026-04-13T09:01:00Z", "..."],
  "series": [
    {
      "binding_id": "f3a1b2c4-...",
      "label":      "Pump 1 / Vibration",
      "values":     [1.24, 1.31, null, 1.35],
      "unit":       "m/s²"
    }
  ]
}
curl -H "Authorization: Bearer <token>" \
  "https://yourdomain.com/api/v1/explorer/query/?binding_ids=<uuid1>,<uuid2>&start=-1h&max_points=500"
import requests
r = requests.get(
    "https://yourdomain.com/api/v1/explorer/query/",
    headers={"Authorization": "Bearer <token>"},
    params={
        "binding_ids": "<uuid1>,<uuid2>",
        "start": "-1h",
        "max_points": 500,
    }
)
data = r.json()
timestamps = data["timestamps"]
for series in data["series"]:
    print(series["label"], series["values"])
const params = new URLSearchParams({
  binding_ids: "<uuid1>,<uuid2>",
  start: "-1h",
  max_points: "500",
});
const r = await fetch(`/api/v1/explorer/query/?${params}`, {
  headers: { "Authorization": "Bearer <token>" }
});
const { timestamps, series } = await r.json();

Quantities / Canonical Physical Ontology

Browse the built-in library of physical quantities (CPO). Each quantity has a stable slug used when creating device bindings.

GET /api/v1/quantities/canonical/ List or search physical quantities
ParamTypeDescription
searchstringoptionalFull-text search across name and slug
phenomenon_classstringoptionalFilter by domain: thermal, mechanical, electromagnetic, chemical, optical
Response
{"count":142,"results":[{"slug":"temperature.celsius","name":"Temperature","base_unit_symbol":"°C","phenomenon_class":"thermal"},...]}
curl -H "Authorization: Bearer <token>" \
  "https://yourdomain.com/api/v1/quantities/canonical/?search=vibration"
# List all mechanical quantities
results = client.quantities.list(domain="mechanical")
for q in results["results"]:
    print(q["slug"], q["base_unit_symbol"])
GET /api/v1/quantities/canonical/{slug}/ Full quantity detail including unit conversions and SI definition
Response
{"slug":"temperature.celsius","name":"Temperature","base_unit_symbol":"°C","si_unit":"K","phenomenon_class":"thermal","description":"Thermodynamic temperature relative to 273.15 K"}
curl -H "Authorization: Bearer <token>" \
  https://yourdomain.com/api/v1/quantities/canonical/temperature.celsius/
qty = client.quantities.get("temperature.celsius")
print(qty["si_unit"])

Alerts

Query alert events, rules, and subscribe to the live SSE alert stream.

GET /api/v1/alerts/ List alert events with optional filtering
ParamTypeDescription
statusstringoptionalactive, resolved, acknowledged
severitystringoptionalcritical, warning, info
device_idstringoptionalFilter by device UUID
curl -H "Authorization: Bearer <token>" \
  "https://yourdomain.com/api/v1/alerts/?status=active&severity=critical"
r = requests.get("https://yourdomain.com/api/v1/alerts/",
    headers={"Authorization": "Bearer <token>"},
    params={"status": "active", "severity": "critical"})
events = r.json()["events"]
const r = await fetch("/api/v1/alerts/?status=active&severity=critical",
  { headers: { "Authorization": "Bearer <token>" } });
const { events } = await r.json();
GET /api/v1/alerts/{event_id}/ Full alert event detail including trigger value, rule, and resolution
curl -H "Authorization: Bearer <token>" https://yourdomain.com/api/v1/alerts/<event_id>/
r = requests.get(f"https://yourdomain.com/api/v1/alerts/{event_id}/",
    headers={"Authorization": "Bearer <token>"})
alert = r.json()
const alert = await (await fetch(`/api/v1/alerts/${eventId}/`,
  { headers: { "Authorization": "Bearer <token>" } })).json();
SSE /api/v1/alerts/stream/ Server-Sent Events stream — pushed instantly on every new alert

A long-lived HTTP connection that pushes alert events whenever a new alert fires or resolves. Use this to drive real-time notification badges, external webhooks, or dashboards without polling.

Event format
event: alert
data: {"event_id":"...","severity":"critical","device":"Pump 1","quantity":"Vibration","value":4.8,"triggered_at":"2026-04-13T12:01:00Z"}
// Browser — EventSource (auto-reconnects)
const src = new EventSource("/api/v1/alerts/stream/");
src.addEventListener("alert", e => {
  const alert = JSON.parse(e.data);
  console.log(`ALERT ${alert.severity}: ${alert.device} — ${alert.quantity} = ${alert.value}`);
});
src.onerror = () => console.warn("SSE reconnecting…");
import sseclient, requests, json

resp = requests.get("https://yourdomain.com/api/v1/alerts/stream/",
    headers={"Authorization": "Bearer <token>"}, stream=True)

for event in sseclient.SSEClient(resp):
    if event.event == "alert":
        alert = json.loads(event.data)
        print(f"ALERT: {alert['device']} — {alert['severity']}")
GET /api/v1/alerts/rules/ List configured alert rules for the organization
curl -H "Authorization: Bearer <token>" https://yourdomain.com/api/v1/alerts/rules/
r = requests.get("https://yourdomain.com/api/v1/alerts/rules/",
    headers={"Authorization": "Bearer <token>"})
rules = r.json()["rules"]
const { rules } = await (await fetch("/api/v1/alerts/rules/",
  { headers: { "Authorization": "Bearer <token>" } })).json();

Canvas & Dashboards

Access dashboards and their panel data. Public share links work without authentication.

GET /api/v1/canvas/ List all dashboards (name, panel count, public link status)
curl -H "Authorization: Bearer <token>" https://yourdomain.com/api/v1/canvas/
r = requests.get("https://yourdomain.com/api/v1/canvas/",
    headers={"Authorization": "Bearer <token>"})
dashboards = r.json()["dashboards"]
const { dashboards } = await (await fetch("/api/v1/canvas/",
  { headers: { "Authorization": "Bearer <token>" } })).json();
GET /api/v1/canvas/public/{token}/ Unauthenticated — view a shared dashboard by public token

No API key required. Use this URL in iframes or to embed dashboards in external portals.

// Navigate directly — no API key needed
window.open("https://yourdomain.com/api/v1/canvas/public/<token>/");
<!-- Embed in an iframe -->
<iframe
  src="https://yourdomain.com/api/v1/canvas/public/<token>/"
  width="100%" height="600"
  frameborder="0"
  allowfullscreen>
</iframe>
POST /api/v1/canvas/public/{token}/query/ Unauthenticated — query time-series data for a shared dashboard

Scoped to the bindings visible on the public dashboard. Same body format as /api/v1/explorer/query/.

curl -X POST https://yourdomain.com/api/v1/canvas/public/<token>/query/ \
  -H "Content-Type: application/json" \
  -d '{"binding_ids":["<uuid>"],"start":"2026-04-13T09:00:00Z","end":"2026-04-13T12:00:00Z"}'
const r = await fetch(`/api/v1/canvas/public/${token}/query/`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    binding_ids: ["<uuid>"],
    start: new Date(Date.now() - 3600_000).toISOString(),
    end:   new Date().toISOString(),
  })
});
const { series } = await r.json();

AI & Argus

Natural language queries and the Argus conversational assistant with tool-use and institutional memory.

POST /api/v1/ai/nlq/ Natural language query — converts plain English to SQL and returns data
Request body
FieldTypeDescription
questionstringrequiredNatural language question about your sensor data
curl -X POST https://yourdomain.com/api/v1/ai/nlq/ \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"question":"What was the average temperature in Building A last week?"}'
r = requests.post("https://yourdomain.com/api/v1/ai/nlq/",
    headers={"Authorization": "Bearer <token>"},
    json={"question": "What was the peak vibration on Pump 1 this month?"})
result = r.json()
print(result["answer"])   # plain-English answer
print(result["sql"])      # generated SQL
const r = await fetch("/api/v1/ai/nlq/", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": "Bearer <token>" },
  body: JSON.stringify({ question: "Show CO2 trends for floor 2 this week" })
});
const { answer, sql, chart_data } = await r.json();
POST /api/v1/ai/argus/message/ Argus conversational assistant — streaming SSE with tool use

Sends a message to Argus and returns a streaming SSE response. Argus can autonomously call tools (query data, save memories, build dashboards, configure alerts) within a single ReAct loop. Pass conversation_id to continue an existing thread.

Request body
FieldTypeDescription
messagestringrequiredUser message
conversation_idstringoptionalUUID of existing conversation to continue
page_contextobjectoptionalCurrent UI context: {page, dashboard_id, device_id, site_id}
SSE event types
data: {"event":"token","text":"Here is what I found..."}
data: {"event":"thinking","tool":"query_data","status":"calling"}
data: {"event":"tool_result","tool":"query_data","result":{...}}
data: {"event":"thinking","tool":"query_data","status":"done"}
data: {"event":"navigate","url":"/api/v1/canvas/<id>/","label":"Dashboard"}
data: {"event":"done","content":"Full response text","conversation_id":"..."}
// Stream Argus response token by token
const resp = await fetch("/api/v1/ai/argus/message/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <token>"
  },
  body: JSON.stringify({
    message: "Build me a dashboard for pump station monitoring",
    page_context: { page: "canvas" }
  })
});

const reader = resp.body.getReader();
const dec    = new TextDecoder();
let buf = "";
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buf += dec.decode(value);
  for (const line of buf.split("\n\n")) {
    if (!line.startsWith("data: ")) continue;
    const ev = JSON.parse(line.slice(6));
    if (ev.event === "token") process.stdout.write(ev.text);
    if (ev.event === "done") { console.log("\nDone:", ev.conversation_id); break; }
  }
  buf = buf.slice(buf.lastIndexOf("\n\n") + 2);
}
import json, requests

resp = requests.post(
    "https://yourdomain.com/api/v1/ai/argus/message/",
    headers={"Authorization": "Bearer <token>"},
    json={"message": "What is the current vibration level on Pump 1?"},
    stream=True,
)
for line in resp.iter_lines():
    if not line or not line.startswith(b"data: "):
        continue
    ev = json.loads(line[6:])
    if ev["event"] == "token":
        print(ev["text"], end="", flush=True)
    elif ev["event"] == "done":
        print(f"\n[conversation: {ev['conversation_id']}]")
        break
GET /api/v1/ai/argus/conversations/ List conversation history (last 50)
curl -H "Authorization: Bearer <token>" \
  https://yourdomain.com/api/v1/ai/argus/conversations/
r = requests.get("https://yourdomain.com/api/v1/ai/argus/conversations/",
    headers={"Authorization": "Bearer <token>"})
conversations = r.json()["conversations"]
GET /api/v1/ai/argus/memory/ List or search institutional memories (supports POST to create)
ParamTypeDescription
qstringoptionalSemantic search query
device_idstringoptionalFilter by device UUID
site_idstringoptionalFilter by site UUID
memory_typestringoptionalobservation, procedure, anomaly, maintenance
# Search memories
curl -H "Authorization: Bearer <token>" \
  "https://yourdomain.com/api/v1/ai/argus/memory/?q=pump+bearing+failure"

# Create a memory
curl -X POST https://yourdomain.com/api/v1/ai/argus/memory/ \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"content":"Pump 2 bearing replaced Jan 2026","memory_type":"maintenance","salience":0.8}'
# Search
r = requests.get("https://yourdomain.com/api/v1/ai/argus/memory/",
    headers={"Authorization": "Bearer <token>"},
    params={"q": "bearing failure"})
memories = r.json()["memories"]

# Create
r = requests.post("https://yourdomain.com/api/v1/ai/argus/memory/",
    headers={"Authorization": "Bearer <token>"},
    json={"content": "CO2 sensor on Floor 2 prone to drift",
          "memory_type": "observation", "salience": 0.7})
GET /api/v1/ai/argus/investigations/ List background AI investigations and their conclusions
curl -H "Authorization: Bearer <token>" \
  https://yourdomain.com/api/v1/ai/argus/investigations/
r = requests.get("https://yourdomain.com/api/v1/ai/argus/investigations/",
    headers={"Authorization": "Bearer <token>"})
invs = [i for i in r.json()["investigations"] if i["status"] == "complete"]

Prism — Derived Fields

Query computed fields derived from raw sensor data using configurable transformation expressions.

GET /api/v1/prism/fields/ List all Prism derived field definitions
curl -H "Authorization: Bearer <token>" https://yourdomain.com/api/v1/prism/fields/
r = requests.get("https://yourdomain.com/api/v1/prism/fields/",
    headers={"Authorization": "Bearer <token>"})
fields = r.json()["fields"]
GET /api/v1/prism/{field_id}/data/ Compute and return time-series data for a derived field
ParamTypeDescription
startstringoptionalISO 8601 start (default: 24h ago)
endstringoptionalISO 8601 end (default: now)
curl -H "Authorization: Bearer <token>" \
  "https://yourdomain.com/api/v1/prism/<field_id>/data/?start=2026-04-13T00:00:00Z"
r = requests.get(f"https://yourdomain.com/api/v1/prism/{field_id}/data/",
    headers={"Authorization": "Bearer <token>"},
    params={"start": "2026-04-13T00:00:00Z"})
data = r.json()

PDP Registry

Browse and query the Peripheral Detection Protocol type registry. Each PeripheralType defines the TEDS metadata and quantity bindings for a specific probe model.

GET /api/v1/pdp/types/ List public peripheral type definitions
ParamTypeDescription
peripheral_classstringoptionalFilter by class: thermal, mechanical, electromagnetic, chemical, optical
searchstringoptionalSearch by name or pdp_type_id
contribution_statusstringoptionalofficial, community, pending
Response
{"count":24,"results":[{"pdp_type_id":"CLR-T-001","name":"K-Type Thermocouple","peripheral_class":"thermal","version":"1.0.0","contribution_status":"official","is_public":true}]}
curl -H "Authorization: Bearer <token>" \
  "https://yourdomain.com/api/v1/pdp/types/?peripheral_class=thermal&contribution_status=official"
import requests
r = requests.get("https://yourdomain.com/api/v1/pdp/types/",
    headers={"Authorization": "Bearer <token>"},
    params={"peripheral_class": "thermal", "contribution_status": "official"})
types = r.json()["results"]
GET /api/v1/pdp/catalog/export/ Export the full PDP catalog as JSON for offline use or firmware embedding

Returns the complete set of public peripheral type definitions in a single JSON document. Useful for embedded systems that need a local copy of the type registry for TEDS auto-provisioning.

curl -H "Authorization: Bearer <token>" \
  https://yourdomain.com/api/v1/pdp/catalog/export/ \
  -o pdp_catalog.json