Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Modellen Tools & Services
Oplossingen
Organisaties Ontwikkelaars Internet Service Providers Managed Service Providers AI-in-a-Box
Kenniscentrum
Ondersteuning Documentation Blog Downloads
Bedrijf
Over ons Onderzoek Vacatures Investeren Contact
Inloggen

Devices API

Reference for the Devices endpoint group — 9 endpoints.

Generated from the live OpenAPI spec. Re-run _generate_api_reference.py after backend changes.

Authentication#

All endpoints require a Bearer JWT in the Authorization header unless noted otherwise. See Concepts → Tokens and scopes and Reference → OAuth endpoints for how to obtain one.

Endpoints#

POST /api/v1/devices/heartbeat#

Heartbeat Batch

Batched presence heartbeat.

Primary shape — a WS gateway posts one request per cadence carrying every device it observed. Response reports how many entries were applied and which were rejected (with reasons); a partial success is not an error.

Auth: admin token or platform token. Tenant scoping enforced per-device inside the batch: an entry for a device in a tenant the caller can't see is rejected but doesn't fail the batch.

Parameters:

Name In Required Type Description
authorization header no string | null

Request body:

Required.

Responses:

Status Body
200 application/json → object
422 application/json → HTTPValidationError

POST /api/v1/devices/{device_id}/heartbeat#

Heartbeat Single

Single-device presence heartbeat.

Companion to the batch endpoint for tests and low-fan-out tools. If at is omitted, uses the current server time. Rejects with 404 (unknown), 410 (revoked), or 403 (cross-tenant) rather than the per-entry rejected structure — single-device semantics prefer clear status codes.

Parameters:

Name In Required Type Description
device_id path yes string
at query no string (date-time) | null
authorization header no string | null

Responses:

Status Body
204 Successful Response
422 application/json → HTTPValidationError

GET /api/v1/me/devices#

List My Devices

List the caller's devices, most-recent first.

include_revoked=true includes soft-deleted entries — useful for a "devices you've ever registered" view. Defaults to live only.

Parameters:

Name In Required Type Description
include_revoked query no boolean
authorization header no string | null

Responses:

Status Body
200 application/json → object
422 application/json → HTTPValidationError

POST /api/v1/me/devices#

Enrol Device

Enrol a new device with its two public keys.

Response codes:

  • 201 — a new device row was created.
  • 200 — Idempotency-Key match: the same key arrived before with identical (kem_pub, sig_pub); the existing device is returned unchanged, no webhook fires.
  • 409 IDEMPOTENCY_CONFLICT — the same key arrived before but with a different body. Client bug or replay from a different device state.
  • 409 DEVICE_LIMIT_EXCEEDED — user already holds settings.devices.max_per_user live devices. Revoke one first.

Parameters:

Name In Required Type Description
Idempotency-Key header no string | null Optional client-supplied key. If a request with the same Idempotency-Key and identical (kem_pub, sig_pub) arrives within 24h, ScaiKey returns the existing device row with HTTP 200 rather than creating a duplicate. Conflicting bodies under the same key return 409 IDEMPOTENCY_CONFLICT.
X-Request-ID header no string | null
authorization header no string | null

Request body:

Required.

Responses:

Status Body
200 application/json → object
422 application/json → HTTPValidationError

DELETE /api/v1/me/devices/{device_id}#

Revoke My Device

User self-revokes a device.

Soft-delete — sets revoked_at and revoked_reason, preserves the row. Fires device.revoked. Subsequent directory lookups filter this device out; audit and forensic queries can still see it. Idempotent: a second DELETE on the same device returns 404 because the first one moved the row out of the non-revoked view; no additional webhook fires.

Args: reason: Optional query-param override picking a canonical reason from _REVOKED_REASONS. Default user_revoked.

Parameters:

Name In Required Type Description
device_id path yes string
reason query no string | null
X-Request-ID header no string | null
authorization header no string | null

Responses:

Status Body
204 Successful Response
422 application/json → HTTPValidationError

POST /api/v1/me/devices/{device_id}/heartbeat#

