Frame.io TypeScript SDK — Authentication Guide

This guide explains how to authenticate with the Frame.io API using the Frame.io TypeScript SDK (frameio). The Frame.io V4 API uses Adobe Identity Management Service (IMS), Adobe’s OAuth 2.0 identity platform.

This is a standalone reference for TypeScript/JavaScript developers. All code examples and flows below are for the frameio package only.


Authentication Types in the TypeScript SDK

The TypeScript SDK supports four OAuth authentication classes, plus direct token usage:

MethodUse caseUser interaction?Requires client secret?
Static tokenQuick scripts, testing, or you already have a tokenNoNo
Server-to-ServerBackend services, cron jobs, automationNoYes
Web AppServer-side apps (Express, Fastify, Next.js)YesYes
SPA (PKCE)Browser apps that can’t store a secretYesNo
Native App (PKCE)Desktop/mobile apps with custom URI scheme redirectsYesNo

Server-to-Server lets your app act as a service account with no user interaction. It’s only available to Frame.io V4 accounts administered via the Adobe Admin Console.

Web App and SPA let your app act as a specific user. Both use Adobe IMS under the hood: the user authorizes your app, and the SDK exchanges the resulting code for tokens. The TypeScript SDK handles the IMS /authorize/v2 and /token/v3 flow for you. For Web App you need a client secret; for SPA you use PKCE instead.

Native App follows the same PKCE flow as SPA but uses the adobe+<hash>://callback redirect URI that Adobe assigns to your Native App credential. This lets your application intercept the redirect at the OS level after authorization.


Service Account Users

When you use Server-to-Server authentication, your application acts as a service account user, a distinct account type that can perform actions on behalf of the service. These are visible to other users in Frame.io: when a service account takes an action, its name is displayed in the UI.

You can grant and revoke service account access through the Adobe Admin Console and Developer Console. Service account names are managed from the Frame.io UI. By default, your first S2S connection is named Service Account User, the second Service Account User 2, and so on.


Quick Start

Prerequisites

  1. Credentials from the Adobe Developer Console:

    • Client ID — required for all OAuth flows
    • Client Secret — required for Server-to-Server and Web App flows
    • Redirect URI — required for Web App, SPA, and Native App flows; must be registered in your Adobe project
  2. Install the SDK:

$npm install frameio

Choosing a method

  • No user involved? Use Server-to-Server (ServerToServerAuth).
  • User involved and you can store a secret? Use Web App (WebAppAuth).
  • User involved but you can’t store a secret? Use SPA (SPAAuth) for browser apps, or Native App (NativeAppAuth) for desktop/mobile apps.

Access Token

If you already have an access token (from another OAuth system or a prior exchange, e.g. via our API Explorer) you can pass it directly:

1import { FrameioClient } from "frameio";
2
3const client = new FrameioClient({ token: "YOUR_ACCESS_TOKEN" });

This is the simplest approach, but the token will eventually expire and the SDK won’t refresh it for you.

Legacy Developer Tokens

For V4-migrated accounts not yet administered via the Adobe Admin Console, you can continue to use Legacy Developer Tokens from the Frame.io developer site. You must include the x-frameio-legacy-token-auth header and set it to true:

1import { FrameioClient } from "frameio";
2
3const client = new FrameioClient({
4 token: "YOUR_LEGACY_DEVELOPER_TOKEN",
5 headers: { "x-frameio-legacy-token-auth": "true" },
6});

Legacy developer tokens do not expire, but they are a transitional mechanism. For new integrations and production workloads, we recommend using one of the OAuth 2.0 flows below. See the Migration Guide for details.


Server-to-Server (Client Credentials)

Use this for backend services and scripts that need Frame.io access without user interaction. This flow is only available to Frame.io V4 accounts administered via the Adobe Admin Console. Your application authenticates as a service account user with no human in the loop.

1import { FrameioClient, ServerToServerAuth } from "frameio";
2
3const auth = new ServerToServerAuth({
4 clientId: "YOUR_CLIENT_ID",
5 clientSecret: "YOUR_CLIENT_SECRET",
6});
7
8const client = new FrameioClient({ token: () => auth.getToken() });

That’s it. auth.getToken() is an async function that the SDK invokes on every request. If the current token is still valid, it returns immediately. If it’s about to expire, it fetches a new one first, completely transparently.

How it works

Your client credentials (client ID + secret) never expire. You only rotate them manually for security hygiene. S2S gives you effectively permanent, uninterrupted API access with zero manual intervention.

