API reference / v1
Integrate the evidence without losing its trail.
Use session-scoped application reads for people and workflows. Use HMAC-authenticated collector endpoints for machines at the edge.
scope first · payload second · retry safely
01 / Integration overview
Two surfaces, one organization boundary.
02 / Authentication and scope
Authenticate for intent, not convenience.
Session endpoints are browser/application reads. A signed-in user must have an active organization membership; admin audit reads additionally require the better-authadminrole.
Collector endpoints are machine reads/writes. Send the issued token inx-collector-token. Tokens have the high-level shapeasc_<collectorId>_<64-hex>. The server verifies an HMAC and returns the same 401 envelope for missing, malformed, revoked, wrong-org, or incorrect credentials.
Never send an orgId to “select” a tenant. Never log the raw collector token, signing secret, or stored token hash. Admins create enrollment tokens and rotate or revoke collector credentials from the operator workflow.
Credential lifecycle
- 01 Admin creates an enrollment token.
- 02 Collector enrolls and receives its long-lived credential once.
- 03 Collector sends config, heartbeat, and observation calls.
- 04 Admin rotates or revokes if the credential must change.
03 / Read endpoints
Read the current record and the trail behind it.
Auth
Session + active organization
Inputs
q, sort, order, status, confidenceLt, take
Auth
Session + active organization
Inputs
id path parameter; no orgId override
Auth
Session + admin role + active organization
Inputs
page, pageSize, actorId, actionType
Auth
x-collector-token HMAC header
Inputs
id path parameter; x-collector-token header
GET /api/audit-logas a paginated JSON feed. There is no dedicated audit-log CSV or download route today; a true downloadable export remains a future API gap if your acceptance process requires one.04 / Ingest endpoints
Send evidence in the shape the collector understands.
Request
Legacy collector ingest: { source, observedAt, devices[] }. Each device can carry mac, ip, hostname, vendor, serial, and assetTag.
Response
202 with scanId, observedAt, receivedAt, source, deviceCount, and createdAlerts.
Request
Optional version, hostname, healthStatus, and snapshot fields for machine health reporting.
Response
200 with serverTime, configVersion, heartbeatInterval, and nextHeartbeatBy.
Request
Stable idempotencyKey, optional sourceLabel/window, and rows[] with required mac plus identifiers, network fields, and observedAt.
Response
201 fresh or 200 replay with accepted, rejected, duplicate, and replay counts.
05 / Request and response examples
Copy a working-shaped request, then adapt the values.
Legacy ingest request
curl -X POST "$ASSETSENTINEL_URL/api/ingest" \
-H "x-collector-token: $ASSET_SENTINEL_TOKEN" \
-H "content-type: application/json" \
-d '{
"source": "branch-office-scan",
"observedAt": "2026-09-05T12:00:00Z",
"devices": [{
"mac": "00:11:22:33:44:55",
"ip": "10.20.4.18",
"hostname": "sw-core-01",
"vendor": "Example Networks",
"serial": "SN-001",
"assetTag": "AT-1042"
}]
}'Heartbeat request
curl -X POST "$ASSETSENTINEL_URL/v1/collectors/$COLLECTOR_ID/heartbeat" \
-H "x-collector-token: $ASSET_SENTINEL_TOKEN" \
-H "content-type: application/json" \
-d '{
"version": "collector-1.8.0",
"hostname": "scan-node-01",
"healthStatus": "healthy",
"snapshot": { "lastScan": "2026-09-05T11:55:00Z" }
}'Observation batch request
curl -X POST "$ASSETSENTINEL_URL/v1/collectors/$COLLECTOR_ID/observations" \
-H "x-collector-token: $ASSET_SENTINEL_TOKEN" \
-H "content-type: application/json" \
-d '{
"idempotencyKey": "scan-2026-09-05T1200Z-001",
"sourceLabel": "branch-office-scan",
"window": { "startedAt": "2026-09-05T11:55:00Z" },
"rows": [{
"mac": "00:11:22:33:44:55",
"ip": "10.20.4.18",
"hostname": "sw-core-01",
"vendor": "Example Networks",
"serial": "SN-001",
"observedAt": "2026-09-05T12:00:00Z"
}]
}'Asset list response shape
{
"items": [{
"id": "asset_123",
"canonicalKey": "hostname:sw-core-01",
"hostname": "sw-core-01",
"confidenceScore": 0.92,
"status": "active",
"sourceCount": 3,
"observationCount": 12,
"fields": [],
"recentObservations": []
}],
"total": 1,
"query": { "take": 50 }
}Observation response shape
{
"observationId": "obs_123",
"rowCount": 1,
"accepted": 1,
"rejected": 0,
"duplicates": 0,
"idempotentReplay": false,
"rejectionReasons": [],
"receivedAt": "2026-09-05T12:00:01Z"
}06 / Errors and retries
Make failure behavior boring.
| Status | Meaning | Client move |
|---|---|---|
| 400 | Request or query does not match the route contract. | Fix the payload; do not retry unchanged. |
| 401 | Session or collector credential is missing/invalid. | Re-authenticate or refresh the credential. |
| 404 | Resource is absent or outside the caller’s organization. | Check the identifier and active scope. |
| 429 | Quota exceeded; body is { "error": "rate_limited" }. | Wait for Retry-After, then back off. |
| 500 | Unexpected server failure. | Retry safely with backoff when the request is idempotent. |
Safe retry rule
Observation retries are designed to be no-ops.
Keep the same idempotencyKey when resubmitting the same observation batch. A fresh submission returns 201; a recognized replay returns 200 withidempotentReplay: trueand does not fan out duplicate evidence.
07 / Rate limits
Design the collector loop around the quota.
Heartbeat
1 / 30s
One accepted heartbeat per 30-second window per credential.
Observations
10 / 60s
Ten accepted observation submissions per 60-second window per credential.
Batch shape
5,000 rows
Rows must have observedAt within ±7 days of server time.
A 429 includes a Retry-Afterheader and the JSON body { "error": "rate_limited" }. Honor both signals, use exponential backoff with jitter, and keep the original idempotency key for a retrying observation batch.
Need the operator workflow?
The getting-started guide covers the first reconciliation pass.
14-day trial · no credit card
Keep the contract close to the collector.
If your integration needs a downloadable audit artifact beyond the current JSON feed, the route map makes that gap explicit for the next API iteration.
Questions or feedback? assetsentinel-2@polsia.app