Connecting a Streamlit App to Autodesk Construction Cloud

This guide explains how our Streamlit app logs into Autodesk and pulls real project data — like issues, RFIs, and submittals — from Autodesk Construction Cloud (ACC). It is written for people who are new to Python or are vibe-coding their way through a project. You do not need deep Python experience to follow along.

This is not a general OAuth tutorial, and these methods only work with Autodesk Construction Cloud. The API endpoints, authentication flow, and project identifiers described here are specific to Autodesk Platform Services (APS) and the ACC API.


The big picture

Before an app can read anything from ACC, it needs permission. That permission comes in the form of an access token — a temporary password that proves “this user said it’s okay for this app to read the data.”

Getting that token is a three-step handshake:

  1. Send the user to Autodesk’s login page. They type their username and password there, not in our app.
  2. Autodesk redirects back to our app with a short-lived code in the URL.
  3. Our app exchanges that code for an access token by calling Autodesk’s token endpoint.

Once we have the access token, every API call includes it in a header so Autodesk knows who we are.

User clicks login
       |
       v
  Autodesk login page
       |
       v
  Redirect back with ?code=abc123
       |
       v
  Our app sends code to Autodesk --> gets back access_token
       |
       v
  Use access_token to call ACC APIs (issues, RFIs, etc.)

The function get_auth_url in auth.py builds a URL that points to Autodesk’s login page. It includes our app’s client_id (which identifies our app), a redirect_uri (where Autodesk should send the user after login), and the scope (what permissions we are asking for, like reading data or account info).

def get_auth_url(client_id, redirect_uri, scope):
    auth_url = "https://developer.api.autodesk.com/authentication/v2/authorize"
    params = {
        "response_type": "code",
        "client_id": client_id,
        "redirect_uri": redirect_uri,
        "scope": scope
    }
    return f"{auth_url}?{urlencode(params)}"

In app.py, we attach this URL to a button. When the user clicks it, their browser goes to Autodesk, they log in, and Autodesk redirects them back to our app with a code parameter in the URL.

How the callback code gets back into the app

After the user logs in, Autodesk redirects their browser to our redirect_uri with the authorization code appended as a query parameter. The URL looks something like:

https://your-app-url.com/?code=abc123xyz

Streamlit makes it easy to read query parameters from the URL with st.query_params. Every time the page loads, the app checks whether a code parameter is present:

params = st.query_params
if "code" in params:
    code = params["code"]
    try:
        token_response = get_token(
            st.secrets["CLIENT_ID"], st.secrets["CLIENT_SECRET"], code, REDIRECT_URI
        )
        st.session_state.access_token = token_response['access_token']
        st.session_state.refresh_token = token_response.get('refresh_token')
        st.query_params.clear()
        st.rerun()
    except Exception as e:
        st.session_state.access_token = None
        st.session_state.refresh_token = None
        st.query_params.clear()

Here is what is happening line by line:

  1. st.query_params reads the URL’s query string. If Autodesk just redirected back, it will contain code.
  2. We pass that code to get_token(...), which exchanges it for an access token (see Step 2 below).
  3. On success, we store the tokens in st.session_state so they persist across Streamlit reruns.
  4. st.query_params.clear() strips the code from the URL. This is important because the code is single-use — if the user refreshes the page, we do not want the app to try exchanging an already-used code and getting an error.
  5. st.rerun() restarts the app so the UI updates to show the authenticated state.

If the exchange fails (for example, the code expired), we clear everything and ask the user to log in again.


Step 2 — Exchange the code for a token

This is the critical part. The code from the URL is temporary and can only be used once. We send it to Autodesk’s token endpoint along with proof that we are who we say we are.

Autodesk’s current authentication system (APS v2) requires that our app’s client_id and client_secret are sent as a special encoded header — not in the body of the request. This is called Basic authentication. A helper function handles the encoding:

def _build_basic_auth_header(client_id, client_secret):
    """Build the Authorization: Basic header required by APS Authentication v2."""
    auth_str = f"{client_id}:{client_secret}"
    encoded = base64.b64encode(auth_str.encode()).decode()
    return {"Authorization": f"Basic {encoded}"}

What this does in plain English: it takes the client_id and client_secret, joins them with a colon, encodes that string in Base64, and wraps it in an Authorization: Basic ... header. This is how Autodesk verifies that the request is coming from our registered app.

The actual token exchange function uses that header:

def get_token(client_id, client_secret, code, redirect_uri):
    token_url = "https://developer.api.autodesk.com/authentication/v2/token"
    headers = _build_basic_auth_header(client_id, client_secret)
    headers["Content-Type"] = "application/x-www-form-urlencoded"
    data = {
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': redirect_uri
    }

    response = requests.post(token_url, headers=headers, data=data)
    if response.status_code == 200:
        return response.json()
    else:
        error_data = response.json()
        if error_data.get('error') == 'invalid_grant':
            raise Exception("Invalid or expired authorization code. Please try authenticating again.")
        else:
            raise Exception("Authentication failed: " + response.text)

If this succeeds (status code 200), we get back a JSON object that contains an access_token and a refresh_token. We store both in Streamlit’s session state so the rest of the app can use them.