Under the hood:

  1. On the first API call, getToken() requests a new access token from Adobe IMS using the client_credentials grant.
  2. The token is cached in memory. Individual access tokens expire (typically 24 hours), but this is handled for you.
  3. When a cached token is within the refresh buffer (default: 60 seconds before expiry), the SDK fetches a fresh one automatically using the same client credentials.
  4. No refresh tokens are involved. The client credentials themselves are the long-lived secret, and they can always be used to mint a new access token.

Explicit authentication

If you want to fetch the token eagerly (for example, to fail fast on bad credentials at startup):

1const auth = new ServerToServerAuth({ clientId: "...", clientSecret: "..." });
2await auth.authenticate(); // throws AuthenticationError if credentials are invalid
3const client = new FrameioClient({ token: () => auth.getToken() });

Web App (Authorization Code)

Use this for server-side applications where users sign in with their Adobe ID. This flow requires a client secret, which must be stored securely on your server.

1

Redirect the user to Adobe IMS

1import { WebAppAuth } from "frameio";
2import crypto from "crypto";
3
4const auth = new WebAppAuth({
5 clientId: "YOUR_CLIENT_ID",
6 clientSecret: "YOUR_CLIENT_SECRET",
7 redirectUri: "https://yourapp.com/callback",
8});
9
10// Generate a cryptographically random state value to prevent CSRF attacks
11const state = crypto.randomBytes(32).toString("hex");
12
13const authorizationUrl = auth.getAuthorizationUrl({ state });
14// Store `state` in the user's session, then redirect them to `authorizationUrl`
2

Handle the callback

When Adobe IMS redirects the user back to your redirectUri, extract the code and state parameters. Verify the state matches what you stored, then exchange the code for tokens:

1// In your callback handler (e.g. an Express route):
2await auth.exchangeCode(req.query.code as string);

This exchanges the authorization code for an access token and a refresh token, storing both internally.

3

Use the client

1import { FrameioClient } from "frameio";
2
3const client = new FrameioClient({ token: () => auth.getToken() });

That’s it. From this point on, getToken() manages the token lifecycle automatically. When the access token approaches expiry, the SDK uses the refresh token to obtain a new one. No user interaction required.

Full Express example

1import crypto from "crypto";
2import express from "express";
3import session from "express-session";
4import { FrameioClient, WebAppAuth } from "frameio";
5
6const auth = new WebAppAuth({
7 clientId: "YOUR_CLIENT_ID",
8 clientSecret: "YOUR_CLIENT_SECRET",
9 redirectUri: "http://localhost:3000/callback",
10});
11
12const app = express();
13app.use(session({ secret: crypto.randomBytes(32).toString("hex"), resave: false, saveUninitialized: false }));
14
15app.get("/login", (req, res) => {
16 const state = crypto.randomBytes(32).toString("hex");
17 (req.session as any).oauthState = state;
18 res.redirect(auth.getAuthorizationUrl({ state }));
19});
20
21app.get("/callback", async (req, res) => {
22 if (req.query.state !== (req.session as any).oauthState) {
23 return res.status(403).send("Invalid state parameter");
24 }
25
26 await auth.exchangeCode(req.query.code as string);
27
28 const client = new FrameioClient({ token: () => auth.getToken() });
29 const accounts = await client.accounts.index();
30 res.json(accounts);
31});
32
33app.listen(3000);

Single Page App / PKCE (Authorization Code + PKCE)

Use this for browser-based applications, desktop apps, or CLI tools that cannot securely store a client secret. This flow uses PKCE (RFC 7636) to protect the authorization code exchange.

1

Generate the authorization URL

1import { SPAAuth } from "frameio";
2
3const auth = new SPAAuth({
4 clientId: "YOUR_CLIENT_ID",
5 redirectUri: "https://yourapp.com/callback",
6});
7
8const state = crypto.randomUUID();
9const result = await auth.getAuthorizationUrl({ state });
10// result.url -> redirect the user here
11// result.codeVerifier -> store this securely until the callback

getAuthorizationUrl returns an AuthorizationUrlResult containing the full URL (with the PKCE code_challenge embedded) and the codeVerifier you’ll need in the next step.

2

Exchange the code with the verifier

When the user is redirected back:

1await auth.exchangeCode({
2 code: "CODE_FROM_CALLBACK",
3 codeVerifier: result.codeVerifier,
4});
3

Use the client

1import { FrameioClient } from "frameio";
2
3const client = new FrameioClient({ token: () => auth.getToken() });

That’s it. Refresh works the same as Web App — the SDK uses the refresh token automatically. The difference is that no client secret is sent during refresh, since the SPA flow is designed for public clients.

The codeVerifier must be stored securely on the client side between the authorization request and the code exchange. Use sessionStorage or equivalent in browser apps.


Native App (Authorization Code + PKCE)

