Setting up Frameio v4 for python

Hello,

I’m current in the process of switching over to Frameio v4 and have run into a couple of problems. We want to make direct python calls (getting a list of Frameio projects, users, etc…) from standalone python and software such as Houdini, Maya.

I’ve connected my Frameio account to my adobe account. I’ve also created a project in the developers console with the Frameio API. Where I’m confused is:

  • Getting the account id and authentication code
  • Making calls

I selected OAuth Native App for authentication. This has given me the client id and direct url. However I’m not sure how this is called:

import requests
account_id = “NEED TO FIND THIS”
url = “``https://api.frame.io/v4/accounts/{CLIENT_ID}/workspaces``”
headers = {
“x-client-version”: “2.16.4”,
“Authorization”: “NEED TO FIND THIS”
}
response = requests.get(url, headers=headers)
data = response.json()

Any advice would be great as to how to get the account id, if the authentication type is correct for what we are trying to achieve and how to call a command to get a list of projects for example that would be great thanks.

Joe

hi @joeleveson we have our developer docs and a specific python sdk to use to help with API calls (the repo is private atm but it is available on pypi to pull and the references are included).

to get accounts you would do something like:

from frameio import Frameio

client = Frameio(
    base_url="https://api.frame.io"
)

client.accounts.index()

Native Apps need to use PKCE for authorization so you would need to get a token using that method.

Here’s some pseudo code that might work for you for getting an auth token (FYI this is just what Claude Code spit out for me so YMMV):

import hashlib
import base64
import secrets
import urllib.parse
import webbrowser
from urllib.parse import urlparse, parse_qs

class AdobeIMSPKCEAuth:
    def __init__(self, client_id, redirect_uri, scopes):
        self.client_id = client_id
        self.redirect_uri = redirect_uri
        self.scopes = scopes
        
    def generate_pkce_challenge(self):
        # Generate code verifier (43-128 characters)
        code_verifier = base64.urlsafe_b64encode(
            secrets.token_bytes(32)
        ).decode('utf-8').rstrip('=')
        
        # Generate code challenge (SHA256 hash of verifier)
        code_challenge = base64.urlsafe_b64encode(
            hashlib.sha256(code_verifier.encode()).digest()
        ).decode('utf-8').rstrip('=')
        
        return code_verifier, code_challenge
    
    def get_authorization_url(self, code_challenge, state):
        base_url = "https://ims-na1.adobelogin.com/ims/authorize/v2"
        params = {
            "client_id": self.client_id,
            "redirect_uri": self.redirect_uri,
            "scope": " ".join(self.scopes),
            "response_type": "code",
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
            "state": state
        }
        return f"{base_url}?{urllib.parse.urlencode(params)}"
    
    def exchange_code_for_token(self, authorization_code, code_verifier):
        import requests
        
        token_url = "https://ims-na1.adobelogin.com/ims/token/v3"
        
        data = {
            "grant_type": "authorization_code",
            "client_id": self.client_id,
            "code": authorization_code,
            "redirect_uri": self.redirect_uri,
            "code_verifier": code_verifier
        }
        
        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }
        
        response = requests.post(token_url, data=data, headers=headers)
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"Token exchange failed: {response.text}")
    
    def authenticate(self):
        # Step 1: Generate PKCE challenge
        code_verifier, code_challenge = self.generate_pkce_challenge()
        state = secrets.token_urlsafe(16)  # CSRF protection
        
        # Step 2: Build authorization URL
        auth_url = self.get_authorization_url(code_challenge, state)
        
        # Step 3: Open browser for user authorization
        print("Opening browser for Adobe IMS authorization...")
        print(f"Authorization URL: {auth_url}")
        webbrowser.open(auth_url)
        
        print("\nAfter authorization, you'll be redirected to your app's deep link.")
        print("If your app isn't installed, you may see an error page with the authorization code in the URL.")
        print("Look for 'code=' parameter in the URL or error message.")
        
        authorization_code = input("Enter authorization code: ").strip()
        
        if not authorization_code:
            raise Exception("No authorization code provided")
        
        # Step 4: Exchange authorization code for access token
        print("Exchanging authorization code for access token...")
        token_response = self.exchange_code_for_token(authorization_code, code_verifier)
        
        return token_response

