Authorization Code flow
Step-by-step PKCE flow: build the authorize URL, exchange the code, refresh the token.
Published ·Updated
The Authorization Code flow with PKCE is the recommended way to authenticate users. It works for web apps, desktop apps, and IDE extensions.
Flow Overview#
- Your app generates a PKCE code verifier and challenge
- You redirect the user to Orshot's authorization URL
- The user signs in (if needed) and approves access to specific workspaces
- Orshot redirects back to your app with an authorization code
- Your app exchanges the code + verifier for access and refresh tokens
Step 1: Generate PKCE Challenge#
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. Generate a random code_verifier and derive a code_challenge from it using SHA-256.
// Generate a random code verifier
const codeVerifier = crypto.randomBytes(32).toString("base64url");
// Derive the challenge using SHA-256
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");import hashlib
import secrets
import base64
code_verifier = secrets.token_urlsafe(32)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()Step 2: Redirect to Authorization#
Build the authorization URL and redirect the user:
https://orshot.com/oauth/authorize?
client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/callback
&response_type=code
&code_challenge=GENERATED_CHALLENGE
&code_challenge_method=S256
&scope=workspace:read render:generate
&state=RANDOM_STATE_VALUE| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your app's client ID |
redirect_uri | Yes | Must match a registered redirect URI |
response_type | Yes | Always code |
code_challenge | Yes | The PKCE challenge derived in Step 1 |
code_challenge_method | Yes | Always S256 |
scope | Recommended | Space-separated list of scopes. If you leave it out, Orshot uses every scope your app registered |
state | Recommended | Random string to prevent CSRF attacks |
Step 3: User Approves#
Orshot shows a consent screen where the user:
- Signs in to their Orshot account (if not already signed in)
- Sees your app's name, logo, and requested permissions
- Selects which workspaces to grant access to
- Clicks "Allow Access" to continue, or "Cancel"
If your app is in development mode, the consent screen shows an "unverified app" warning. Only the app owner and test users can proceed — other users will be blocked.
Step 4: Handle the Redirect#
After approval, Orshot redirects the user to your redirect_uri with an authorization code:
https://your-app.com/callback?code=osc_abc123...&state=YOUR_STATE_VALUEAlways verify the state parameter matches what you sent in Step 2 to prevent CSRF attacks.
If the user clicks Cancel, the redirect includes an error parameter:
https://your-app.com/callback?error=access_denied&error_description=User%20denied%20the%20authorization%20request&state=YOUR_STATE_VALUEThis is the only error sent to your redirect_uri. Problems with the authorize request itself are shown to the user on Orshot's consent screen. See Error Responses.
Step 5: Exchange Code for Tokens#
POST to the token endpoint to exchange the authorization code for tokens. The token endpoint does not take a client_secret: the code_verifier proves the request comes from your app.
curl -X POST https://api.orshot.com/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "YOUR_CLIENT_ID",
"code": "osc_abc123...",
"redirect_uri": "https://your-app.com/callback",
"code_verifier": "YOUR_CODE_VERIFIER"
}'Response:
{
"access_token": "ost_...",
"refresh_token": "osr_...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "workspace:read render:generate",
"user_id": "user-uuid",
"workspace_ids": [50, 51]
}| Field | Description |
|---|---|
access_token | Use this in API requests (Authorization: Bearer ost_...) |
refresh_token | Use this to get new access tokens (see Token Management) |
expires_in | Access token lifetime in seconds (900 = 15 minutes). Read it from each response. Do not hard-code it |
workspace_ids | The workspaces the user granted access to |
Full Example (Node.js)#
import crypto from "crypto";
import express from "express";
const app = express();
const CLIENT_ID = process.env.ORSHOT_CLIENT_ID;
const REDIRECT_URI = "http://localhost:3000/callback";
// Store verifiers by state (use a proper session store in production)
const pendingAuth = new Map();
app.get("/login", (req, res) => {
const state = crypto.randomBytes(16).toString("hex");
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");
pendingAuth.set(state, codeVerifier);
const url = new URL("https://orshot.com/oauth/authorize");
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("redirect_uri", REDIRECT_URI);
url.searchParams.set("response_type", "code");
url.searchParams.set("code_challenge", codeChallenge);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("scope", "workspace:read render:generate");
url.searchParams.set("state", state);
res.redirect(url.toString());
});
app.get("/callback", async (req, res) => {
const { code, state } = req.query;
const codeVerifier = pendingAuth.get(state);
if (!codeVerifier) {
return res.status(400).send("Invalid state");
}
pendingAuth.delete(state);
const response = await fetch("https://api.orshot.com/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
client_id: CLIENT_ID,
code,
redirect_uri: REDIRECT_URI,
code_verifier: codeVerifier,
}),
});
const tokens = await response.json();
// Store tokens securely — tokens.access_token, tokens.refresh_token
res.json({ success: true, workspace_ids: tokens.workspace_ids });
});
app.listen(3000);Error Responses#
Sent to your redirect_uri:
| Error | Description |
|---|---|
access_denied | The user clicked Cancel |
Shown to the user on Orshot's consent screen (your app does not receive these):
| Error | Description |
|---|---|
invalid_client | Unknown or disabled client ID |
invalid_scope | Requested scope not allowed for this client |
invalid_request | Missing or invalid parameters |
access_denied | The app is in development mode and the user is not the owner or a test user, or the user has no owner or admin access |
Returned by the token endpoint (HTTP 400, or 401 for invalid_client):
| Error | Description |
|---|---|
invalid_request | Missing or invalid parameters |
invalid_client | Unknown or disabled client ID |
invalid_grant | Unknown or already used code, redirect_uri mismatch, or wrong code_verifier |
expired_token | The code is older than 5 minutes |
unsupported_grant_type | The grant_type is not supported |
API calls made later with a token from a development-mode app can fail with sandbox_restricted. See Token Management.
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