Rotate this device's own tunnel credential
const url = 'https://example.com/api/v1/tunnel-credential/rotate';const options = {method: 'POST', headers: {'X-API-Key': 'example', 'X-Device-ID': 'example'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://example.com/api/v1/tunnel-credential/rotate \ --header 'X-API-Key: example' \ --header 'X-Device-ID: example'Revoke the calling device’s active sotc_ tunnel credential(s) and
return a new one — authenticated by the device’s OWN API key.
Why this exists (BUG-367): an already-provisioned board whose tunnel
credential is stale used to re-mint it only through the operator route
POST /api/devices/{id}/tunnel-credential/rotate, which needs an admin
JWT. otapulse-auto-provision therefore logged in as an admin from the
device, which can never work against an MFA-mandatory admin, needs admin
credentials on the device, and the only MFA-free alternative
(re-provisioning with a provisioning token) rotates the device’s API key
as a side effect. This route heals ONLY the tunnel credential.
Authorization — a key can only ever mint a credential for its own device:
X-API-Keymust be an active, unexpired key (401 otherwise).- The key must be DEVICE-BOUND (
api_keys.device_idset). Org-scoped service keys are refused (403) even though other device routes accept them: minting a remote-shell credential is not something a fleet-wide key may do for an arbitrary device. X-Device-IDresolves only within the key’s organization (404 for a device of another org) and must be the key’s bound device (403 for any other device) — the GAP-BE-014 ruleget_device_from_headeralready enforces, re-asserted here.- The key needs
devices:writeand the device must be active (403).
Rate limited per source IP (per minute) and per device (per hour), and every successful rotation writes an audit-log row.
Parameters
Section titled “ Parameters ”Header Parameters
Section titled “ Header Parameters ”Responses
Section titled “ Responses ”Successful Response
The operator rotate response plus the caller’s own backend device
UUID, which otapulse-auto-provision records as device_uuid= in
its tunnel marker (engine/provision.py cross-checks that value).
object
Example generated
{ "device_id": "example", "rotated_at": "2026-04-15T12:00:00Z", "rotation_count": 1, "tunnel_token": "example", "tunnel_token_prefix": "example"}Validation Error
object
object
object
Example generated
{ "detail": [ { "ctx": {}, "input": "example", "loc": [ "example" ], "msg": "example", "type": "example" } ]}