# Usage example:
def main():
    # Configuration - Replace with your Adobe Developer Console credentials
    CLIENT_ID = "your_client_id_from_adobe_console"
    REDIRECT_URI = "your_redirect_uri_from_adobe_console"  # e.g., "adobe+your_scheme://adobeid/your_client_id"
    SCOPES = ["openid", "offline_access", "additional_info.roles", "email", "profile"]
    
    # Initialize authenticator
    auth = AdobeIMSPKCEAuth(CLIENT_ID, REDIRECT_URI, SCOPES)
    
    try:
        # Perform authentication flow
        token_data = auth.authenticate()
        
        # Extract tokens
        access_token = token_data.get("access_token")
        refresh_token = token_data.get("refresh_token")
        expires_in = token_data.get("expires_in")
        token_type = token_data.get("token_type")
        
        print("\nAuthentication successful!")
        print(f"Access Token: {access_token}")
        print(f"Refresh Token: {refresh_token}")
        print(f"Token Type: {token_type}")
        print(f"Expires in: {expires_in} seconds")
        
        # Use access_token for Adobe API calls:
        # headers = {"Authorization": f"Bearer {access_token}"}
        
    except Exception as e:
        print(f"Authentication failed: {e}")

if __name__ == "__main__":
    main()

Hello Charlie,

Thanks very much for your response. I used the code you gave me and it generates an authentication URL. That takes me to this page to the requesting your consent page. When I click “Allow Access” the page just goes blank. Should the page give me the authorization code instead? Not sure why its going blank.

Thanks

Joe

hi @joeleveson you will need to catch the access code callback from the browser and use that to exchange for an access token in your code. If you look at the URL that’s returned in the blank browser it should have a section that has code= in the url. Everything after that and before &state= is your access code.

IE:

def authenticate(self):
    # Step 1: Generate PKCE challenge
    code_verifier, code_challenge = self.generate_pkce_challenge()
    state = secrets.token_urlsafe(16)
    
    # Step 2: Build authorization URL
    auth_url = self.get_authorization_url(code_challenge, state)
    
    # Step 3: Open browser for user authorization
    print("Opening browser for Adobe IMS authorization...")
    print(f"Authorization URL: {auth_url}")
    webbrowser.open(auth_url)
    
    print("\n" + "="*60)
    print("IMPORTANT: After clicking 'Allow Access':")
    print("1. The page will go BLANK - this is normal!")
    print("2. Look at the URL in your browser's address bar")
    print("3. Find 'code=' in the URL")
    print("4. Copy everything after 'code=' and before the next '&'")
    print("5. Paste it below")
    print("="*60)
    print("\nExample URL: adobe+scheme://adobeid/client_id?code=ABC123XYZ&state=...")
    print("In this case, copy: ABC123XYZ")
    print()
    
    authorization_code = input("Paste the authorization code here: ").strip()
    
    if not authorization_code:
        raise Exception("No authorization code provided")
    
    # Clean up the code in case they copied extra characters
    if '&' in authorization_code:
        authorization_code = authorization_code.split('&')[0]
    
    print("Exchanging authorization code for access token...")
    token_response = self.exchange_code_for_token(authorization_code, code_verifier)
    
    return token_response

Hi Charlie,

Thanks very much. Getting there. I get a 43 character code back. I had to change:

params = {
“client_id”: self.client_id,
“redirect_uri”: self.redirect_uri,
“scope”: " ".join(self.scopes),
“response_type”: “code”,
“code_challenge”: code_challenge,
“code_challenge_method”: “S256”,
“state”: state
}

To:

params = {
“client_id”: self.client_id,
“redirect_uri”: self.redirect_uri,
“scope”: " ".join(self.scopes),
“response_type”: “code”,
“code”: code_challenge,
“code_method”: “S256”,
“state”: state
}

It is still failing unfortunately. I get:

Authentication failed: Token exchange failed: {“error”:“access_denied”}

Is there something I need setting on my Adobe account perhaps.?

Many thanks again for your help. It very much appreicated.

hi @joeleveson the changes you made are the reason why you are getting the access_denied response. IMS is very specific about the parameter names, so you need to re-add code_challenge and code_challenge_method back to your token exchange step. I listed the official docs above if you need to reference them again

Thanks Charlie. I’ll read through the docs. I found the original params before my changes gave we no code only code_challange which didn’t work. Will have a read through. Thanks

Hello @CharlieAnderson ,

We went back and our administrator:

  • Connected his account from Frameio to his Adobe account
  • Created a Frameio API project
  • Added my email address as a beta user
  • Pushed the project to production

However even with the code it still doesn’t give back a code= in the url. It only has this response_type=code&code_challenge=.

