Authorization Code Flow
Implement OAuth 2.0 Authorization Code with PKCE for your Orshot app
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 | Yes | Space-separated list of scopes |
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 "Approve" to continue
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 denies access, the redirect will include an error parameter:
https://your-app.com/callback?error=access_denied&state=YOUR_STATE_VALUEStep 5: Exchange Code for Tokens#
POST to the token endpoint to exchange the authorization code for tokens:
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",
"client_secret": "YOUR_CLIENT_SECRET",
"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": ["workspace-uuid-1", "workspace-uuid-2"]
}| 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) |
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 CLIENT_SECRET = process.env.ORSHOT_CLIENT_SECRET;
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,
client_secret: CLIENT_SECRET,
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#
| Error | Description |
|---|---|
invalid_request | Missing or invalid parameters |
invalid_client | Unknown or disabled client ID |
invalid_grant | Code expired, already used, or PKCE verifier mismatch |
invalid_scope | Requested scope not allowed for this client |
access_denied | User denied the authorization request |

All Set? Let's Start Automating
- Image, PDF and Video Generation via API
- Canva like editor with AI and smart features
- No-Code Integrations (Zapier, Make, n8n etc.)
- Embed Orshot Studio in your app
- Start Free. No credit card required. Cancel anytime.