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#

  1. Your app generates a PKCE code verifier and challenge
  2. You redirect the user to Orshot's authorization URL
  3. The user signs in (if needed) and approves access to specific workspaces
  4. Orshot redirects back to your app with an authorization code
  5. 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
ParameterRequiredDescription
client_idYesYour app's client ID
redirect_uriYesMust match a registered redirect URI
response_typeYesAlways code
code_challengeYesThe PKCE challenge derived in Step 1
code_challenge_methodYesAlways S256
scopeYesSpace-separated list of scopes
stateRecommendedRandom 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_VALUE

Always 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_VALUE

Step 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"]
}
FieldDescription
access_tokenUse this in API requests (Authorization: Bearer ost_...)
refresh_tokenUse this to get new access tokens (see Token Management)
expires_inAccess token lifetime in seconds (900 = 15 minutes)
workspace_idsThe 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#

ErrorDescription
invalid_requestMissing or invalid parameters
invalid_clientUnknown or disabled client ID
invalid_grantCode expired, already used, or PKCE verifier mismatch
invalid_scopeRequested scope not allowed for this client
access_deniedUser denied the authorization request

All Set? Let's Start Automating

Get Your API Key →
  • 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.