Skip to content

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.

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 localhost

Tokens 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.

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.

MethodPathAuthPurpose
GET/pair/codeLocalhost onlyReturn the current outstanding pairing code
GET/admin/paircodeLocalhost onlyReturn the current code (used by revka gateway get-paircode)
POST/admin/paircode/newLocalhost onlyGenerate a fresh code (used by --new)
Terminal window
# From the CLI (calls the admin endpoints under the hood)
revka gateway get-paircode # show the current code
revka gateway get-paircode --new # rotate to a fresh code

Codes are single-use: once a client pairs with a code it is consumed, and you must rotate to issue another.

There are two pairing endpoints. Both return a bearer token; pick based on whether your client sends headers or a JSON body.

The original pairing path. Send the code in the X-Pairing-Code header, optionally with device-metadata headers.

POST /pair
X-Pairing-Code: 123456
X-Revka-Device-Name: My Laptop
X-Revka-Device-Type: cli
X-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>"
}
HeaderRequiredMeaning
X-Pairing-CodeyesThe one-time code shown at startup
X-Revka-Device-NamenoDevice label shown on the dashboard Pairing page
X-Revka-Device-TypenoDevice type label (e.g. cli, mobile)
X-Revka-Device-HardwarenoHardware / platform label

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.

Terminal window
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"
}
FieldTypeRequiredMeaning
codestringyesThe one-time pairing code
device_namestringnoDevice label (truncated to 120 chars)
device_typestringnoDevice type label (truncated to 120 chars)
hardwarestringnoHardware / platform label (truncated to 120 chars)

Failure responses:

  • 400 Bad Request — invalid or expired code
  • 429 Too Many RequestsToo many attempts. Locked out for Ns (see Rate limiting)

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/initiate
Authorization: Bearer rk_<existing-token>

The response carries a short-lived code (typically rendered as a QR code). The full loop:

  1. On an already-paired device (e.g. the dashboard), call POST /api/pairing/initiate to generate a code.

  2. Display the code as a QR or copy it to the new device.

  3. On the new device, submit it to POST /api/pair (or POST /pair) to receive that device’s own bearer token.

  4. The new device appears in the device registry and on the dashboard Pairing page.

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):

MethodPathAuthPurpose
GET/api/devicesBearerList paired devices
DELETE/api/devices/{id}BearerRevoke a device — invalidates its token immediately
POST/api/devices/{id}/token/rotateBearerRotate: issue a new pairing code so the device can re-pair
Terminal window
# List paired devices
curl http://127.0.0.1:42617/api/devices \
-H "Authorization: Bearer rk_<token>"
# Revoke a device by its registry UUID
curl -X DELETE http://127.0.0.1:42617/api/devices/<device-id> \
-H "Authorization: Bearer rk_<token>"

Each device record carries:

FieldMeaning
idDevice UUID from the registry (use in the path)
nameDevice name supplied at pairing
device_typeDevice type label
hardwareHardware / platform label
paired_atWhen the device first paired
last_seenLast authenticated request from the device
ip_addressSource IP recorded for the device

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:

MethodPathWhy service-token-only
POST/api/cost/usageMutates the budget ledger — sidecars report token usage here
POST/api/auth/profiles/{id}/resolveDecrypts a stored workflow credential at step-execution time
Terminal window
# A trusted sidecar reports token usage to the cost ledger
curl -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}'

Two independent layers protect the auth surface against brute force, both keyed per client IP with a sliding window.

The AuthRateLimiter guards the pairing and bearer-token endpoints:

ConstantValueMeaning
MAX_ATTEMPTS10Failures allowed per window
WINDOW_SECS60Sliding window duration
LOCKOUT_SECS300Lockout 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.

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 limit
webhook_rate_limit_per_minute = 60 # 0 disables the webhook rate limit
rate_limit_max_keys = 10000 # max tracked IP keys (LRU eviction)
trust_forwarded_headers = false # see caution below
KeyTypeDefaultMeaning
gateway.pair_rate_limit_per_minuteinteger10Pairing attempts per minute per IP. 0 disables.
gateway.webhook_rate_limit_per_minuteinteger60Webhook requests per minute per IP. 0 disables.
gateway.rate_limit_max_keysinteger10000Maximum tracked IP keys before eviction.
gateway.trust_forwarded_headersbooleanfalseUse X-Forwarded-For / X-Real-IP for the client key.
  1. Start the gateway and read the pairing code from its startup output (or fetch it).

    Terminal window
    revka gateway
    revka gateway get-paircode
  2. 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"}'
  3. Save the returned rk_ token — it is shown only once.

  4. Call an authenticated endpoint to confirm.

    Terminal window
    curl http://127.0.0.1:42617/api/status \
    -H "Authorization: Bearer rk_<token>"
  • “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 --new and retry.
  • Behind a proxy and getting locked out by a single shared IP. Set trust_forwarded_headers = true so 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/rotate to issue a fresh code, or DELETE /api/devices/{id} then re-pair.
  • 401 on every /api/* call. Confirm the Authorization: Bearer rk_<token> header is present and the token has not been revoked. Sidecars must use X-Revka-Service-Token instead.