OAuth
OAuth
OAuth 2.0 enables third-party applications to access Addepar data on behalf of users without handling their credentials directly. If your application serves multiple users or requires delegated access, OAuth is the required authentication method. Failing to implement it correctly results in 401 or 403 responses on every API call.
How it works
The Addepar OAuth implementation uses the Authorization Code grant flow:
- Your application redirects the user to Addepar's authorization URL.
- The user logs in to Addepar and selects their firm (if they have access to multiple firms).
- The user authorizes the requested scopes.
- Addepar redirects back to your
redirect_uriwith a one-time authorization code appended. - Your application exchanges the authorization code (plus your client secret) for an access token and refresh token.
- Your application uses the access token in API requests until it expires (240 seconds), then uses the refresh token to obtain a new access token.
Client setup
Before you can authenticate users, you need credentials from Addepar and must provide redirect configuration.
Addepar provides:
| Credential | Description |
|---|---|
| Client ID | A string identifying your application (e.g., your firm name) |
| Client Secret | A 256-bit hex-encoded secret for token exchange |
| Authorized Scopes | The set of scopes your application can request |
You provide to Addepar:
| Requirement | Description |
|---|---|
| Redirect URI | The URL Addepar redirects to after authorization; multiple URIs supported |
| Terms of Service URL | Your application's terms |
| Privacy Policy URL | Your application's privacy policy |
Parameters and headers
Authorization request
| Parameter | Location | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
response_type | Query | string | Yes | Must be code | Specifies the grant type |
client_id | Query | string | Yes | Provided by Addepar | Identifies your application |
redirect_uri | Query | string | Yes | Must be URL-encoded; must match a registered redirect URI | Where Addepar sends the authorization code |
scope | Query | string | Yes | Space-separated list from available scopes | The permissions your application requests |
state | Query | string | Recommended | Opaque string; returned unchanged in the redirect | Prevents CSRF attacks |
code_challenge | Query | string | No (required for PKCE) | Hex-encoded SHA-256 hash, min 32 characters | The PKCE challenge derived from your code verifier |
code_challenge_method | Query | string | No (required for PKCE) | Must be S256 | The hash algorithm used for the challenge |
Token request
| Parameter | Location | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
client_id | Body | string | Yes | Provided by Addepar | Identifies your application |
client_secret | Body | string | Yes (confidential clients) | 256-bit hex string | Authenticates your application |
grant_type | Body | string | Yes | authorization_code or refresh_token | The type of token request |
code | Body | string | Yes (initial exchange) | One-time use; expires after 60 seconds | The authorization code from the redirect |
redirect_uri | Body | string | Yes (initial exchange) | Must match the authorization request | The same redirect URI used in step 1 |
refresh_token | Body | string | Yes (refresh flow) | Obtained from initial token exchange | Used to obtain a new access token |
code_verifier | Body | string | No (required for PKCE) | The original random string used to derive code_challenge | Proves possession of the challenge |
Token response
| Field | Type | Description |
|---|---|---|
access_token | string | Bearer token for API requests; valid for 240 seconds |
refresh_token | string | Token for obtaining new access tokens; store securely |
addepar_subdomain | string | The firm's subdomain for API calls |
addepar_firm | string | The firm ID for the Addepar-Firm header |
token_type | string | Always bearer |
expires_in | integer | Token lifetime in seconds (240) |
Integration example
Step 1: Request authorization
Redirect the user to the authorization URL. Your Addepar contact provides the correct base URL for your integration.
# Construct the authorization URL (example)
https://id.addepar.com/oauth2/authorize?\
response_type=code&\
client_id=your_client_id&\
redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback&\
scope=portfolio&\
state=random_csrf_tokenAfter the user authorizes, Addepar redirects to your URI with the code:
https://your-app.com/callback?code=EXAMPLE_AUTH_CODE_4f8a2b91c6d3e507&state=random_csrf_token
Step 2: Exchange the code for tokens
curl --request POST \
--url 'https://examplefirm.addepar.com/api/public/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your_client_id' \
--data-urlencode 'client_secret=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'redirect_uri=https://your-app.com/callback' \
--data-urlencode 'code=EXAMPLE_AUTH_CODE_4f8a2b91c6d3e507'{
"access_token": "EXAMPLE_ACCESS_TOKEN_9f3c7d21",
"refresh_token": "EXAMPLE_REFRESH_TOKEN_2b8e4a96f1c05d3a",
"addepar_subdomain": "examplefirm",
"addepar_firm": "2",
"token_type": "bearer",
"expires_in": 240
}Step 3: Use the access token
curl --request POST \
--url 'https://examplefirm.addepar.com/api/v1/portfolio/query' \
--header 'Authorization: Bearer EXAMPLE_ACCESS_TOKEN_9f3c7d21' \
--header 'Addepar-Firm: 2' \
--header 'Content-Type: application/vnd.api+json' \
--data '{
"data": {
"type": "portfolio_query",
"attributes": {
"columns": ["value"],
"groupings": [],
"start_date": "2026-01-01",
"end_date": "2026-07-01",
"portfolio_type": "FIRM",
"portfolio_id": 1
}
}
}'Step 4: Refresh an expired token
Access tokens expire after 240 seconds. Use the refresh token to obtain a new one without re-prompting the user.
curl --request POST \
--url 'https://examplefirm.addepar.com/api/public/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your_client_id' \
--data-urlencode 'client_secret=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=EXAMPLE_REFRESH_TOKEN_2b8e4a96f1c05d3a'{
"access_token": "EXAMPLE_ACCESS_TOKEN_7a1d9c44",
"refresh_token": "EXAMPLE_REFRESH_TOKEN_2b8e4a96f1c05d3a",
"addepar_subdomain": "examplefirm",
"addepar_firm": "2",
"token_type": "bearer",
"expires_in": 240
}PKCE (public clients)
Proof Key for Code Exchange adds a security layer for applications that cannot store a client secret (single-page apps, mobile apps). Addepar's implementation uses hexadecimal encoding for the challenge, which differs from the RFC 7636 standard (base64url).
Generate the verifier and challenge
import hashlib, os
code_verifier = os.urandom(32).hex() # 64-char random hex string
code_challenge = hashlib.sha256(code_verifier.encode()).hexdigest()Include in the authorization request
Add code_challenge and code_challenge_method=S256 to your authorization URL query parameters.
Include in the token exchange
Add code_verifier to your token request body. The server verifies that SHA256(code_verifier) matches the stored challenge before issuing tokens.
Available scopes
| Scope | Access level | Description |
|---|---|---|
PROFILE | Read-only | Name, email, user ID, and firm ID |
PORTFOLIO | Read-only | Portfolio data including name, quantity, and value for all clients, entities, accounts, and securities |
TRANSACTIONS / TRANSACTIONS_WRITE | Read or read-write | Transaction data including type, owner, and value |
FILES / FILES_WRITE | Read or read-write | File names and content associated with clients |
GROUPS / GROUPS_WRITE | Read or read-write | Group details, attributes, and membership |
ENTITIES / ENTITIES_WRITE | Read or read-write | Entities including clients, trusts, accounts, and investments |
POSITIONS / POSITIONS_WRITE | Read or read-write | Ownership positions between entities |
USERS / USERS_WRITE | Read or read-write | User details, contacts, and affiliations |
TEAMS / TEAMS_WRITE | Read or read-write | Teams and team membership |
AUDIT_TRAIL | Read-only | Audit logs including transactions, reports, roles, and permissions |
REPORTS_WRITE | Write | Report generation (no read-only scope available) |
BENCHMARKS_READ / BENCHMARKS_WRITE | Read or read-write | Benchmarks, associations, compositions, and imported data |
BILLING_READ / BILLING_WRITE | Read or read-write | Billable portfolios and fee schedule assignments |
Error recovery
| Failure | Response | Recovery |
|---|---|---|
| Authorization code expired | 400 Bad Request on token exchange | The code expires after 60 seconds. Restart the authorization flow from step 1. |
| Invalid client secret | 400 Bad Request on token exchange | Verify the client secret matches what Addepar provided. Secrets are case-sensitive. |
| Access token expired | 401 Unauthorized on any API call | Use the refresh token to obtain a new access token. Do not re-prompt the user. |
| Refresh token invalid | 400 Bad Request on refresh | The refresh token may have been revoked. Restart the full authorization flow. |
| Scope not authorized | 403 Forbidden on API call | The user did not grant the scope your request requires. Re-authorize with the correct scope. |
| PKCE challenge mismatch | 400 Bad Request on token exchange | Verify you are using hex encoding (not base64url) and that the verifier matches the challenge sent in the authorization request. |
| Redirect URI mismatch | 400 Bad Request on authorization | The redirect URI must exactly match one of the URIs registered with Addepar, including protocol and path. |
Related resources
- Get Set Up -- Initial API access and credential generation
- Rate Limiting -- Request limits that apply to all authenticated calls
- Response Codes -- Complete reference for HTTP status codes
Updated about 4 hours ago