Atribu
API Reference

Quickstart

Connect an AI agent to Atribu with OAuth — no account required before you start

You are an AI agent, a script, or a developer building one. You have no Atribu account, no workspace and no API key. This is where you start.

The bootstrap is an OAuth hand-off, exactly like connecting Gmail or Cloudflare: you register a client, build an authorization URL, and hand that URL to your human. They create their account or sign in inside that hand-off and press Approve. You get a token back and you are the user from then on.

There is no signup API

Account creation is a human act behind a captcha. Nothing here requires the human to have an Atribu account beforehand — the URL you hand them works for a brand-new visitor, who is walked through signup and returned to your consent screen automatically.

Register a client and build the authorize URL

Dynamic Client Registration (RFC 7591). Public clients only — there is no client secret, PKCE proves the exchange.

1. Register
curl -sX POST https://www.atribu.app/oauth/mcp/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My Agent",
    "redirect_uris": ["http://127.0.0.1:7788/callback"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "mcp:read"
  }'
201 Created
{
  "client_id": "mcpc_9f2c…",
  "client_id_issued_at": 1788500000,
  "client_name": "My Agent",
  "redirect_uris": ["http://127.0.0.1:7788/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "mcp:read"
}

redirect_uris must be https, or http on loopback (localhost, 127.0.0.1, [::1]) for a local tool. They are matched exactly at /authorize — no prefix matching, no wildcards.

Now mint a PKCE pair and build the URL:

2. Authorize URL (PKCE, S256 — plain is refused)
import base64, hashlib, os, urllib.parse

verifier  = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode()
challenge = base64.urlsafe_b64encode(
    hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()
state = base64.urlsafe_b64encode(os.urandom(16)).rstrip(b"=").decode()

url = "https://www.atribu.app/oauth/mcp/authorize?" + urllib.parse.urlencode({
    "response_type": "code",
    "client_id": "mcpc_9f2c…",
    "redirect_uri": "http://127.0.0.1:7788/callback",
    "scope": "mcp:read",
    "state": state,
    "code_challenge": challenge,
    "code_challenge_method": "S256",
    "resource": "https://mcp.atribu.app/mcp",
})
print(url)   # keep `verifier` — step 3 needs it

Everything on this endpoint is discoverable at https://www.atribu.app/.well-known/oauth-authorization-server (RFC 8414).

Every failure comes back to you, not to a dead page

Once your redirect_uri is matched against the registered set, every error is an RFC 6749 redirect back to it with error and error_descriptionaccess_denied when the human presses Deny, invalid_request for a missing PKCE challenge, invalid_scope, invalid_target, unsupported_response_type. Handle error on your callback as a first-class outcome. An unknown client_id or an unregistered redirect_uri deliberately renders a page instead: redirecting to an unvalidated URI is what the spec forbids.

Your agent gives the human a URL. They open it, create their account, approve. They come back.

Print the URL and stop. This is the human's step, and it is the only one.

Open this to connect your Atribu account: https://www.atribu.app/oauth/mcp/authorize?response_type=code&client_id=…

What they see, whether or not they already have an account:

  1. No account yet — the page sends them to sign in; an unrecognised email goes straight to signup with the pending authorization preserved. They confirm their email, and the confirmation link brings them back to your consent screen, not to a dashboard.
  2. The consent screen — your client_name, the scopes you asked for as individual checkboxes, and the host your redirect_uri points at. A user with no workspace yet is told so plainly: "You have no workspace yet — your agent will create one after you approve."
  3. Approve — the browser is redirected to your redirect_uri with code and your state.

Verify state matches what you sent before touching code.

Exchange the code for a token

POST /oauth/mcp/token
curl -sX POST https://www.atribu.app/oauth/mcp/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=THE_CODE_FROM_THE_CALLBACK \
  -d redirect_uri=http://127.0.0.1:7788/callback \
  -d client_id=mcpc_9f2c… \
  -d code_verifier=THE_VERIFIER_FROM_STEP_1
200 OK
{
  "access_token": "atb_user_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "atb_refresh_…",
  "scope": "mcp:read"
}

The access token lives 1 hour; the refresh token lives 60 days and rotates on every use — store the new one each time. Replaying an old refresh token burns the whole family, which is the reuse detection working.

Refreshing
curl -sX POST https://www.atribu.app/oauth/mcp/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d refresh_token=atb_refresh_… \
  -d client_id=mcpc_9f2c…

Make your first call

The token is a user credential: it spans every workspace that person belongs to, including ones created after they approved.

MCP
curl -sX POST https://mcp.atribu.app/mcp \
  -H "Authorization: Bearer atb_user_…" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"whoami","arguments":{}}}'

whoami costs zero units and answers with the workspaces, profiles, currency, PII mode and write-back state the caller actually has — call it first, always, rather than guessing.

REST — with the same token
curl -H "Authorization: Bearer atb_user_…" \
  "https://api.atribu.app/api/v1/workspaces"

