API Authentication

Call the Attribution API from your own apps using OAuth 2.0 with PKCE. Create an API client in the dashboard, then exchange credentials for short-lived access tokens you can use with curl or any HTTP client.

Overview

The Attribution API is organized around REST. It accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, verbs, and OAuth 2.0 Bearer authentication. Every endpoint the dashboard calls is available to your apps and scripts under the same auth.

Base API URL: https://api.attributionapp.com. The dashboard at dashboard.attributionapp.com and the API are backed by the same service and share a single OAuth authorization server — authorize at https://dashboard.attributionapp.com/oauth/authorize, exchange tokens at https://api.attributionapp.com/oauth/token.

Versioning: some paths are prefixed with their version (e.g. /v2/filters/tree). Breaking changes will introduce a new prefix; older versions keep working.

Testing against non-production data: there is no separate sandbox URL. Instead, the consent screen during authorization asks the user to pick which project the access token will be scoped to. If your account has a staging or dedicated test project, choose that one at consent time — the resulting token only sees data from the project you selected. This is the recommended way to exercise the API safely without touching production data.

How authentication works

Attribution uses OAuth 2.0 Authorization Code with PKCE (RFC 6749, RFC 7636). The flow:

  1. You create an API client in the dashboard and receive a client_id + client_secret.
  2. Your app generates a random code_verifier and its SHA-256 code_challenge.
  3. You open /oauth/authorize in a browser, sign in if needed, pick which project the token should belong to, and approve the requested scopes.
  4. The browser redirects to your app with a one-time authorization_code.
  5. Your app exchanges the code (plus the verifier) for an access_token + refresh_token.
  6. Your app calls the API with Authorization: Bearer <access_token>.

The three things you need:

  • A client_id and client_secret from the dashboard.
  • A fresh code_verifier / code_challenge pair per authorization, generated by your app.
  • The set of scopes you want the token to carry — see Scopes.

Token lifetimes:

  • Access token — 1 hour, sent as Authorization: Bearer ... on every API call.
  • Refresh token — 30 days, single-use. Each refresh rotates both tokens; the old refresh token becomes invalid the moment a new pair is issued.

Before you start

You need:

  • An active Attribution plan that includes the API Access feature and you need to be the project owner. If you don't see the API Access card in Settings → Security, upgrade your plan first.
  • Comfort running curl from a terminal.

1. Create an API client

Heads up: every account can only have one API client at a time. To rotate, revoke the existing one and create a new one. Revoking immediately invalidates all outstanding access and refresh tokens — apps will need to re-authorize.

  1. Sign in to Attribution.
  2. Open Settings → Security.
  3. Scroll to API Access and fill out:
    • Client name — anything that helps you recognise the app later (e.g. Weekly Reporting Pipeline).
    • Redirect URIs — the local URL your app will listen on during authorization. For command-line tools this is typically http://localhost:8080/callback. You can list several, comma-separated.
  4. Click Create API Client.
  5. Attribution shows your client_id and client_secret exactly once. Copy both into your secrets store (1Password, AWS Secrets Manager, env vars in CI) immediately — the secret is not recoverable if you lose it.

2. Authorize your app

2.1 Generate the PKCE pair

PKCE protects the authorization code in transit. Generate a fresh code_verifier and its SHA-256 code_challenge:

# 64 random bytes, url-safe base64, no padding
CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '\n=' | tr '/+' '_-')

# SHA-256 of the verifier, url-safe base64, no padding
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | base64 | tr -d '\n=' | tr '/+' '_-')

2.2 Open the authorization URL

Print the URL and open it in your browser. You'll be asked to sign in (if you aren't already), pick which project the token will be scoped to, and approve the requested scopes.

Tip: if you have a dedicated staging or test project, select it on the consent screen — the access token will only see data from that project, leaving production untouched.

CLIENT_ID="atb_…paste your client id here…"
CLIENT_SECRET="…your client secret…"
REDIRECT_URI="http://localhost:8080/callback"
SCOPES="filters:read reports:read"
STATE=$(openssl rand -hex 16)

echo "https://dashboard.attributionapp.com/oauth/authorize?\
client_id=${CLIENT_ID}&\
redirect_uri=$(printf %s "$REDIRECT_URI" | jq -sRr @uri)&\
response_type=code&\
scope=$(printf %s "$SCOPES" | jq -sRr @uri)&\
state=${STATE}&\
code_challenge=${CODE_CHALLENGE}&\
code_challenge_method=S256"

After you approve, your browser is redirected to:

http://localhost:8080/callback?code=<authorization_code>&state=<the same state>

The simplest way to capture the code during a one-off test is to listen on the port with nc:

# In another terminal, before clicking Approve in the browser:
nc -l 8080
# Once the callback hits, copy the `code` value from the GET line.

In production, your app should run a small HTTP server on the redirect URI (http.server in Python, express in Node.js, Net::HTTPServer in Ruby).

3. Exchange the code for tokens

Send client credentials via HTTP Basic auth (RFC 6749 §2.3.1, the recommended method):

CODE="…the authorization_code from the callback…"

curl -sS https://api.attributionapp.com/oauth/token \
  -u "${CLIENT_ID}:${CLIENT_SECRET}" \
  -d grant_type=authorization_code \
  -d "code=${CODE}" \
  -d "code_verifier=${CODE_VERIFIER}" \
  -d "redirect_uri=${REDIRECT_URI}"