Use this for desktop and mobile applications. When you create a Native App credential in the Adobe Developer Console, Adobe assigns you a redirect URI of the form adobe+<hash>://callback — you register your application to handle that custom URI scheme at the OS level. Loopback redirects (http://127.0.0.1:<port>/callback) are also supported for local development. The flow is identical to SPA — it uses PKCE with no client secret.

1import { NativeAppAuth } from "frameio";
2
3const auth = new NativeAppAuth({
4 clientId: "YOUR_CLIENT_ID",
5 redirectUri: "adobe+abc123def456://callback", // from your Adobe Developer Console Native App credential
6 // Also supports loopback: "http://127.0.0.1:8080/callback"
7});
8
9const { url, codeVerifier } = await auth.getAuthorizationUrl({
10 state: crypto.randomUUID(),
11});
12
13// Open system browser to `url`
14// Listen for redirect on your custom URI scheme or loopback server
15
16await auth.exchangeCode({ code: "CODE_FROM_REDIRECT", codeVerifier });
17const client = new FrameioClient({ token: () => auth.getToken() });

Redirect URI rules

Adobe enforces redirect URI rules at two points: when you register the credential in the Adobe Developer Console, and when the redirect_uri parameter hits the /authorize/v2 endpoint. The value you pass to redirectUri in this SDK must match one of the “Redirect URI patterns” you registered on the credential — otherwise Adobe redirects to the Default Redirect URI on the credential instead.

  • Web App and SPA credentials require HTTPS.
  • Native App credentials use a non-HTTPS redirect — typically the adobe+<hash>://callback URI shown in the Developer Console for the credential.

See the Adobe Developer Console for the exact patterns accepted for your credential.

The Python SDK does not include a Native App credential class, since Python has no standard way to register custom URI scheme handlers. The TypeScript SDK supports all four credential types including Native App.


Manual Token Refresh

For Web App, SPA, and Native App flows, the SDK refreshes tokens automatically via getToken(). If you need explicit control, you can call refresh() directly:

1await auth.refresh(); // fetches a new access token using the refresh token

This is useful when you want to force a refresh ahead of a critical operation rather than relying on the automatic refresh buffer.

refresh() is available on WebAppAuth, SPAAuth, and NativeAppAuth. It throws ConfigurationError if no refresh token is available (i.e. you must call exchangeCode() first). ServerToServerAuth does not have a refresh() method — it uses authenticate() to fetch a new token via client credentials instead.


Token Persistence

All auth classes support exportTokens() and importTokens() for persisting token state across restarts. For Web App, SPA, and Native App flows this is especially important, since the access and refresh tokens live in memory by default — if your application restarts, users would need to re-authenticate unless you persist them. For Server-to-Server, persistence is optional (the client credentials can always mint a new token), but importing a cached token avoids an extra round-trip on startup.

Export and import

1// After exchangeCode(), save the token state
2const tokenData = auth.exportTokens();
3// tokenData is: { access_token: "...", refresh_token: "...", expires_at: 1234567890.0 }
4// Save it to your database, file, or secret store
5
6// On next startup, restore it
7auth.importTokens(tokenData);
8const client = new FrameioClient({ token: () => auth.getToken() });
9// The SDK will automatically refresh if the token is near expiry

Store exported tokens securely. They contain access and refresh tokens that grant API access. Avoid writing tokens to plaintext files in production.

Automatic persistence with onTokenRefreshed

To persist tokens automatically every time they’re refreshed, use the onTokenRefreshed callback:

1import fs from "fs/promises";
2
3const TOKEN_FILE = "tokens.json";
4
5const auth = new WebAppAuth({
6 clientId: "...",
7 clientSecret: "...",
8 redirectUri: "...",
9 onTokenRefreshed: (tokens) => {
10 fs.writeFile(TOKEN_FILE, JSON.stringify(tokens));
11 },
12});
13
14// On startup, restore if available
15try {
16 const saved = JSON.parse(await fs.readFile(TOKEN_FILE, "utf-8"));
17 auth.importTokens(saved);
18} catch {
19 // No saved tokens — user will need to authenticate
20}

The callback receives the same shape as exportTokens() and fires after every successful token refresh.


Revoking Tokens

To sign out a user and invalidate their tokens with Adobe IMS:

1await auth.revoke();

This makes two best-effort revocation requests to Adobe IMS — one for the access token and one for the refresh token, in parallel — and clears all local token state. For confidential clients (WebAppAuth), revocation requests use HTTP Basic Auth; for public clients (SPAAuth, NativeAppAuth), the client_id is sent as a query parameter. Revocation errors are logged but not thrown. After revoking, the user will need to re-authenticate.


Error Handling