The same token, on both surfaces. Since #1084 an atb_user_… token is a first-class /api/v1 principal, so nothing else has to be minted. Three things about it are worth knowing before your second call:

  • It names a person, not a tenant. An API key is one profile; this token may reach several, so ?profile_id=<uuid> is required on profile-scoped routes — its absence is a 400, never a guess. GET /api/v1/workspaces above needs neither, which is why it is the right first call.
  • Its scopes are an intersection, of what the human granted on the consent screen and what their own membership already allows. mcp:read maps to analytics:read + campaigns:read; no MCP scope reaches exports, commerce, or any messaging surface.
  • /api/v1/me/** is deliberately out of reach. A credential cannot administer the class it belongs to, so minting or revoking tokens stays a signed-in human's act.

No workspace yet? Bootstrap one — no browser required

GET /api/v1/workspaces above answers an empty list for a brand-new account. The same four calls take you from there to a live tracking key (#1085):

Bootstrap: workspace → profile → tracking key → readiness
# 1. Create a workspace and become its owner
curl -X POST https://api.atribu.app/api/v1/workspaces \
  -H "Authorization: Bearer atb_user_…" \
  -H "Content-Type: application/json" \
  -d '{"name": "My Agency"}'
# → { "data": { "workspace_id": "...", "name": "My Agency", "created": true, "checkout": null } }

# 2. Create a profile inside it (use the workspace_id from step 1)
curl -X POST https://api.atribu.app/api/v1/profiles \
  -H "Authorization: Bearer atb_user_…" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id": "WORKSPACE_ID", "name": "My Website"}'
# → { "data": { "profile_id": "...", "workspace_id": "...", "created": true } }

# 3. Issue a tracking key for that profile
curl -X POST "https://api.atribu.app/api/v1/tracking/keys?profile_id=PROFILE_ID" \
  -H "Authorization: Bearer atb_user_…"
# → { "data": { "id": "...", "public_key": "trk_live_...", "status": "active", "created": true } }

# 4. Check what is still missing before the profile can attribute anything
curl "https://api.atribu.app/api/v1/profile/readiness?profile_id=PROFILE_ID" \
  -H "Authorization: Bearer atb_user_…"
# → { "data": { "summary": { "done": 1, "total": 12, ... }, "steps": [ ... ] } }

Safe to retry

POST /api/v1/workspaces is idempotent on (you, name) for a few minutes: calling it again with the SAME name returns the SAME workspace (created: false, 200) instead of a duplicate, so a dropped connection never leaves two workspaces behind.

Starting on a paid plan

Pass "plan": "growth" or "plan": "agency" in step 1's body instead of "starter" (the default). The workspace immediately trials at that plan's real limits — nothing is blocked while a human pays — and the response also carries a real Stripe Checkout hand-off in checkout:

{
  "data": {
    "workspace_id": "...",
    "name": "My Agency",
    "created": true,
    "checkout": { "id": "...", "url": "https://api.atribu.app/h/...", "expires_at": "..." }
  }
}

Give the human checkout.url, then poll GET /api/v1/handoffs/{checkout.id} until it settles (status: "completed" names the resulting subscription). A mint that could not run — billing unconfigured on this deployment, or Stripe refusing the request — never blocks the workspace: checkout comes back null with a warnings entry naming checkout_unavailable instead of either of those, and you can retry the mint alone with POST /api/v1/workspaces/{workspace_id}/checkout-session.

Full detail: Authentication.

Using an API key instead

A key is the right credential for a server-to-server job that belongs to a profile rather than to a person: a nightly export, a CI check, a webhook consumer. It carries no user identity and no consent screen, and it can never mint another key.

  1. Log in to Atribu
  2. Go to Settings > Developer
  3. Click Create API Key
  4. Select your scopes and copy the key

Save your key

The key starts with atb_live_ and is shown only once. Store it somewhere secure.

Request
curl -H "Authorization: Bearer atb_live_YOUR_KEY_HERE" \
  "https://www.atribu.app/api/v1/overview?date_from=2026-03-01&date_to=2026-03-25"
Success response (200 OK)
{
  "data": {
    "current": {
      "spend": 4250.00,
      "revenue": 12800.00,
      "roas": 3.01,
      "outcomes": 145,
      "attributed_outcomes": 132,
      "coverage_percent": 91.03,
      "visitors": 8420,
      "pageviews": 24100,
      "bounce_rate": 42.5,
      "avg_engaged_seconds": 185,
      "cash_revenue": 12800.00,
      "cash_payments": 48
    },
    "previous": { "..." : "..." }
  },
  "meta": {
    "date_from": "2026-03-01",
    "date_to": "2026-03-25",
    "profile_id": "your-profile-id"
  }
}

The full OpenAPI 3.1 document — every route, scope, and response shape — is served from the API host itself, unauthenticated:

https://api.atribu.app/api/v1/openapi.json

Point an SDK generator or an AI agent's tool-loader at it directly; no key required to fetch the spec.

Next steps

On this page