Pairing devices
A device (a “worker” in the API, ids starting wkr_) is paired by an account that is already signed in. Pairing follows the OAuth device flow: a device with no account shows a code, and the account claims it.
Device Mailcycle API Account │ │ │ ├─ POST /workers/pairing-session ► │ │◄─ { token, code } ─────────────┤ │ ├─ show QR(token) and code │ │ ├─ GET …/pairing-session/:token ─► │ │◄─ state: waiting ──────────────┤ │ │ │◄─ POST /workers/pair/lookup ───┤ │◄─ state: operator_detected ────┤ │ │ │◄─ POST /workers/pair ──────────┤ │◄─ awaiting_inbox + credential ─┤ (credential sent once) │ │ │◄─ POST /workers/:id/inboxes ───┤ │ │◄─ POST /workers/:id/keys ──────┤ │◄─ state: inbox_assigned ───────┤ │ └─ open mail │ │On the device
Section titled “On the device”POST /workers/pairing-sessionwith aplatform(ios,android,web,unknown, oragentfor an AI agent), adeviceName, adeviceKey: an X25519 public key, base64url, that the account can seal a secret to, and optionally aninstallId, a random id the device keeps across pairings. No token needed. The response carries a pairingtokenand an 8-digitcode. Show the token as a QR code, and the code for typing.- Poll
GET /workers/pairing-session/:token. The state moves throughwaiting,operator_detected,awaiting_inboxandinbox_assigned. The device credential is returned once, on the first poll after the account claims the device. The response also carries the sealedkeyBundleonce the account has handed over keys, and, on the typed-code path, the sealedwrapSeal. - To give up,
DELETE /workers/pairing-session/:token. Do this whenever the pairing screen closes, so an account holding the code stops waiting for this device. - To unpair later,
DELETE /workers/selfwith the device credential, then forget the credential. The account removes the device and hears it at once.
On the account
Section titled “On the account”Looking up a code, claiming a device and handing it keys all need a signed-in session. An API key gets 403 session_required.
Each SDK does the steps below for you, for example mc.devices.pair() and mc.devices.assign() in TypeScript.
POST /workers/pair/lookupwith the 8-digitcodefinds the waiting session, and returns the device’sdeviceKeyif it sent one.POST /workers/pairclaims it, with thesessionId, a client-generatedworkerId(wkr_…) and the sealedprofile. The claim must come from the account that looked the code up; otherwise it returns409 lookup_required. On Free, a second device returnsworker_limit_reached. A device that sent aninstallIdthis account has paired before replaces its old entry: the old one is removed, its credential revoked and its addresses left unassigned, so pairing the same phone again never adds a second device.- Assign it an address with
POST /workers/:id/inboxes, then hand it the sealed address keys withPOST /workers/:id/keysand akeyBundle. Do the same each time you assign it another address.
Pairing a fleet
Section titled “Pairing a fleet”Nothing here is per device. A stack of devices can be read first and claimed afterwards: look every code up, hold the previews (and, on the QR path, each device’s wrapping secret) in memory, then claim them one after another and assign addresses at the end.
Two things make that safe. Sessions have no timer, so a code read ten minutes ago is still good. And only failed lookups count against the rate limit, so a batch of correct codes is never refused for its size.
A device can leave while you hold its code. Listen on the event stream: a sync frame with the scope pairing means a code you looked up was given up, and worker.removed means a device you claimed has gone. Either way, ask POST /workers/pair/status about the codes you hold and drop every one that comes back gone. Ask again after the stream reconnects.
Claim one at a time rather than in parallel: each claim mints a credential, and the address keys that follow are handed over as one bundle per device. Watch for worker_limit_reached on the first claim that hits the plan’s device cap, since every claim after it fails the same way.
How the keys get there
Section titled “How the keys get there”The address keys are sealed on the account’s side under a wrapping secret the device holds, and the server relays a blob it cannot open.
- QR code. The device puts its wrapping secret in the QR code, and the account reads it with the camera. The secret never crosses the network. This is
keyTransport: optical. - Typed code. The 8 digits carry no secret. Instead the account mints a wrapping secret, seals it to the device’s
deviceKey, and sends it with the claim aswrapSeal. The device opens it with its own private key. The server cannot read it by watching, but it relays thedeviceKey, so it could in principle substitute its own at pairing. This iskeyTransport: relayed. Scan the QR code where you can.
Don’t claim a device by typed code if it sent no deviceKey: it could never open its mail.
Guarantees
Section titled “Guarantees”- Pairing tokens are random, generated by the server and stored only as a hash.
- The 8-digit code is a lookup key onto the token, not the secret itself.
- Sessions have no timer, so a device can wait as long as it needs. Each is used once. Sessions nobody claims are removed after 14 days.
- The device credential is minted only after the account confirms, is scoped to that device, and can be revoked on its own.
- Code lookups are limited: after ten failed lookups in five minutes, lookups return
429. - The server keeps only a hash of the
installIdtogether with the account id, so the same device cannot be matched across accounts. The id is dropped from the pairing session when it is claimed.
Once paired
Section titled “Once paired”- A device reads, marks, stars, moves and deletes the mail at its own addresses with its credential, and opens it with the keys it was handed. See Reading mail.
- On Personal and above it can send from those addresses. See Sending mail.
- Every request a device makes stamps its
lastActiveAt, from the server’s clock, rounded down to five minutes.POST /workers/heartbeatalso reports activity. A device is online if it was active in the last 15 minutes. - An open device can hold a live connection,
GET /workers/live, to hear at once when it is removed, an address is assigned to it or unassigned, or mail arrives at one of its addresses. See Devices. Without it, a device finds out at its next request.
Managing devices
Section titled “Managing devices”GET /workers lists devices, PATCH /workers/:id replaces the sealed profile or sets status to disabled, and DELETE /workers/:id removes a device. Its addresses stay on the account, unassigned. Changing or removing a device needs a signed-in session; an API key can only list and read them. See Devices.
© Northlab Studios Ltd, trading as Mailcycle. Registered in England and Wales, company no. 17462734. Registered office: 167-169 Great Portland Street, London W1W 5PF.