> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://next.developer.frame.io/platform/v2/refreshing-oauth-2-tokens/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://next.developer.frame.io/_mcp/server. # Refreshing OAuth 2 Tokens If you haven't, please refer to [this](/oauth-2-applications/building-an-oauth-2-app) 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: ```json { "access_token":"BEARER_TOKEN", "expires_in":3600, "refresh_token":"REFRESH_TOKEN", "scope":"account.read offline", "token_type":"bearer" } ``` 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](/oauth-2-applications/building-an-oauth-2-app) 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`** ```python title="Python" def callback(): # Where `request` refers to our initial call to the auth URL state = request.args.get('state') scope = request.args.get('scope') code = request.args.get('code') error = request.args.get('error') if error: return "Error: " + error # Set up for client authorization and set up the data you need to send. client_auth = requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET) post_data = { "grant_type": "authorization_code", "code": code, "redirect_uri": REDIRECT_URI, "state": state, "scope": SCOPE } # Send a POST request with the data you need to receive an access token. response = requests.post(TOKEN, auth=client_auth, data=post_data) # Stash the refresh token for later os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"] return response.text ``` ### With PKCE **`Python`** ```python title="Python" def callback(): # Where `request` refers to our initial call to the auth URL state = request.args.get('state') scope = request.args.get('scope') code = request.args.get('code') error = request.args.get('error') if error: return "Error: " + error # If using PKCE, you must include the CLIENT_ID in your request body post_data = { "grant_type": "authorization_code", "code": code, "redirect_uri": REDIRECT_URI, "state": state, "scope": SCOPE "client_id": CLIENT_ID } # Send a POST request with the data you need to receive an access token. # If using PKCE, use the below request with no auth response = requests.post(TOKEN_URL, data=post_data) # Stash the refresh token for later os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"] 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`: \ * `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](/oauth-2-applications/building-an-oauth-2-app#the-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`** ```python title="Python" def refresh(): # Fetch the refresh token, assuming we have it REFRESH_TOKEN = os.environ.get('REFRESH_TOKEN') client_auth = requests.auth.HTTPBasicAuth(CLIENT_ID,CLIENT_SECRET) post_data = { "grant_type": "refresh_token", "scope": SCOPE, "refresh_token": REFRESH_TOKEN # if using PKCE, you will need to include your client_id as below # "client_id": CLIENT_ID } response = requests.post(TOKEN_URL, auth=client_auth, data=post_data) # Catch + stash a new Refresh Token os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"] 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`** ```python title="Python" def refresh(): # Fetch the refresh token, assuming we have it REFRESH_TOKEN = os.environ.get('REFRESH_TOKEN') post_data = { "grant_type": "refresh_token", "scope": SCOPE, "refresh_token": REFRESH_TOKEN "client_id": CLIENT_ID } response = requests.post(TOKEN_URL, data=post_data) # Catch + stash a new Refresh Token os.environ['REFRESH_TOKEN'] = response.json()["refresh_token"] return response.text ``` Congratulations! You can now handle the entire token lifecycle of an OAuth2.0 client application.