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.

request / responseready
GET/api/assets
POST/v1/collectors/:id/observations

scope first · payload second · retry safely

01 / Integration overview

Two surfaces, one organization boundary.

For signed-in operators
Application API
GET /api/assets, GET /api/assets/{id}, and GET /api/audit-log use the better-auth session. Organization identity comes from the caller’s active membership, never from an orgId supplied in a query or body.
For registered collectors
Machine API
GET /v1/collectors/{id}/config, POST /v1/collectors/{id}/heartbeat, and POST /v1/collectors/{id}/observations use x-collector-token. The credential resolves the collector and organization together.

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

  1. 01 Admin creates an enrollment token.
  2. 02 Collector enrolls and receives its long-lived credential once.
  3. 03 Collector sends config, heartbeat, and observation calls.
  4. 04 Admin rotates or revokes if the credential must change.

03 / Read endpoints

Read the current record and the trail behind it.

GET
/api/assets
List reconciled assets with search, filtering, sorting, and confidence fields.
200 · 400 · 401 · 403 · 500

Auth
Session + active organization

Inputs
q, sort, order, status, confidenceLt, take

GET
/api/assets/{id}
Read one asset, its confidence summary, lineage, recent observations, and timeline.
200 · 400 · 401 · 404 · 500

Auth
Session + active organization

Inputs
id path parameter; no orgId override

GET
/api/audit-log
Read the paginated JSON audit feed for the rolling 90-day window.
200 · 400 · 401 · 403 · 500

Auth
Session + admin role + active organization

Inputs
page, pageSize, actorId, actionType

GET
/v1/collectors/{id}/config
Retrieve config version, heartbeat cadence, scan cadence, subnets, and free-form config.
200 · 401 · 500

Auth
x-collector-token HMAC header

Inputs
id path parameter; x-collector-token header

Audit export note. The current repository exposes 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.

POST
/api/ingest
x-collector-token

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.

POST
/v1/collectors/{id}/heartbeat
x-collector-token

Request
Optional version, hostname, healthStatus, and snapshot fields for machine health reporting.

Response
200 with serverTime, configVersion, heartbeatInterval, and nextHeartbeatBy.

POST
/v1/collectors/{id}/observations
x-collector-token

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.

StatusMeaningClient move
400Request or query does not match the route contract.Fix the payload; do not retry unchanged.
401Session or collector credential is missing/invalid.Re-authenticate or refresh the credential.
404Resource is absent or outside the caller’s organization.Check the identifier and active scope.
429Quota exceeded; body is { "error": "rate_limited" }.Wait for Retry-After, then back off.
500Unexpected 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.

Read getting started

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