This is the full format of the url returned:

https://ims-na1.adobelogin.com/ims/authorize/v2?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=openid+offline_access+additional_info.roles+email+profile&response_type=code&code_challenge=CHALLENGE_CODE&code_challenge_method=S256&state=STATE_CODE

Sorry to keep messaging but we’re at a loss as to where we are going wrong as it happens for the admin as well as me.

Many Thanks

Joe

hi @joeleveson the url returned is the initial authorization url that the user needs to click on, log in to their adobe account, to then click the prompt of “allow access” window, which then should redirect to the redirect uri that is in your Project (IE adobe+scheme://adobeid/client_id?code=AUTHORIZATION_CODE_HERE&state=... where you should get the code to use to exchange.

Thanks Charlie. Me and the administrator only get sent to a blanks page with this URL after clicked allow access.

https://adobeid-na1.services.adobe.com/ims/fromSusi#

Have no idea why this is. Many Thanks

Joe

hi @joeleveson https://adobeid-na1.services.adobe.com/ims/fromSusi# is the default fallback page when there’s an error with the redirect. Can you open dev tools in that browser and see if you get the same error message I do?

That url in the console should contain the info you need. I was unable to catch the redirect when only using python, so if you want to use a native app for a desktop solution you might want to consider using electron to catch the redirect.

Otherwise you may have to just switch to web app and run a local server to catch the redirect.

Thanks @CharlieAnderson. I get a message:

Prevented navigation to: LONG_URL due to an unknown protocol.

Then when clicking on the LONG_URL I get:

{“error”:“invalid_token”,“error_description”:“Bad cdscKey”}

I’ll speak to the IT department about what you suggest and come back you. Thanks very much for your help its much appreicated.

Joe

Sure thing! I may see about adding the python/electron helper that CC helped me write in my personal github for folks if it’s helpful.

1 Like

@joeleveson I just pushed this to github. It’s a bit of a hack via electron on MacOS but it should work for you if you’re set on using Native App OAuth.

Thanks very much @CharlieAnderson . That’s great. will have a look and let you know how I get on. Many thanks

Hi @CharlieAnderson ,

I’ve run into some problems with the electron-helper installation. This is the log file details:

0 info it worked if it ends with ok
1 verbose cli [ ‘/usr/bin/node’, ‘/usr/bin/npm’, ‘run’, ‘package’ ]
2 info using npm@6.14.11
3 info using node@v10.24.0
4 verbose run-script [ ‘prepackage’, ‘package’, ‘postpackage’ ]
5 info lifecycle frameio-oauth-helper@1.0.0~prepackage: frameio-oauth-helper@1.0.0
6 info lifecycle frameio-oauth-helper@1.0.0~package: frameio-oauth-helper@1.0.0
7 verbose lifecycle frameio-oauth-helper@1.0.0~package: unsafe-perm in lifecycle true
8 verbose lifecycle frameio-oauth-helper@1.0.0~package: PATH: /usr/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/home/joeleveson/Downloads/frameio-python-oauth/electron-helper/node_modules/.bin:/home/joeleveson/.local/bin:/home/joeleveson/bin:/usr/share/Modules/bin:/lib/modules/4.18.0-348.20.1.el8_5.x86_64/aja/current/bin:/opt/Autodesk/io/bin:/opt/Autodesk/sw/tools:/opt/Autodesk/sw:/usr/local/bin:/usr/bin:/bin:/sbin:/usr/local/sbin:/usr/sbin
9 verbose lifecycle frameio-oauth-helper@1.0.0~package: CWD: /home/joeleveson/Downloads/frameio-python-oauth/electron-helper
10 silly lifecycle frameio-oauth-helper@1.0.0~package: Args: [ ‘-c’,
10 silly lifecycle ‘electron-packager . FrameioOAuth --platform=darwin --arch=universal --overwrite’ ]
11 silly lifecycle frameio-oauth-helper@1.0.0~package: Returned: code: 1 signal: null
12 info lifecycle frameio-oauth-helper@1.0.0~package: Failed to exec package script
13 verbose stack Error: frameio-oauth-helper@1.0.0 package: electron-packager . FrameioOAuth --platform=darwin --arch=universal --overwrite
13 verbose stack Exit status 1
13 verbose stack at EventEmitter. (/usr/lib/node_modules/npm/node_modules/npm-lifecycle/index.js:332:16)
13 verbose stack at EventEmitter.emit (events.js:198:13)
13 verbose stack at ChildProcess. (/usr/lib/node_modules/npm/node_modules/npm-lifecycle/lib/spawn.js:55:14)
13 verbose stack at ChildProcess.emit (events.js:198:13)
13 verbose stack at maybeClose (internal/child_process.js:982:16)
13 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:259:5)
14 verbose pkgid frameio-oauth-helper@1.0.0
15 verbose cwd /home/joeleveson/Downloads/frameio-python-oauth/electron-helper
16 verbose Linux 4.18.0-348.20.1.el8_5.x86_64
17 verbose argv “/usr/bin/node” “/usr/bin/npm” “run” “package”
18 verbose node v10.24.0
19 verbose npm v6.14.11
20 error code ELIFECYCLE
21 error errno 1
22 error frameio-oauth-helper@1.0.0 package: electron-packager . FrameioOAuth --platform=darwin --arch=universal --overwrite
22 error Exit status 1
23 error Failed at the frameio-oauth-helper@1.0.0 package script.
23 error This is probably not a problem with npm. There is likely additional logging output above.
24 verbose exit [ 1, true ]