All auth errors inherit from FrameioAuthError, so you can catch them broadly or handle specific cases:

1import {
2 FrameioAuthError,
3 AuthenticationError,
4 TokenExpiredError,
5 ConfigurationError,
6 NetworkError,
7 RateLimitError,
8} from "frameio";
9
10try {
11 await auth.exchangeCode("...");
12} catch (error) {
13 if (error instanceof TokenExpiredError) {
14 // The refresh token has expired; redirect the user to sign in again
15 } else if (error instanceof AuthenticationError) {
16 // Token exchange failed
17 console.error(`Error: ${error.errorCode} - ${error.errorDescription}`);
18 } else if (error instanceof NetworkError) {
19 // Timeout or connection failure (after retries)
20 } else if (error instanceof RateLimitError) {
21 // 429 from Adobe IMS; retry after error.retryAfter seconds
22 } else if (error instanceof FrameioAuthError) {
23 // Catch-all for any other auth error
24 }
25}

Error reference

ExceptionWhen it’s raised
ConfigurationErrorMissing or invalid configuration (e.g. empty clientId, non-HTTPS redirect URI, non-HTTPS imsBaseUrl)
AuthenticationErrorToken exchange or refresh rejected by Adobe IMS (has .errorCode and .errorDescription)
TokenExpiredErrorRefresh token itself is expired; user must re-authenticate
NetworkErrorHTTP timeout or connection failure after all retries
RateLimitErrorAdobe IMS returned 429; check .retryAfter for backoff guidance
PKCEErrorAvailable for consumer use in PKCE flows; not thrown internally by the SDK

Handling expired refresh tokens in production

For Web App, SPA, and Native App flows, the refresh token will eventually expire. When that happens, getToken() raises TokenExpiredError. You should catch this and redirect the user through the authorization flow again.

1import { TokenExpiredError } from "frameio";
2
3try {
4 const client = new FrameioClient({ token: () => auth.getToken() });
5 const assets = await client.files.list({ projectId: "..." });
6} catch (error) {
7 if (error instanceof TokenExpiredError) {
8 // Clear persisted tokens and redirect user to login
9 await auth.revoke();
10 return res.redirect("/login");
11 }
12}

Configuration Reference

These parameters have sensible defaults and rarely need to be set. If you do need to customize behavior — pointing at a staging IMS, injecting a custom fetch, tuning timeouts, or wiring up a logger — pass any of them as optional parameters when constructing the auth class:

ParameterDefaultDescription
scopesFlow-specific defaultsSpace-separated OAuth scopes. S2S defaults to openid AdobeID frame.s2s.all; user-facing flows default to openid email profile offline_access additional_info.roles.
imsBaseUrlhttps://ims-na1.adobelogin.comAdobe IMS base URL. Override for staging or non-production environments. Must use HTTPS.
fetchglobalThis.fetchCustom fetch implementation for proxy, mTLS, or custom HTTP handling.
timeout30000HTTP request timeout in milliseconds for token endpoint calls.
maxRetries2Maximum retries for transient failures (5xx, timeouts). Rate-limit retries (429) are tracked separately.
refreshBuffer60Seconds before token expiry to trigger proactive refresh.
onTokenRefreshedundefinedCallback fired after every successful token refresh. Receives an object with access_token, refresh_token, and expires_at.
loggerNo-op (silent)Logger instance with debug, info, warn, error methods (e.g. console, pino, winston).

Staging environments

Point at a staging Adobe IMS instance by overriding imsBaseUrl. The SDK also exports DEFAULT_IMS_BASE_URL (https://ims-na1.adobelogin.com) if you need to reference the production value programmatically.

1const auth = new ServerToServerAuth({
2 clientId: "...",
3 clientSecret: "...",
4 imsBaseUrl: "https://ims-na1-stg1.adobelogin.com",
5});

Custom fetch

For proxy support or custom TLS configuration:

1import { ProxyAgent } from "undici";
2
3const proxyAgent = new ProxyAgent("http://corporate-proxy:8080");
4
5const auth = new ServerToServerAuth({
6 clientId: "...",
7 clientSecret: "...",
8 fetch: (url, init) => fetch(url, { ...init, dispatcher: proxyAgent }),
9});

Concurrency Safety

The TypeScript SDK is safe for concurrent use. When multiple getToken() calls happen simultaneously and a refresh is needed, only one refresh request fires. The others await the same promise and receive the same result. No external locking is required.

This deduplication uses JavaScript’s single-threaded event loop and a shared Promise — if a refresh is already in flight, concurrent callers join it instead of starting a second request.

If revoke() is called while a refresh is in flight, the refresh is rejected with an AuthenticationError and tokens remain cleared — the revocation always wins.