In app.py, this looks like:

token_response = get_token(st.secrets["CLIENT_ID"], st.secrets["CLIENT_SECRET"], code, REDIRECT_URI)
st.session_state.access_token = token_response['access_token']
st.session_state.refresh_token = token_response.get('refresh_token')

st.secrets is Streamlit’s built-in way of reading secrets from a .streamlit/secrets.toml file so you never hardcode passwords in your code.


Step 3 — Refresh when the token expires

Access tokens do not last forever. When one expires, instead of making the user log in again, we use the refresh_token to get a new access token behind the scenes:

def refresh_token(client_id, client_secret, refresh_token):
    token_url = "https://developer.api.autodesk.com/authentication/v2/token"
    headers = _build_basic_auth_header(client_id, client_secret)
    headers["Content-Type"] = "application/x-www-form-urlencoded"
    data = {
        'grant_type': 'refresh_token',
        'refresh_token': refresh_token
    }

    response = requests.post(token_url, headers=headers, data=data)
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception("Token refresh failed: " + response.text)

Same pattern as before — Basic auth header, minimal body, POST to the token endpoint. The only difference is grant_type is refresh_token instead of authorization_code.


Step 4 — Identify the ACC project with its GUID

Every project in Autodesk Construction Cloud has a globally unique identifier (GUID) — a long string of letters, numbers, and dashes that looks like this:

a1b2c3d4-e5f6-7890-abcd-ef1234567890

This GUID is how the ACC API knows which project you are asking about. It is not something you make up — Autodesk assigns it when the project is created in ACC. You can find it in the ACC admin panel or through the APS API.

In our app, we store a mapping of human-readable project names to their GUIDs in projects_list.py:

PROJECTS = {
    "10050 - Riverside Office Tower": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "Harbor View Mixed-Use 200 Waterfront Drive": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
    "Greenfield Elementary School Renovation": "1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
    # ... more projects
}

The user picks a project by name in the Streamlit sidebar, and the app looks up the corresponding GUID. That GUID then gets passed into every ACC API call as the project_id. Without it, Autodesk has no way to know which project’s data you want.

These GUIDs are specific to Autodesk Construction Cloud. Other Autodesk products use different project systems and different identifiers — you cannot use an ACC GUID to query Fusion or Revit Cloud data.


Step 5 — Use the token to get ACC project data

Now comes the payoff. With a valid access_token and a project GUID, we can call ACC API endpoints. For example, here is how acc_issues.py fetches issues from a project:

def get_issues(access_token, project_id, status_filter=None, limit=100, page=1):
    url = f"https://developer.api.autodesk.com/construction/issues/v1/projects/{project_id}/issues"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }

    params = {
        "limit": limit,
        "offset": (page - 1) * limit
    }

    if status_filter:
        params["filter[status]"] = status_filter

    response = requests.get(url, headers=headers, params=params)

    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"Failed to retrieve issues: {response.text}")

Notice the pattern:

  • The access_token goes in a Bearer header — this is different from the Basic header used during login. Bearer just means “here is my token, let me in.”
  • The project_id (the GUID from Step 4) is embedded directly in the URL. This tells the ACC API exactly which project’s issues to return.
  • limit and offset handle pagination so you don’t download thousands of issues at once.
  • The URL path starts with /construction/ — this is the ACC-specific portion of the Autodesk API. These endpoints only exist for Autodesk Construction Cloud projects.

In app.py, calling this is straightforward:

response = get_issues(
    st.session_state.access_token,
    current_project_id,
    status_filter=filter_value,
    limit=items_limit
)
items = response.get('results', [])

The same pattern works for RFIs (get_rfis) and submittals (get_submittals) — different URL, same header, same structure.


Where everything lives

FileWhat it does
auth.pyBuilds the login URL, exchanges codes for tokens, refreshes tokens
acc_issues.pyCalls ACC APIs to fetch issues, RFIs, submittals, and attachments
projects_list.pyMaps human-readable project names to their ACC project GUIDs
app.pyThe Streamlit UI — buttons, sidebar, chat, displays data
.streamlit/secrets.tomlStores CLIENT_IDCLIENT_SECRETGROQ_API_KEYREDIRECT_URI

Glossary for vibe coders

TermWhat it means
OAuth 2.0A standard way for apps to get permission to access a user’s data without seeing their password
access_tokenA temporary key that lets the app make API calls on behalf of the user
refresh_tokenA longer-lived key used to get a new access_token when the old one expires
client_idA public identifier for your app, assigned when you register it with Autodesk
client_secretA private key for your app — never share this or put it in your code
Basic authSending credentials encoded in Base64 in a header (used during token exchange)
Bearer tokenSending the access_token in a header (used for all API calls after login)
scopeWhat permissions your app is asking for (e.g. data:readaccount:read)
GUIDGlobally Unique Identifier — the long ID string (e.g. 7d5c351e-a9b7-...) that identifies a specific ACC project
redirect_uriThe URL Autodesk sends the user back to after login
APSAutodesk Platform Services — the umbrella name for Autodesk’s developer APIs
ACCAutodesk Construction Cloud — the product that holds project issues, RFIs, submittals, etc.