Pairing & authentication
Device pairing flow, bearer tokens, device registry, token rotation, service tokens, and rate limiting.
Every /api/* route on the Revka gateway is protected by bearer-token authentication, and the only way to mint a token is the pairing flow. A device exchanges a one-time pairing code for a persistent bearer token, the token gates all subsequent requests, and the device is recorded in a SQLite-backed registry you can list, revoke, or rotate. This page covers the full flow — code retrieval, the legacy and enhanced pairing endpoints, the device registry, token rotation, the internal service token, and the rate limits that protect all of it.
Reach for this page when you are pairing a browser, phone, or headless script with a running gateway, building a client against the REST API, or hardening the auth surface. For the CLI side of the same flow (revka gateway get-paircode, revka pair token) see revka gateway, daemon & service; for the cryptographic design behind tokens and secrets see Secrets, pairing & device auth.
How authentication works
Section titled “How authentication works”With [gateway].require_pairing = true (the default), the gateway prints a 6-digit one-time pairing code on first start. A client posts that code to a pairing endpoint and receives a 256-bit bearer token prefixed rk_. The token is stored only as a SHA-256 hash — the plaintext is shown exactly once — and is sent on every later call:
Authorization: Bearer rk_<token>[gateway]require_pairing = true # default — set false to disable auth (local-only use)allow_public_bind = false # must be explicit to bind beyond localhostTokens persist to config on success (persist_pairing_tokens), so a daemon restart does not require re-pairing. Existing tokens are accepted in plaintext (rk_...) or pre-hashed (64-char hex) form, and legacy zc_-prefixed tokens are still honored for backward compatibility.
Retrieve a pairing code
Section titled “Retrieve a pairing code”The gateway exposes the current outstanding code to localhost callers so a host-side dashboard can render a QR code or copy button, and through localhost-only admin endpoints for the CLI.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /pair/code | Localhost only | Return the current outstanding pairing code |
GET | /admin/paircode | Localhost only | Return the current code (used by revka gateway get-paircode) |
POST | /admin/paircode/new | Localhost only | Generate a fresh code (used by --new) |
# From the CLI (calls the admin endpoints under the hood)revka gateway get-paircode # show the current coderevka gateway get-paircode --new # rotate to a fresh codeCodes are single-use: once a client pairs with a code it is consumed, and you must rotate to issue another.
Exchange a code for a bearer token
Section titled “Exchange a code for a bearer token”There are two pairing endpoints. Both return a bearer token; pick based on whether your client sends headers or a JSON body.
Legacy: POST /pair (header-based)
Section titled “Legacy: POST /pair (header-based)”The original pairing path. Send the code in the X-Pairing-Code header, optionally with device-metadata headers.
POST /pairX-Pairing-Code: 123456X-Revka-Device-Name: My LaptopX-Revka-Device-Type: cliX-Revka-Device-Hardware: MacBook Pro / macOS{ "paired": true, "persisted": true, "token": "rk_<64 hex chars>", "message": "Save this token — use it as Authorization: Bearer <token>"}| Header | Required | Meaning |
|---|---|---|
X-Pairing-Code | yes | The one-time code shown at startup |
X-Revka-Device-Name | no | Device label shown on the dashboard Pairing page |
X-Revka-Device-Type | no | Device type label (e.g. cli, mobile) |
X-Revka-Device-Hardware | no | Hardware / platform label |
Enhanced: POST /api/pair (JSON body)
Section titled “Enhanced: POST /api/pair (JSON body)”The preferred API-level path, used by the dashboard. It accepts the code plus device metadata in a JSON body and writes to both the pairing guard and the device registry.
curl -X POST http://127.0.0.1:42617/api/pair \ -H 'Content-Type: application/json' \ -d '{"code": "123456", "device_name": "My Phone", "device_type": "mobile", "hardware": "iOS"}'{ "token": "rk_<64 hex chars>", "persisted": true, "message": "Pairing successful"}| Field | Type | Required | Meaning |
|---|---|---|---|
code | string | yes | The one-time pairing code |
device_name | string | no | Device label (truncated to 120 chars) |
device_type | string | no | Device type label (truncated to 120 chars) |
hardware | string | no | Hardware / platform label (truncated to 120 chars) |
Failure responses:
400 Bad Request— invalid or expired code429 Too Many Requests—Too many attempts. Locked out for Ns(see Rate limiting)
Initiate pairing from an existing device
Section titled “Initiate pairing from an existing device”An already-paired client can mint a new pairing code on demand — no daemon restart needed — so a second device can pair. This is the dashboard “Pair a new device” flow.
POST /api/pairing/initiateAuthorization: Bearer rk_<existing-token>The response carries a short-lived code (typically rendered as a QR code). The full loop:
-
On an already-paired device (e.g. the dashboard), call
POST /api/pairing/initiateto generate a code. -
Display the code as a QR or copy it to the new device.
-
On the new device, submit it to
POST /api/pair(orPOST /pair) to receive that device’s own bearer token. -
The new device appears in the device registry and on the dashboard Pairing page.
Device registry, revocation & rotation
Section titled “Device registry, revocation & rotation”Paired devices are persisted in a SQLite registry at <workspace_dir>/devices.db, so they survive daemon restarts. Manage them over the API (all bearer-authenticated):
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/devices | Bearer | List paired devices |
DELETE | /api/devices/{id} | Bearer | Revoke a device — invalidates its token immediately |
POST | /api/devices/{id}/token/rotate | Bearer | Rotate: issue a new pairing code so the device can re-pair |
# List paired devicescurl http://127.0.0.1:42617/api/devices \ -H "Authorization: Bearer rk_<token>"
# Revoke a device by its registry UUIDcurl -X DELETE http://127.0.0.1:42617/api/devices/<device-id> \ -H "Authorization: Bearer rk_<token>"Each device record carries:
| Field | Meaning |
|---|---|
id | Device UUID from the registry (use in the path) |
name | Device name supplied at pairing |
device_type | Device type label |
hardware | Hardware / platform label |
paired_at | When the device first paired |
last_seen | Last authenticated request from the device |
ip_address | Source IP recorded for the device |
Service token (X-Revka-Service-Token)
Section titled “Service token (X-Revka-Service-Token)”Trusted local sidecars — chiefly the operator-mcp runtime — authenticate with an internal service token instead of a user bearer token. The service token is auto-generated at gateway startup and persisted to <state_dir>/service-token (mode 0600 on POSIX). It is sent in its own header:
X-Revka-Service-Token: <service-token>This header is accepted alongside Authorization: Bearer on the routes that need privileged, machine-to-machine access. Notable service-token-only endpoints:
| Method | Path | Why service-token-only |
|---|---|---|
POST | /api/cost/usage | Mutates the budget ledger — sidecars report token usage here |
POST | /api/auth/profiles/{id}/resolve | Decrypts a stored workflow credential at step-execution time |
# A trusted sidecar reports token usage to the cost ledgercurl -X POST http://127.0.0.1:42617/api/cost/usage \ -H "X-Revka-Service-Token: $(cat ~/.revka/service-token)" \ -H 'Content-Type: application/json' \ -d '{"model":"anthropic/claude-sonnet-4","provider":"openrouter","input_tokens":1200,"output_tokens":340}'Rate limiting
Section titled “Rate limiting”Two independent layers protect the auth surface against brute force, both keyed per client IP with a sliding window.
Auth rate limiter (brute-force lockout)
Section titled “Auth rate limiter (brute-force lockout)”The AuthRateLimiter guards the pairing and bearer-token endpoints:
| Constant | Value | Meaning |
|---|---|---|
MAX_ATTEMPTS | 10 | Failures allowed per window |
WINDOW_SECS | 60 | Sliding window duration |
LOCKOUT_SECS | 300 | Lockout after the limit is breached (5 minutes) |
On lockout the endpoint returns 429 Too Many Requests with a Too many attempts. Locked out for Ns message giving the seconds until unlock. Stale entries are swept every 5 minutes to bound memory.
Gateway-level pairing rate limit (config)
Section titled “Gateway-level pairing rate limit (config)”Separately, the gateway applies a per-minute sliding-window limiter to the pairing (and webhook) routes, tunable in config:
[gateway]pair_rate_limit_per_minute = 10 # 0 disables the pairing rate limitwebhook_rate_limit_per_minute = 60 # 0 disables the webhook rate limitrate_limit_max_keys = 10000 # max tracked IP keys (LRU eviction)trust_forwarded_headers = false # see caution below| Key | Type | Default | Meaning |
|---|---|---|---|
gateway.pair_rate_limit_per_minute | integer | 10 | Pairing attempts per minute per IP. 0 disables. |
gateway.webhook_rate_limit_per_minute | integer | 60 | Webhook requests per minute per IP. 0 disables. |
gateway.rate_limit_max_keys | integer | 10000 | Maximum tracked IP keys before eviction. |
gateway.trust_forwarded_headers | boolean | false | Use X-Forwarded-For / X-Real-IP for the client key. |
Quick start: pair a client
Section titled “Quick start: pair a client”-
Start the gateway and read the pairing code from its startup output (or fetch it).
Terminal window revka gatewayrevka gateway get-paircode -
Exchange the code for a token.
Terminal window curl -X POST http://127.0.0.1:42617/api/pair \-H 'Content-Type: application/json' \-d '{"code": "123456", "device_name": "My Laptop", "device_type": "cli"}' -
Save the returned
rk_token — it is shown only once. -
Call an authenticated endpoint to confirm.
Terminal window curl http://127.0.0.1:42617/api/status \-H "Authorization: Bearer rk_<token>"
Troubleshooting
Section titled “Troubleshooting”- “Too many attempts. Locked out for Ns”. You hit the auth rate limiter. Wait the reported seconds, or pair from
127.0.0.1(loopback is exempt). - Pairing input rejects your code. Codes are single-use. Rotate with
revka gateway get-paircode --newand retry. - Behind a proxy and getting locked out by a single shared IP. Set
trust_forwarded_headers = trueso the limiter keys on the real client IP — but only if the proxy is trusted. - Lost a device’s token. Use
POST /api/devices/{id}/token/rotateto issue a fresh code, orDELETE /api/devices/{id}then re-pair. 401on every/api/*call. Confirm theAuthorization: Bearer rk_<token>header is present and the token has not been revoked. Sidecars must useX-Revka-Service-Tokeninstead.