The package run output:

cd electron-helper && npm install && npm run package
npm WARN read-shrinkwrap This version of npm is compatible with lockfileVersion@1, but package-lock.json was generated for lockfileVersion@3. I’ll try to do my best with it!

electron@35.7.5 postinstall /home/joeleveson/Downloads/frameio-python-oauth/electron-helper/node_modules/electron
node install.js

/home/joeleveson/Downloads/frameio-python-oauth/electron-helper/node_modules/electron/install.js:47
checksums: process.env.electron_use_remote_checksums ?? process.env.npm_config_electron_use_remote_checksums ? undefined : require(‘./checksums.json’),
^

SyntaxError: Unexpected token ?
at Module._compile (internal/modules/cjs/loader.js:723:23)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
at Module.load (internal/modules/cjs/loader.js:653:32)
at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
at Function.Module._load (internal/modules/cjs/loader.js:585:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
at startup (internal/bootstrap/node.js:283:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)
npm WARN notsup Unsupported engine for electron@35.7.5: wanted: {“node”:“>= 12.20.55”} (current: {“node”:“10.24.0”,“npm”:“6.14.11”})
npm WARN notsup Not compatible with your version of node/npm: electron@35.7.5
npm WARN notsup Unsupported engine for @electron/get@2.0.3: wanted: {“node”:“>=12”} (current: {“node”:“10.24.0”,“npm”:“6.14.11”})
npm WARN notsup Not compatible with your version of node/npm: @electron/get@2.0.3
npm WARN frameio-oauth-helper@1.0.0 No repository field.

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! electron@35.7.5 postinstall: node install.js
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the electron@35.7.5 postinstall script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/joeleveson/.npm/_logs/2026-01-17T18_04_00_552Z-debug.lo
g

Thanks

Joe

This looks like a problem for others.

Will keep looking into it. Thanks

hi @joeleveson my repo is for MacOS only, linux/windows isn’t supported in the electron-helper at this time. Let me see if I can make some quick updates to support linux.

edit: All done, you can pull the latest changes @joeleveson and see if that helps. Also I noticed you are using Node.js v10.24.0 and this version requires 12.20+ (the readme specifies 18+)

Hi @CharlieAnderson ,

Thanks very much for making this compatiable with Linux. This is great as it is the platform my company uses. I’ve updated the Node.js and have successfully installed electron-helper.

I am finding it failing when running the python command:

│ Error: no_redirect
│ Details: Did not receive redirect within timeout

I am curious as it writes the args json file however there are no args in the subprocess in electron_auth.py line 204 just the Frameio0Auto path. Nothing seems to happen when I run

frameio-python-oauth/electron-helper/FrameioOAuth-linux-x64/FrameioOAuth

I’ve tried running it with variable args afterwards: the url, the json args file but nothing happens and not url opens either. It just gives me the error message:

No configuration found - waiting for URL via open-url event

I wonder if something need configuring to pick the args.

Thanks for your help again. Very grateful.

Joe

hi @joeleveson the Electron app was using process.env.HOME to find the config directory, but that can be undefined on Linux in certain environments (especially packaged apps). I’ve pushed a fix that uses Node’s os.homedir() instead, which is more reliable.

Please pull the latest changes and rebuild:

cd frameio-python-oauth

git pull origin main

cd electron-helper

npm run package:linux

cd ..

# Then try auth again

python src/cli.py auth

Let me know if that helps.