For CLIs, varios, and other devices that don't have a browser or can't
safely embed one. The pattern: the device shows the user a short code
and asks them to open we-fly.cloud/oauth/device on any other device
(phone, laptop) to enter it.
If your integration is a regular web/server app, use the authorization-code flow instead.
Prerequisites
Register an OAuth application as a confidential client and tick
device_code in the grant types. (Public clients can also use device
code in principle, but in practice the threat model that motivates
"public client" — secret leak via shipped bundle — is identical to
device-code's threat model, so we only allow confidential.)
You'll get a client_id (wf_app_…) and a client_secret
(wf_secret_…).
Flow
Device we-fly User
│
│ 1. Start the flow.
│ ─────────────────────────────────► /api/oauth/device_authorization
│ │
│ 2. Receive device_code, user_code, verification_uri. │
│ ◄───────────────────────────────── │
│ │
│ 3. Show the user_code and the URL on screen. │
│ "Open https://we-fly.cloud/oauth/device and enter ABCD-1234" │
│ │
│ 4. Begin polling /token with the device_code. │
│ While the user hasn't authorised yet, we return │
│ authorization_pending — keep polling at the │
│ advertised `interval` (seconds). │
│ │
│ 5. User │
│ opens │
│ URL, │
│ enters │
│ code, │
│ consents│
│ │
│ 6. Next poll returns access_token + refresh_token. │
│ ◄───────────────────────────────── │
Step 1 — request a device code
curl -X POST https://we-fly.cloud/api/oauth/device_authorization \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d "scope=flights:read flights:write"
Response:
{
"device_code": "wf_dc_…",
"user_code": "WXYZ-1234",
"verification_uri": "https://we-fly.cloud/oauth/device",
"verification_uri_complete": "https://we-fly.cloud/oauth/device?user_code=WXYZ-1234",
"expires_in": 600,
"interval": 5
}
device_code— opaque secret your device polls with. Don't show it to the user.user_code— 8 characters in two groups (XXXX-XXXX) drawn from an unambiguous alphabet (no0/O, no1/I, no vowels). Show this prominently.verification_uri— URL the user opens.verification_uri_complete— same URL but pre-fills the code via a query string. Useful for "tap-to-open" buttons or QR codes.interval— minimum seconds between consecutive polls. Honour this. Polling faster yieldsslow_downerrors.expires_in— total lifetime of the codes (seconds). The user has this long to type the code before the device must restart the flow.
Step 2 — instruct the user
Display both the URL and the code prominently. Examples:
┌────────────────────────────────────────────┐
│ Connect this vario to we-fly │
│ │
│ 1. Open https://we-fly.cloud/oauth/device │
│ 2. Enter the code: WXYZ-1234 │
│ │
│ Waiting… │
└────────────────────────────────────────────┘
If you can render a QR code, encode the verification_uri_complete
value — the user just scans and lands on the consent screen.
Step 3 — poll /token
curl -X POST https://we-fly.cloud/api/oauth/token \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=${DEVICE_CODE}"
The grant_type value is the full URN, exactly as written. We translate
to a friendlier short name internally; the wire format is fixed by the
RFC.
While the user hasn't authorised, you'll get:
{
"error": "authorization_pending",
"error_description": "the user has not yet completed the authorization flow"
}
HTTP 400 — keep polling. Once the user clicks Authorize:
{
"access_token": "wf_at_…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "wf_rt_…",
"refresh_token_expires_in": 2592000,
"scope": "flights:read flights:write"
}
If the user clicks Cancel:
{
"error": "access_denied",
"error_description": "the user denied the request"
}
If you polled too fast you'll get slow_down instead of
authorization_pending. Increase your interval by 5 seconds and keep
polling.
If the code expires before the user enters it (the device timed them
out): expired_token. Restart the flow from step 1.
Reference implementation (Node.js)
import { setTimeout as sleep } from "node:timers/promises";
const DEVICE_URL = "https://we-fly.cloud/api/oauth/device_authorization";
const TOKEN_URL = "https://we-fly.cloud/api/oauth/token";
async function connect() {
const basic = Buffer.from(
`${process.env.WEFLY_CLIENT_ID}:${process.env.WEFLY_CLIENT_SECRET}`,
).toString("base64");
// Step 1: start the flow.
const init = await fetch(DEVICE_URL, {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ scope: "flights:read flights:write" }),
}).then((r) => r.json());
console.log(
`Open ${init.verification_uri} and enter ${init.user_code}`,
);
let interval = init.interval * 1000;
const deadline = Date.now() + init.expires_in * 1000;
// Step 3: poll.
while (Date.now() < deadline) {
await sleep(interval);
const res = await fetch(TOKEN_URL, {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: init.device_code,
}),
});
const body = await res.json();
if (res.ok) {
return body; // { access_token, refresh_token, … }
}
if (body.error === "authorization_pending") continue;
if (body.error === "slow_down") {
interval += 5000;
continue;
}
throw new Error(`flow failed: ${body.error_description ?? body.error}`);
}
throw new Error("device code expired before user authorised");
}
What you must implement
- Honour the
interval. Polling faster than advertised will earn youslow_downresponses; sustained violations are treated the same as 429 abuse and can get your app suspended. - Use the prefixed device_code (
wf_dc_…) as-is. We pattern-match on the prefix when scrubbing logs. - Don't reuse device_codes. Once a token has been issued, the code is consumed; if you present it again we treat it as a replay and revoke any tokens minted from it.
- Restart the flow if it times out. No silent re-prompt — start from step 1 with a fresh code.
Compared to authorization-code
| Device code | Authorization code + PKCE | |
|---|---|---|
| Requires browser on the same device? | No | Yes |
| Code visible to user | Yes | No (encoded in URL) |
| User types anything? | Yes (8 chars) | No |
| Suitable for SPAs? | Overkill | Yes (recommended) |
| Suitable for embedded devices (vario, Pi)? | Yes | Awkward |
| Polling required? | Yes (5s default) | No |
| User experience | Two-device | Single-device |
Pick device code when you genuinely don't have a usable browser on the device. Otherwise authorization-code + PKCE is simpler and faster.