Collector API v1 reference
Request/response shapes for the collector-facing API v1 endpoints: enroll, heartbeat and observations, plus tenant/site binding rules and error codes.
Overview
The v0.1 collector talks to three POST, JSON, snake_case endpoints under /api/v1/collectors. On deployments where the main site is access-restricted, identical mirrors are also exposed under /api/public/v1/collectors so a collector on an unauthenticated network path can still reach the control plane.
| Endpoint | Mirror | Purpose |
|---|---|---|
| POST /api/v1/collectors/enroll | /api/public/v1/collectors/enroll | Exchange a one-time enrollment token for a collector identity and credential |
| POST /api/v1/collectors/heartbeat | /api/public/v1/collectors/heartbeat | Report liveness, version and health; receive commands and current scan scopes |
| POST /api/v1/collectors/observations | /api/public/v1/collectors/observations | Upload a batch of discovered hosts and open ports |
Authentication and tokens
Enrollment tokens are random, short-lived, single-use, and bound to a specific partner, customer and site at issue time. The control plane stores only a SHA-256 hash of the token, never the token itself, and invalidates it atomically on successful enrollment so it can never be replayed.
Heartbeat and observations calls authenticate with a bearer token issued at enrollment.
Authorization: Bearer <auth_token>Enroll
Called once, on first start, to exchange the one-time enrollment token for a durable collector identity.
{
"token": "<one-time-token>",
"hostname": "web-scanner-01",
"os": "linux",
"os_version": "Ubuntu 22.04",
"arch": "amd64",
"version": "0.1.0",
"instance_id": "b3f6c1a2-..."
}{
"collector_id": "col_...",
"customer_id": "cus_...",
"site_id": "site_...",
"partner_id": "prt_...",
"api_base_url": "https://api.assuriq.io",
"auth_token": "at_...",
"auth_token_expires_at": "2026-09-24T00:00:00Z",
"client_cert_pem": null,
"client_key_pem": null,
"credential_note": "bearer-token; mTLS device certificates are planned",
"heartbeat_interval_s": 60,
"clock_skew_tolerance_ms": 5000,
"modules": ["core-heartbeat", "port-discovery"],
"scan_scopes": [
{
"cidrs": ["10.20.0.0/24"],
"excludes": ["10.20.0.1"],
"blocked_targets": [],
"max_concurrency": 20,
"discovery_only": true,
"credentialed": false,
"method": "tcp_connect"
}
]
}Heartbeat
Sent on the interval returned at enrollment (heartbeat_interval_s). All fields except the bearer token are optional; the server updates health, version, hostname, OS and arch, and last_seen from whatever is supplied.
{
"collector_id": "col_...",
"instance_id": "b3f6c1a2-...",
"hostname": "web-scanner-01",
"os": "linux",
"os_version": "Ubuntu 22.04",
"arch": "amd64",
"version": "0.1.0",
"status": "healthy",
"sent_at": "2026-08-24T00:00:00Z",
"cpu_pct": 4.2,
"memory_pct": 18.5,
"disk_free_pct": 71.0,
"uptime_s": 86400,
"queue_depth": 0,
"active_job_id": null,
"last_error": null
}{
"ok": true,
"collector_id": "col_...",
"customer_id": "cus_...",
"site_id": "site_...",
"state": "healthy",
"server_time": "2026-08-24T00:00:01Z",
"clock_skew_ms": 120,
"heartbeat_interval_s": 60,
"update": null,
"commands": [],
"scan_scopes": []
}Observations
Uploads a batch of discovered hosts and their open ports. batch_id is an idempotency key: resubmitting the same batch_id after a network retry is safe and returns the original result rather than duplicating data.
{
"batch_id": "b_2f1c...",
"observed_at": "2026-08-24T00:05:00Z",
"scan_job_id": "job_...",
"collector_id": "col_...",
"scope_cidrs": ["10.20.0.0/24"],
"hosts": [
{
"ip": "10.20.0.42",
"hostname": "printer-3f",
"mac": "AA:BB:CC:00:11:22",
"rtt_ms": 4,
"observed_at": "2026-08-24T00:05:00Z",
"open_ports": [
{ "port": 9100, "proto": "tcp", "service": "jetdirect", "state": "open" }
],
"attributes": {}
}
]
}{
"ok": true,
"batch_id": "b_2f1c...",
"accepted": 1,
"rejected": 0,
"created_assets": 1,
"updated_assets": 0,
"errors": [],
"duplicate": false
}Assets are reconciled in order of confidence: MAC address first, then hostname plus site, then IP address — so the same physical host is not duplicated across successive scans as its IP changes.
Topology fields (Collector 0.2.0-dev)
Collectors 0.2.0-dev and later may include optional per-host topology arrays alongside ip/hostname/open_ports. All fields are optional and 0.1.x payloads remain valid. AssurIQ stores the raw arrays as observations first, then derives interfaces, VLANs, addresses, routes, neighbors and forwarding entries, and finally computes graph edges labelled verified (LLDP/CDP neighbor) or inferred (forwarding table, route, subnet).
| Field | Shape | Derived into |
|---|---|---|
| sys_name, sys_description, sys_object_id, snmp_version, vendor, model, serial, device_type | strings | Asset identity (device class, vendor/model, SNMP identity) |
| interfaces[] | index, name, description, alias, mac, type, mtu, speed_bps, admin_status, oper_status, pvid, poe | net_interfaces, Switch Port Matrix |
| vlans[] | id, name, source | net_vlans, VLAN Explorer |
| ip_addresses[] | address, prefix_length, subnet, interface_index, vlan_id | net_ip_addresses, subnets |
| routes[] | destination, prefix_length, next_hop, interface_index, metric | net_routes, L3 view, inferred route edges |
| neighbors[] | protocol (LLDP/CDP), local_port_id, remote_chassis_id, remote_port_id, remote_system_name, confidence | net_neighbors, verified L1 edges |
| forwarding[] | mac, interface_index, bridge_port, vlan_id, status | net_forwarding, inferred L2 endpoint attachments, port-move detection |
| topology.subnets[] / topology.links[] | cidr, gateway, vlan_id / source & target asset, interface, protocol, layer, confidence | net_subnets, explicit edges (deduplicated against derived ones) |
The response adds a topology summary: assets_with_topology, edges, subnets and links. Neighbors whose remote chassis cannot be matched to a known asset at the same site are kept as unresolved nodes rather than guessed.
Tenant and site binding rules
Every observations call is validated against the calling collector's assigned tenant and site before anything is written.
| Condition | Result |
|---|---|
| collector_id, customer_id or site_id in the payload does not match the authenticated collector | 403 rejected |
| The collector has no assigned site | 409 rejected |
| A host IP falls outside the collector's authorized scan scope | That host is rejected individually; the rest of the batch is still processed |
Error codes
- AIQ-ENR-005Enrollment token binding mismatchThe token is valid but does not match the partner, customer or site the install attempted to enroll into.
- AIQ-ENR-006Customer missingThe token's bound customer no longer exists, so enrollment cannot be completed.
- AIQ-ENR-007Site missing or belongs to a different customerThe token's bound site no longer exists, or now belongs to a different customer than the token was issued for.
- AIQ-ENR-008Credential issuance failedEnrollment validated but the server could not issue a collector credential.
- AIQ-COL-008Collector ID mismatchA heartbeat or observations call included a collector_id that does not match the authenticated collector's own id.
- AIQ-COL-009Customer ID mismatchA request included a customer_id that does not match the authenticated collector's assigned customer.
- AIQ-COL-010Collector has no assigned siteObservations were submitted by a collector that has not been assigned to a site.
- AIQ-COL-011Site ID mismatchA request included a site_id that does not match the authenticated collector's assigned site.
- AIQ-COL-012Assigned site invalidThe collector's assigned site no longer exists or is otherwise invalid, so observations cannot be reconciled.
- AIQ-ING-002Observation processing failedThe control plane accepted an observations batch but failed while reconciling it into assets.
Production hardening: the CSR flow
- AIQ-ENR-005 Enrollment token binding mismatch
- AIQ-ENR-006 Customer missing
- AIQ-ENR-007 Site missing or belongs to a different customer
- AIQ-ENR-008 Credential issuance failed
- AIQ-COL-008 Collector ID mismatch
- AIQ-COL-009 Customer ID mismatch
- AIQ-COL-010 Collector has no assigned site
- AIQ-COL-011 Site ID mismatch
- AIQ-COL-012 Assigned site invalid
- AIQ-ING-002 Observation processing failed