Response:

{
  "access_token":  "eyJraWQiOi…",
  "token_type":    "Bearer",
  "expires_in":    3600,
  "refresh_token": "5b1f…",
  "scope":         "filters:read reports:read"
}

Alternative: you can pass client_id and client_secret as form-body fields instead of the Basic-auth header (RFC 6749 §2.3.1, client_secret_post). Either works.

The authorization code is single-use and expires within a few minutes — exchange it immediately after the callback.

4. Call the API

Send the access token in the Authorization: Bearer header:

ACCESS_TOKEN="…the access_token from the response above…"

curl -sS https://api.attributionapp.com/v2/filters/tree \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

Endpoints check the scopes embedded in your token. If a request returns 403 insufficient_scope, edit the scope parameter in your authorization URL (step 2.2) and re-authorize.

5. Refresh the access token

Access tokens are valid for 1 hour. Refresh tokens are valid for 30 days and rotate on every use — the old refresh token becomes invalid the moment a new pair is issued.

REFRESH_TOKEN="…the refresh_token from earlier…"

curl -sS https://api.attributionapp.com/oauth/token \
  -u "${CLIENT_ID}:${CLIENT_SECRET}" \
  -d grant_type=refresh_token \
  -d "refresh_token=${REFRESH_TOKEN}"

The response shape matches step 3. Persist the new refresh_token immediately; the previous one is dead.

If a refresh ever returns 400 invalid_grant, your refresh token was either revoked, used twice, or has expired — your app must go back to step 2 and re-authorize via the browser.

6. Revoke a token (optional)

RFC 7009. Invalidates a specific refresh or access token without revoking the entire client. Useful when:

  • An app is being decommissioned and you want its stored refresh token to stop working immediately.
  • A token was leaked but the client itself is still trusted (rotating the secret is overkill).
curl -sS https://api.attributionapp.com/oauth/revoke \
  -u "${CLIENT_ID}:${CLIENT_SECRET}" \
  -d "token=${REFRESH_TOKEN}" \
  -d "token_type_hint=refresh_token"

Returns 200 with an empty body on success — including for already-revoked or unknown tokens, per RFC 7009, to avoid leaking token-existence information.

In-dashboard revocation: you can also revoke an entire OAuth session interactively from Settings → Security → Active Sessions. This lists every active session — dashboard logins, MCP connections, and API clients — and lets you sign each one out individually.

Scopes

Pass a space-separated list to the scope parameter in step 2.2. Every scope your app requests is shown to the user on the consent screen; request only what you need.

ResourceScopesWhat it lets you do
accountaccount:read, account:writeRead or update account and team settings
billingbilling:read, billing:writeRead invoices or update payment method
filtersfilters:read, filters:writeList, create, or modify filters
integrationsintegrations:read, integrations:writeList integrations or manage their settings
projectproject:read, project:writeRead or update project metadata and settings
reportsreports:read, reports:writeRun reports; write covers saving project views
visitorsvisitors:readList and inspect visitors and companies

Widening scopes later requires a fresh authorization — re-run step 2 with the updated scope parameter.

Errors

Errors come back as a JSON body with an error field — and, on the authorization endpoint, in the redirect query string per RFC 6749.

Authorization (/oauth/authorize)

HTTPerrorWhen
302 → redirect_uriaccess_deniedThe user refused on the consent screen.
302 → redirect_uriinvalid_scopeOne of the requested scopes is unknown.
400invalid_requestA required parameter (client_id, redirect_uri, code_challenge, …) is missing or malformed.
400invalid_clientclient_id doesn't match a known client, or redirect_uri is not on the client's whitelist.

Recovery: fix the URL and resend. access_denied means start over from step 2.

Token exchange (/oauth/token)

HTTPerrorWhen
400invalid_requestA required parameter is missing or malformed.
400invalid_grantCode or refresh token is unknown, expired, already used, or doesn't match the client.
401invalid_clientclient_secret doesn't match the stored value (Basic header or form body).
400unsupported_grant_typegrant_type is not authorization_code or refresh_token.
400invalid_scopeRequested scope contains an unknown value.

Recovery: on invalid_grant, restart from step 2.1 with a fresh code_verifier and a fresh browser approval — the previous code or refresh token cannot be revived.

API calls

HTTPerrorWhen
401invalid_tokenToken expired, signature failed, or audience mismatch.
403insufficient_scopeThe token doesn't carry the scope this endpoint requires.
403api_access_requiredYour subscription doesn't include API access.

Recovery: on invalid_token, refresh per step 5. On insufficient_scope, re-authorize per step 2 with a wider scope list. On api_access_required, upgrade your plan.

Security checklist

  • Treat client_secret like a password: never commit it, never log it, never paste it into client-side code.
  • Use HTTPS for every API call — the access token must never travel over plain HTTP.
  • Store refresh tokens at rest the same way you store the secret. They grant a fresh access token without re-authorization.
  • If you suspect a leak, revoke the client in the dashboard and create a new one. All outstanding access and refresh tokens become invalid immediately.
  • Rotate the client_secret if a contributor leaves, even if no specific leak is known.
  • Use Settings → Security → Active Sessions to interactively sign out any active OAuth session if you suspect a compromise — the fastest path when you can't immediately locate the leaked refresh token to revoke programmatically.
  • Never expose client_secret to the browser. Server-side web apps are fine; pure browser SPAs need a backend to hold the secret and proxy API calls.