Device flow
Authenticate CLI tools without a browser: poll the device endpoint while the user approves elsewhere.
Published ·Updated
The Device Flow (RFC 8628) lets users authorize your app from a separate device with a browser. This is ideal for CLI tools, IoT devices, and environments without direct browser access.
Flow Overview#
- Your app requests a device code from Orshot
- Orshot returns a
user_codeand averification_uri_completelink - You show the link and the code to the user
- The user opens the link in a browser, checks the code matches, and approves access
- Meanwhile, your app polls the token endpoint until the user completes authorization
Step 1: Request a Device Code#
curl -X POST https://api.orshot.com/v1/oauth/device/code \
-H "Content-Type: application/json" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"scope": "workspace:read render:generate"
}'Response:
{
"device_code": "device-code-string",
"user_code": "A1B2-C3D4",
"verification_uri": "https://orshot.com/oauth/device",
"verification_uri_complete": "https://orshot.com/oauth/device?user_code=A1B2-C3D4",
"expires_in": 600,
"interval": 5
}| Field | Description |
|---|---|
device_code | Used by your app to poll for the token (keep this secret) |
user_code | Show it to the user so they can check it matches the approval page |
verification_uri | The base approval URL. It has no field to type a code, so do not send users here on its own |
verification_uri_complete | The link to show the user. It opens the approval page with the code filled in |
expires_in | How long the codes are valid (600 seconds = 10 minutes) |
interval | Minimum seconds between polling requests |
Step 2: Display the Code#
Show the user the verification_uri_complete link and the code:
To authorize this app, open:
https://orshot.com/oauth/device?user_code=A1B2-C3D4
Check that the page shows this code:
A1B2-C3D4You can also show the link as a QR code. The bare verification_uri has no field to type a code, so always use verification_uri_complete.
Step 3: Poll for Tokens#
While the user is authorizing, poll the token endpoint at the specified interval:
curl -X POST https://api.orshot.com/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"client_id": "YOUR_CLIENT_ID",
"device_code": "device-code-string"
}'While waiting (user hasn't approved yet):
{
"error": "authorization_pending"
}After the user approves:
{
"access_token": "ost_...",
"refresh_token": "osr_...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "workspace:read render:generate",
"user_id": "user-uuid",
"workspace_ids": [50]
}Polling Errors#
All polling errors return HTTP 400 (401 for invalid_client).
| Error | Meaning | Action |
|---|---|---|
authorization_pending | User hasn't approved yet | Keep polling at the specified interval |
expired_token | The device code has expired | Start over from Step 1 |
invalid_grant | The code was already used or is not for this client | Start over from Step 1 |
invalid_request | Missing or invalid parameters | Fix the request |
invalid_client | Unknown or disabled client ID | Check your client ID |
If the user clicks Cancel on the approval page, you keep getting authorization_pending until the code expires after 10 minutes.
Access tokens last the number of seconds in expires_in. Read it from each response. Do not hard-code it.
Full Example (Node.js CLI)#
const CLIENT_ID = process.env.ORSHOT_CLIENT_ID;
async function authenticate() {
// Step 1: Get device code
const codeRes = await fetch("https://api.orshot.com/v1/oauth/device/code", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: CLIENT_ID,
scope: "workspace:read render:generate",
}),
});
const { device_code, user_code, verification_uri_complete, interval } =
await codeRes.json();
// Step 2: Show the user
console.log(`\nOpen: ${verification_uri_complete}`);
console.log(`Check the page shows: ${user_code}\n`);
console.log("Waiting for authorization...");
// Step 3: Poll for tokens
let pollInterval = interval * 1000;
while (true) {
await new Promise((r) => setTimeout(r, pollInterval));
const tokenRes = await fetch("https://api.orshot.com/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: CLIENT_ID,
device_code,
}),
});
const data = await tokenRes.json();
if (data.access_token) {
console.log(
"Authorized! Access granted to workspaces:",
data.workspace_ids,
);
return data;
}
if (data.error === "slow_down") {
pollInterval += 5000;
} else if (data.error !== "authorization_pending") {
throw new Error(`Authorization failed: ${data.error}`);
}
}
}User Experience Tips#
- Always show
verification_uri_complete. The approval page needs the code in the link - Display a QR code linking to
verification_uri_completefor mobile-friendly approval - Show a clear "Waiting for authorization..." message while polling
- Handle
expired_tokengracefully — offer to restart the flow
Ready to automate?
Start rendering images, PDFs and videos from your templates in under 2 minutes. Free plan, no credit card.
Get your API key- Image, PDF and video generation via API
- Visual editor with AI and smart layouts
- Zapier, Make, MCP and 40+ integrations
- White-label embed for your own app
- 100 free credits a month, no credit card required