Docs

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.

CredentialHeaderBest forRefresh
API keyAuthorization: Bearer ak_live_... or X-API-Key: ak_live_...Server-to-server, CLIs, agents, scheduled jobsManual rotation
OAuth 2.1 access tokenAuthorization: Bearer <access_token>Third-party apps acting on behalf of a userRefresh token flow
Supabase JWTAuthorization: Bearer <jwt>First-party web app onlyShort-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:

  1. Sign in and open the workspace the key should act in (keys are per-workspace).
  2. Go to Settings -> Developer.
  3. Click Create key, give it a name, and pick the scopes it needs.
  4. 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:

bash
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):

json
{
  "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:

bash
# 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_keys record 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 use ak_test_ and carry is_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:

bash
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

bash
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 via api_keys.create or the dashboard): the key's exact scopes are used. If a tool requires pages:write and the key lacks it, the call returns 403 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 flag legacy_mode = true for 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

bash
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:

json
{
  "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

  1. Generate a code_verifier (43 to 128 chars, URL-safe) and derive code_challenge = BASE64URL(SHA256(verifier)).

  2. Send the user to:

    code
    https://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>
  3. After consent, the user is redirected back with ?code=...&state=....

  4. Exchange the code for an access token:

    bash
    curl -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:

json
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "...",
  "scope": "data:read pages:read pages:write"
}

Refreshing tokens

bash
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

bash
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:

  1. The scopes granted on the credential (API key or OAuth token), and
  2. 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.