Heartbeat Device

Update the device's last_seen timestamp.

Deliberately shaped as a fire-and-forget 204. The device (or an upstream service acting on its behalf — ScaiGrid's WS layer for ScaiClip) posts on a cadence; consumers derive online at query time from last_seen + a staleness threshold. No webhook fires — heartbeats are noisy by construction and consumers don't want to invalidate caches on every one.

Parameters:

Name In Required Type Description
device_id path yes string
authorization header no string | null

Responses:

Status Body
204 Successful Response
422 application/json → HTTPValidationError

PUT /api/v1/me/devices/{device_id}/keys#

Rotate Device Keys

Rotate a device's public keys.

Bumps key_version by one, sets keys_rotated_at, replaces both public keys in one transaction, fires device.keys.rotated. The old public key material is overwritten — we don't keep a history table (that's the client's concern for its channel-rewrap flow). Revoked devices refuse rotation.

Parameters:

Name In Required Type Description
device_id path yes string
X-Request-ID header no string | null
authorization header no string | null

Request body:

Required.

Responses:

Status Body
200 application/json → object
422 application/json → HTTPValidationError

GET /api/v1/tenants/{tenant_id}/devices#

Bulk Lookup Devices

Parameters:

Name In Required Type Description
tenant_id path yes string
subjects query yes string Comma-separated list of user subjects. Canonical form: usr_… stable ID. URN form urn:scaikey:{tenant_slug}:usr:{user_id} also accepted as a compatibility shape for SCWP-native callers. Both resolve to the same user. Capped at 100 entries per request.
include_revoked query no boolean Include soft-deleted devices in the response.
authorization header no string | null

Responses:

Status Body
200 application/json → object
422 application/json → HTTPValidationError

GET /api/v1/tenants/{tenant_id}/groups/{group_id}/devices#

Group Devices Transitive

Parameters:

Name In Required Type Description
tenant_id path yes string
group_id path yes string
include_revoked query no boolean Include soft-deleted devices.
authorization header no string | null

Responses:

Status Body
200 application/json → object
422 application/json → HTTPValidationError

Schemas#

Definitions for every type referenced by the endpoints above. Schema-to-schema references on this page link within the page; cross-page references would require visiting the linked page.

DeviceEnrolRequest#

Request body for enrolling a new device.

Field Type Required Description
kem_pub string yes X25519 KEM public key, 32 bytes, base64-encoded (url-safe or standard).
sig_pub string yes Ed25519 signing public key, 32 bytes, base64-encoded.
kind UserDeviceKind no Informational device category — 'DESKTOP', 'MOBILE', etc.
platform string | null no Platform identifier — 'macos', 'ios-17', 'android-14', etc.
display_name string | null no User-supplied label — 'Marcel's MacBook Pro'. Optional.

DeviceRotateKeysRequest#

Request body for rotating a device's keys.

Field Type Required Description
kem_pub string yes New X25519 KEM public key, base64-encoded.
sig_pub string yes New Ed25519 signing public key, base64-encoded.

HTTPValidationError#

Field Type Required Description
detail array of ValidationError no

HeartbeatBatchRequest#

Field Type Required Description
heartbeats array of HeartbeatEntry no One entry per observed device. Cap: 500 per request.

HeartbeatEntry#

Field Type Required Description
device_id string yes
at string (date-time) yes ISO 8601 timestamp of the observed activity. Ignored on the server if older than the row's current last_seen (latest-write-wins).

UserDeviceKind#

Broad category of the device — informational only.

Callers surface this in UI ("your Mac", "your phone") and can filter on it in the directory query (e.g. "give me only mobile devices"), but no ScaiKey authz decision is keyed off kind. Add new variants freely; do not remove existing ones without a migration.

Type: string (enum)

Values: DESKTOP, MOBILE, SERVER, BROWSER, OTHER

ValidationError#

Field Type Required Description
loc array of string | integer yes
msg string yes
type string yes
Updated 2026-09-27 14:47:46 View source (.md) rev 7