XCAICX docs Product Contact

Licensing REST API

The licensing authority. Three audiences, three surfaces: devices, operators, and monitoring.

Base URL for the hosted authority: https://licensing.xcaicx.com

SurfaceAuthEndpoints
DevicesNone at activation (the batch token is the credential); bearer afterwards/v1/activate, /v1/usage, /v1/revocations
OperatorsX-Admin-Key header/v1/admin/*
MonitoringNone/healthz, /

The signing key never leaves the server process. Devices verify with the public key compiled into the SDK, so nothing on a camera can mint a licence.

Note

Interactive docs (/docs, /redoc, /openapi.json) are disabled in production. They grant no access on their own, but publishing the full admin surface tells an attacker exactly what to aim at. Set XCAICX_PUBLIC_DOCS=1 in a staging environment where browsing them is genuinely useful.


Monitoring#

GET /healthz#

json
{"ok": true, "public_key": "hLmLM9bA8tx9oKN4H91ycEFbZyXRJVQQ2-BUnH9X6oU", "time": 1786636101}

Careful

public_key is the fingerprint of the key the server signs with. It must match the public key compiled into your SDK build. If it does not, every licence the server mints is rejected by every camera already in the field, and the camera-side symptom is a bare BadSignature that points nowhere near the cause. Check this after every deployment.

GET /#

Service index. Names the host and the device endpoints, and deliberately carries no link to the source repository and no listing of the admin surface.


Device endpoints#

POST /v1/activate#

Exchanges an OEM batch key for a unit-bound licence. This is the billing event.

json
{
  "batch_token": "XCAICX1.eyJsaWMi…",
  "device_id": "b2cd209a6826bd8b33c0fdaad6304106",
  "fingerprint_source": "devicetree-serial",
  "weak_binding": false,
  "sdk_version": "1.0.0"
}
FieldRequiredNotes
batch_tokenyesThe batch key flashed onto the camera
device_idyesHardware fingerprint, ≤ 128 chars
fingerprint_sourcenoWhich source produced it; recorded for fleet visibility
weak_bindingnotrue when derived from a spoofable source such as a MAC
sdk_versionnoRecorded per activation

200 — new activation

json
{
  "unit_token": "XCAICX1.eyJsaWMi…",
  "license_id": "LIC-D3ACF708DB6048AB",
  "reactivated": false,
  "batch_units_remaining": 4999
}

200 — re-activation (same device, same batch)

json
{"unit_token": "XCAICX1.…", "license_id": "LIC-D3ACF708DB6048AB", "reactivated": true}

Idempotent by design: a device already on file gets its existing token back and consumes no additional unit. A factory reflash or an SD-card swap must not be a billing event.

Errors

StatusMeaning
400device_id missing or longer than 128 characters
402Batch exhausted — all purchased units activated
403Bad signature, expired batch key, revoked batch key, or a validly-signed key this authority never issued
500Batch references a SKU not in the catalogue

The 403 for a signed-but-unknown key is deliberate: such a token verifies cryptographically but has no seat accounting behind it, so handing out a unit would be giving away inventory.

POST /v1/usage#

Accepts spooled usage records from a device.

http
POST /v1/usage
Authorization: Bearer XCAICX1.<the unit token>
json
{
  "device_id": "b2cd209a6826bd8b33c0fdaad6304106",
  "license_id": "LIC-D3ACF708DB6048AB",
  "records": [
    {"period_start": 1786500000, "period_end": 1786586400,
     "inferences": 2592000, "frames": 2592000,
     "stream_seconds": 86400, "events": 214}
  ]
}
json
{"accepted": 1, "duplicates": 0}

Notes that matter:

  • The token decides whose usage this is, not body.license_id. Trusting the body would let any device attribute its usage to somebody else's account.
  • A token bound to a different device than device_id is rejected 403.
  • Records dedupe on (device_id, period_start, period_end), so a device that never received an acknowledgement can resend safely.
  • At most 1000 records are processed per call.
StatusMeaning
401Missing bearer token, or the token does not verify
403Token bound to a different device, or licence revoked

GET /v1/revocations#

json
{"revoked": ["LIC-6B1F…", "LIC-90AC…"], "as_of": 1786636101}

Pulled by every online SDK on heartbeat and cached in state_dir/crl.json. Unauthenticated: it is a list of licence ids that no longer work, which is not sensitive and needs to be reachable by cameras whose own credential may already be revoked.


Admin endpoints#

All require X-Admin-Key. The comparison is constant-time — an admin key is a bearer credential and a naive == leaks its prefix to a patient attacker.

bash
export XCAICX_ADMIN_KEY="…"
curl -s -H "X-Admin-Key: $XCAICX_ADMIN_KEY" https://licensing.xcaicx.com/v1/admin/fleet

GET /v1/admin/catalogue#

Every SKU with its modules, caps, term, allowance and price tiers.

POST /v1/admin/customers#

json
{"name": "CamCo", "kind": "oem", "contact": "[email protected]"}

kind is oem or end_user. Returns {"customer_id": "CUST-…"}. Supply id to choose your own.

POST /v1/admin/batches#

Issues an OEM batch key good for units activations.

json
{"customer_id": "CUST-CAMCO", "sku": "oem-industrial", "units": 5000, "term_days": null}
json
{
  "batch_id": "B-2026-1C4417D5",
  "license_id": "LIC-BATCH-…",
  "batch_token": "XCAICX1.…",
  "units": 5000,
  "unit_price": "<per your agreement>",
  "order_value": "<units x unit_price>",
  "expires_at": 0
}

unit_price and order_value come back as real numbers from your own authority, rated from your catalogue; they are elided here because commercial terms are per programme.

The batch key is minted with dev: "*" and act: "required", which is what forces the per-unit exchange. term_days: null uses the SKU default.

StatusMeaning
404Unknown customer
400Unknown SKU

POST /v1/admin/licenses#

Issues a direct licence — an end-user camera, or a pre-provisioned unit.

json
{
  "customer_id": "CUST-PLANT7",
  "sku": "edge-industrial",
  "device_id": "3f2a9c4e8b1d5a6f",
  "term_days": 365,
  "api_quota": 0,
  "max_streams": null,
  "activation": "optional"
}

device_id: "*" leaves the licence unbound — only appropriate for a batch-style key. activation: "optional" is what makes an air-gapped unit work without ever calling home.

POST /v1/admin/revoke#

json
{"license_id": "LIC-D3ACF708DB6048AB", "reason": "chargeback"}

Takes effect immediately for usage reporting, and at the next engine start for the camera — once it has fetched /v1/revocations.

GET /v1/admin/batches/{batch_id}#

json
{
  "batch_id": "B-2026-1C4417D5", "customer_id": "CUST-CAMCO",
  "sku": "oem-industrial", "units_purchased": 5000,
  "units_activated": 1204, "units_remaining": 3796, "expires_at": 0
}

GET /v1/admin/invoice/{customer_id}?period_days=30#

Rates the customer's activations and metered usage for the period. Line types: per-unit from activations, per-unit from direct licences, metered inference over the pooled allowance, and annual maintenance on perpetual SKUs when period_days >= 365.

json
{
  "customer_id": "CUST-CAMCO",
  "period_start": 1784044101, "period_end": 1786636101,
  "currency": "USD",
  "lines": [
    {"description": "… - per unit", "quantity": 1204.0, "unit": "unit",
     "unit_price": "<tier rate>", "amount": "<quantity x rate>"}
  ],
  "total": "<sum of lines>"
}

GET /v1/admin/usage/{license_id}#

json
{"license_id": "LIC-…", "inferences": 2592000, "frames": 2592000,
 "stream_seconds": 86400, "events": 214, "records": 30}

GET /v1/admin/fleet#

Every activated unit, most recent first, capped at 1000.

json
{
  "units": [
    {"device_id": "b2cd209a…", "license_id": "LIC-…", "batch_id": "B-2026-…",
     "fingerprint_source": "mac", "weak_binding": 1, "sdk_version": "1.0.0",
     "activated_at": 1786584928, "sku": "oem-industrial",
     "customer_id": "CUST-CAMCO", "revoked": 0}
  ],
  "count": 1
}

Careful

weak_binding is the field to watch. Weakly-bound units are those whose fingerprint came from a spoofable source; a cluster of them on one customer is worth a phone call before it becomes a support case.


Error format#

FastAPI's standard shape:

json
{"detail": "batch B-2026-1C4417D5 is exhausted: 5000/5000 units activated. Purchase additional units to activate more cameras."}

Messages are written to be shown to a human operator, including the numbers needed to act on them.

Rate limiting#

/v1/activate is rate-limited at the edge: 20 requests per 10 seconds per IP.

Note

A factory provisioning line that legitimately activates faster than that will trip it. The fix is to allowlist the line's egress IP, not to raise the global limit — the limit exists because activation is the one unauthenticated endpoint that mints credentials.

Next#

Commercial software. Use requires a valid XCAICX licence token. Questions an integrator cannot answer from this page belong in an email to [email protected] — and, usually, in a fix to this page.

© 2026 AZMX AI · xcaicx.com