Authentication
Every request to the Amdahl Platform API must be authenticated. The platform supports three credential types. Pick the one that matches your integration shape.
| Credential | Header | Best for | Refresh |
|---|---|---|---|
| API key | Authorization: Bearer ak_live_... or X-API-Key: ak_live_... | Server-to-server, CLIs, agents, scheduled jobs | Manual rotation |
| OAuth 2.1 access token | Authorization: Bearer <access_token> | Third-party apps acting on behalf of a user | Refresh token flow |
| Supabase JWT | Authorization: Bearer <jwt> | First-party web app only | Short-lived |
All three land on the same authenticator. Behind the header, each request is resolved to { user_id, business_id, effective_scopes } before any tool runs. Scope enforcement is identical across credential types, and every tool invocation writes one row to the platform audit log.
If both Authorization and X-API-Key are present, Authorization wins.
API keys
API keys are the recommended path for almost every integration. They are long-lived, per-user plus per-business, and scoped explicitly at creation time.
Create an API key
The fastest path is the dashboard. In console.amdahl.co:
- Sign in and open the workspace the key should act in (keys are per-workspace).
- Go to Settings -> Developer.
- Click Create key, give it a name, and pick the scopes it needs.
- Copy the plaintext key immediately. It is shown once and is never retrievable again.
The key is bound to your user and that one workspace. To act against another workspace, switch workspaces and create a separate key there.
You can also mint a key programmatically with the api_keys.create tool, if your current credential holds the keys:manage scope:
curl -s -X POST "$AMDAHL_BASE/api_keys" \
-H "Authorization: Bearer $AMDAHL_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My integration",
"scopes": ["data:read", "context:read", "pages:read", "pages:write"]
}'Response (plaintext key is returned exactly once):
{
"id": "f1e2...",
"key": "ak_live_abc123...",
"key_prefix": "ak_live_abc123",
"name": "My integration",
"scopes": ["data:read", "context:read", "pages:read", "pages:write"],
"scope_mode": "strict",
"created_at": "2026-04-14T12:00:00.000Z"
}Requested scopes must be a subset of the calling credential's scopes. Asking for a scope you do not hold returns 403 forbidden.
Use an API key
Two header formats are accepted:
# Preferred (matches OAuth shape; works everywhere)
curl -H "Authorization: Bearer $AMDAHL_KEY" ...
# Equivalent alternative
curl -H "X-API-Key: $AMDAHL_KEY" ...Storage and security
- Plaintext keys are returned on creation and on rotation. They are never retrievable again.
- The server stores only a SHA-256 hash. Rows in
api_keysrecord the prefix (ak_live_abc123) for display. - Keys are per-user plus per-business. Switching business context requires a different key.
- All keys use the
ak_live_prefix in production. Test keys useak_test_and carryis_test=true.
Rotate a key
Rotation mints a replacement with the same scopes and either revokes the old key immediately (grace_period_hours: 0) or keeps it valid for a window (up to 168 hours) to let deployments roll forward:
curl -s -X POST "$AMDAHL_BASE/api_keys/$OLD_KEY_ID/rotate" \
-H "Authorization: Bearer $AMDAHL_KEY" \
-d '{ "grace_period_hours": 24 }'The response includes a new plaintext key. Update your integration, confirm traffic has moved, then let the grace window expire.
Revoke a key
curl -s -X DELETE "$AMDAHL_BASE/api_keys/$KEY_ID" \
-H "Authorization: Bearer $AMDAHL_KEY"Revocation is immediate. You cannot revoke the key you are currently authenticating with (lock-out prevention); rotate instead.
scope_mode: legacy vs strict
Every API key carries a scope_mode field that governs how the unified tools layer interprets its scopes:
strict(default for every new key minted viaapi_keys.createor the dashboard): the key's exact scopes are used. If a tool requirespages:writeand the key lacks it, the call returns403 forbidden.legacy(back-fill only for keys issued before the unified tools layer): role defaults are applied as a grandfather rule, and every request emits an audit flaglegacy_mode = truefor tracking.
You will not set scope_mode yourself unless you are migrating historical data.
OAuth 2.1
Use OAuth when your app acts on behalf of an end user and you cannot securely store a long-lived key (for example a desktop app or a third-party integration that signs users in).
The platform implements RFC 7591 dynamic client registration and authorization code with PKCE (S256 only).
Dynamic client registration
curl -s -X POST "https://app.amdahl.co/oauth/register" \
-H "Content-Type: application/json" \
-d '{
"client_name": "My desktop integration",
"redirect_uris": ["https://myapp.example/oauth/callback"]
}'Response:
{
"client_id": "mcp_abc123...",
"client_secret": "...",
"redirect_uris": ["https://myapp.example/oauth/callback"]
}Redirect URIs must be HTTPS in production; http://localhost and http://127.0.0.1 are allowed for local development only.
Authorization code + PKCE
-
Generate a
code_verifier(43 to 128 chars, URL-safe) and derivecode_challenge = BASE64URL(SHA256(verifier)). -
Send the user to:
codehttps://app.amdahl.co/oauth/authorize ?response_type=code &client_id=mcp_abc123... &redirect_uri=https://myapp.example/oauth/callback &code_challenge=<challenge> &code_challenge_method=S256 &scope=data:read%20pages:read%20pages:write &state=<random-anti-csrf> -
After consent, the user is redirected back with
?code=...&state=.... -
Exchange the code for an access token:
bashcurl -s -X POST "https://app.amdahl.co/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=<code>" \ -d "redirect_uri=https://myapp.example/oauth/callback" \ -d "client_id=mcp_abc123..." \ -d "code_verifier=<verifier>"
Response:
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...",
"scope": "data:read pages:read pages:write"
}Refreshing tokens
curl -s -X POST "https://app.amdahl.co/oauth/token" \
-d "grant_type=refresh_token" \
-d "refresh_token=<refresh_token>" \
-d "client_id=mcp_abc123..."Revoking OAuth clients
curl -s -X DELETE "$AMDAHL_BASE/oauth_clients/$CLIENT_ID" \
-H "Authorization: Bearer $AMDAHL_KEY"Revokes the client and invalidates every access and refresh token it holds. Irreversible. Only callable by a member of the business that registered the client.
Scopes
Scopes are fine-grained resource:action strings enforced on every tool call. Examples: pages:write, context:read, webhooks:delete.
Effective scopes on a request are the intersection of:
- The scopes granted on the credential (API key or OAuth token), and
- The default scopes for the user's role in the business (
viewer,editor,admin,owner).
A request that asks for an action outside its effective scope returns 403 forbidden with details.missing_scope naming the scope that would unblock it.
The full role default bundles and the complete tool-to-scope matrix are in api-reference/scopes.md.
JWT (web app only)
The first-party Amdahl web app signs in with Supabase Auth and calls the Platform API with the Supabase access token (Authorization: Bearer <jwt>). This path is not intended for external integrations: tokens are short-lived (one hour), bound to browser session state, and scoped by the user's role defaults. External integrations should use an API key or OAuth.
FAQ
My request returned 401 unauthenticated. Your header is missing, malformed, or pointing at a revoked credential. Re-read Quickstart step 2 and confirm the header shape.
My request returned 403 forbidden with missing_scope. The credential is valid but lacks the scope that tool requires. Mint or rotate a key with the needed scope, or grant the user a higher role. Cross-reference api-reference/scopes.md.
My OAuth access token expired. Use the refresh token flow above. Refresh tokens are valid until the user explicitly revokes the client or an admin revokes the client via oauth_clients.revoke.
I lost the plaintext value of my API key. Keys cannot be recovered. Rotate (keeping the same scopes) or revoke and create a new one.
Can I share one API key across teammates? No. Keys are per-user plus per-business. Mint one per integration or per operator so audit rows attribute actions correctly.
Can I call the API without a business context? No. Every credential resolves to exactly one business_id. To operate against multiple businesses, mint a key per business.