The three calls
| Call | Auth | Body | Returns |
|---|---|---|---|
POST /api/v1/device-pairing | X-DEVICE-KEY | none | CreatePairingResponse |
PUT /api/v1/device-pairing/{pairing_id} | Authorization: Bearer <write_token> | WritePairingRequest | 204 |
GET /api/v1/device-pairing/{pairing_id} | X-DEVICE-KEY | none | PollPairingResponse |
The flow
Desktop mints the pairing
The desktop calls
POST /api/v1/device-pairing with its X-DEVICE-KEY. The response is { pairing_id, write_token, expires_in_secs }. The write_token is returned once and is single-use; the exchange keeps no recoverable copy.Desktop encodes the QR
The desktop puts
pairing_id and write_token into a QR code on screen. The phone scans it. Nothing secret about the account travels here — the write token authorizes exactly one write to exactly one mailbox.Phone deposits its public keys
The phone calls
PUT /api/v1/device-pairing/{pairing_id} with Authorization: Bearer <write_token> and a body of its two public keys. On success the exchange returns 204 and the write token is spent.The write body
The phone sends both public keys, each standard base64 (+ / / alphabet, = padding). URL-safe base64 is rejected.
Ed25519 session public key, exactly 32 bytes, standard base64.
Uncompressed P-256 public key, exactly 65 bytes with a leading
0x04, standard base64.400, not 401 — the token was valid, the body was not. Encode both keys as standard base64 before sending.
The poll response
GET returns one of two shapes, distinguished by status. Branch on status, not on field presence.
pending while the mailbox is empty, then ready once the phone has written.Present only when
status is ready. The phone’s Ed25519 session public key.Present only when
status is ready. The phone’s P-256 ECDH public key.Lifetime and single use
The write token expires after theexpires_in_secs returned by the mint. Read that value; do not hardcode a duration. The token is also single-use: the first successful PUT spends it, and any further write fails. A reused, expired, or missing token is rejected 401. Once a pairing is complete, a second write returns 409 with code pairing_already_completed.
If the pairing expires before the phone writes, mint a fresh one. There is no extend call.
Errors
Failures carryapplication/problem+json (RFC 9457) with a stable code; branch on code, not on the human-readable detail. See the error model for the parsing rule.
| Status | When | What to do |
|---|---|---|
400 | Malformed key encoding or length on PUT | Re-encode both keys as standard base64 at the stated byte lengths |
401 | Write token missing, expired, or already spent | Mint a new pairing; the old token cannot be reused |
404 | Unknown, expired, or foreign pairing_id | Mint a new pairing; a pairing_id belongs to the account that minted it |
409 | Pairing already completed (pairing_already_completed) | Stop writing; the desktop should poll and read the keys |