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#

  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.

JavaScript
// 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");
Python
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:

JavaScript
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
scopeRecommendedSpace-separated list of scopes. If you leave it out, Orshot uses every scope your app registered
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 "Allow Access" to continue, or "Cancel"

Step 4: Handle the Redirect#

After approval, Orshot redirects the user to your redirect_uri with an authorization code:

JavaScript
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 clicks Cancel, the redirect includes an error parameter:

JavaScript
https://your-app.com/callback?error=access_denied&error_description=User%20denied%20the%20authorization%20request&state=YOUR_STATE_VALUE

This 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.

Shell
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:

JSON
{
  "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]
}
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). Read it from each response. Do not hard-code it
workspace_idsThe workspaces the user granted access to

Full Example (Node.js)#

JavaScript
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:

ErrorDescription
access_deniedThe user clicked Cancel

Shown to the user on Orshot's consent screen (your app does not receive these):

ErrorDescription
invalid_clientUnknown or disabled client ID
invalid_scopeRequested scope not allowed for this client
invalid_requestMissing or invalid parameters
access_deniedThe 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):

ErrorDescription
invalid_requestMissing or invalid parameters
invalid_clientUnknown or disabled client ID
invalid_grantUnknown or already used code, redirect_uri mismatch, or wrong code_verifier
expired_tokenThe code is older than 5 minutes
unsupported_grant_typeThe 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.

Was this page helpful?

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