For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
Developer Tools
CommunityContact UsConsole
DocsAPI Reference
DocsAPI Reference
  • Getting Started
    • Welcome to Frame.io V2 API
    • Key Concepts
    • Authentication
  • Migration
    • V2 to V4 Migration Guide
  • OAuth 2 Applications
    • OAuth 2 Code Authorization Flow
    • Building an OAuth2 App
    • Refreshing OAuth 2 Tokens
  • Workflows - Assets
    • Reading the File Tree
    • Uploading Assets
    • Manage Version Stacks
    • Working with Annotations
    • Search for Assets
  • Workflows - Projects
    • Working with Review Links
    • Gather All Comments from a Project
  • Workflows - Admin
    • User Management
    • Working with Audit Logs
  • Automations - Webhooks
    • Webhooks Overview
    • Comment Workflows with Zapier
  • Automations - Zapier
    • Zapier Basics
    • Frame.io Resources in Zapier
    • Upload Assets to Frame.io using Zapier
    • Webhooks in Zapier
  • Custom Actions
    • Custom Actions Overview
    • Three Ways to Deploy Custom Actions
    • Deploy Custom Actions to Zapier
  • Other Tools
    • Using ngrok
    • Using Glitch
  • Troubleshooting
    • API Error Codes
    • Rate Limits
    • Browser Support
  • Deprecated
    • How to - Authorize (Hardware)
    • How to - Authorize (Application)
    • How to - Manage Auth (Hardware)
    • How to - Manage Auth (Application)

© 2026 Adobe Inc. All rights reserved.

TermsPrivacyDo not sell or share my personal information
Developer-friendly docs for your API
Logo
Developer Tools
CommunityContact UsConsole
On this page
  • Basics of token refresh
  • Capturing the refresh token on successful authentication
  • Without PKCE
  • With PKCE
  • Executing a refresh
  • Without PKCE
  • With PKCE
OAuth 2 Applications

Refreshing OAuth 2 Tokens

Was this page helpful?
Previous

Reading the File Tree

Next
This guide assumes you've already built an OAuth2 app

If you haven’t, please refer to this guide, and return here once you’ve captured the access_token and refresh_token from a successful OAuth 2 credentials grant.

Basics of token refresh

Assuming you’ve included the offline scope in your OAuth2.0 credentials request, successful authentication via Frame.io’s Accounts application will return a payload that looks like the following:

1{
2 "access_token":"BEARER_TOKEN",
3 "expires_in":3600,
4 "refresh_token":"REFRESH_TOKEN",
5 "scope":"account.read offline",
6 "token_type":"bearer"
7}

The access_token is a bearer token that can be used to act on behalf of the authenticated user; it will expire after 3600 seconds (one hour); and after that, the refresh_token can be used to to fetch a new access_token. The refresh token will then expire after 30 days, at which point you will need the user to login from scratch, producing a new access/refresh token pair; and so on.

If you do not request the offline scope explicitly, you will not receive a refresh_token, and therefore after an hour will have to fully re-authenticate the user.

Capturing the refresh token on successful authentication

Needless to say, you can’t use a refresh_token you don’t have, so be sure in your app to:

  • Request the offline scope
  • Capture the refresh_token that’s returned in a successful callback.

For convenience, the callback from our OAuth 2 App Guides is reproduced here, with an os call to stash the Refresh token. Please note that two examples are provided: one with PKCE configured (does not include basic auth header), and one without (includes basic auth header).

Without PKCE

Python
1def callback():
2 # Where `request` refers to our initial call to the auth URL
3 state = request.args.get('state')
4 scope = request.args.get('scope')
5 code = request.args.get('code')
6 error = request.args.get('error')
7
8 if error:
9 return "Error: " + error
10
11 # Set up for client authorization and set up the data you need to send.
12 client_auth = requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET)
13
14 post_data = {
15 "grant_type": "authorization_code",
16 "code": code,
17 "redirect_uri": REDIRECT_URI,
18 "state": state,
19 "scope": SCOPE
20 }
21
22 # Send a POST request with the data you need to receive an access token.
23 response = requests.post(TOKEN, auth=client_auth, data=post_data)
24 # Stash the refresh token for later
25 os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"]
26
27 return response.text

With PKCE

Python
1def callback():
2 # Where `request` refers to our initial call to the auth URL
3 state = request.args.get('state')
4 scope = request.args.get('scope')
5 code = request.args.get('code')
6 error = request.args.get('error')
7
8 if error:
9 return "Error: " + error
10
11 # If using PKCE, you must include the CLIENT_ID in your request body
12 post_data = {
13 "grant_type": "authorization_code",
14 "code": code,
15 "redirect_uri": REDIRECT_URI,
16 "state": state,
17 "scope": SCOPE
18 "client_id": CLIENT_ID
19 }
20
21 # Send a POST request with the data you need to receive an access token.
22 # If using PKCE, use the below request with no auth
23 response = requests.post(TOKEN_URL, data=post_data)
24 # Stash the refresh token for later
25 os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"]
26
27 return response.text

Executing a refresh

The refresh itself is a single call to Frame.io’s token URL:

  • Method: POST
  • URL: https://applications.frame.io/oauth2/token
  • Content-Type: application/x-www-form-urlencoded

A refresh will always include at least the following three attributes in its form data:

  • grant_type: refresh_token
  • scope: <SCOPES>
  • refresh_token: <REFRESH_TOKEN>

If you’re using PKCE, you’ll need to include your app’s client_id in this form data; if not, you’ll need to include a Basic authentication header with your app’s client_id and client_secret as the Username and Password, respectively.

Without PKCE

Similar to making the initial authentication callback without PKCE, this standard refresh will require supplying your client_id and client_secret as the Username and Password in a Basic Authentication header.

Python
1def refresh():
2 # Fetch the refresh token, assuming we have it
3 REFRESH_TOKEN = os.environ.get('REFRESH_TOKEN')
4
5 client_auth = requests.auth.HTTPBasicAuth(CLIENT_ID,CLIENT_SECRET)
6 post_data = {
7 "grant_type": "refresh_token",
8 "scope": SCOPE,
9 "refresh_token": REFRESH_TOKEN
10 # if using PKCE, you will need to include your client_id as below
11 # "client_id": CLIENT_ID
12 }
13
14 response = requests.post(TOKEN_URL, auth=client_auth, data=post_data)
15 # Catch + stash a new Refresh Token
16 os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"]
17
18 return response.text

With PKCE

Again, we’re mimicking the rules of our initial /callback cycle:

  • We don’t include an Authorization header
  • We must include the client_id in our payload
Python
1def refresh():
2 # Fetch the refresh token, assuming we have it
3 REFRESH_TOKEN = os.environ.get('REFRESH_TOKEN')
4
5 post_data = {
6 "grant_type": "refresh_token",
7 "scope": SCOPE,
8 "refresh_token": REFRESH_TOKEN
9 "client_id": CLIENT_ID
10 }
11
12 response = requests.post(TOKEN_URL, data=post_data)
13 # Catch + stash a new Refresh Token
14 os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"]
15
16 return response.text

Congratulations! You can now handle the entire token lifecycle of an OAuth2.0 client application.