# auth.md

API for Agents (https://apiforagents.dev) sells public data to agents, pay as you go, per record. You are an agent. This service supports agentic registration: discover, register, claim if needed, exchange for an access_token, call the API, handle revocation. Follow the steps in order.

Short version: `POST https://apiforagents.dev/agent/identity` with `{"type":"anonymous"}` gives you an identity at once, with $0.10 of free credit across up to 5 paid calls so you can prototype with real data before anyone logs in. When that is spent, a 402 carries a top-up link for a person, and the claim ceremony puts the agent under their account. Nothing beyond the trial is ever charged without a person paying first.

## Step 1. Discover

A 401 from the API carries `WWW-Authenticate: Bearer resource_metadata="https://apiforagents.dev/.well-known/oauth-protected-resource"`.

- Protected resource metadata: `GET https://apiforagents.dev/.well-known/oauth-protected-resource`
- Authorization server metadata: `GET https://apiforagents.dev/.well-known/oauth-authorization-server` (the `agent_auth` block names every endpoint below)
- Signing keys: `GET https://apiforagents.dev/.well-known/jwks.json`
- Resource (the `aud` for an ID-JAG): `https://apiforagents.dev/v1/`

## Step 2. Pick a method

1. You hold a session tied to a user and your provider can mint an ID-JAG for this audience: use `identity_assertion`. No provider is on the trust list yet; fall back to the next method.
2. You have the user's email: use `service_auth`. A claim ceremony is required before any credential is issued.
3. You have neither: use `anonymous`. You get a limited credential at once; the claim is optional until the user wants paid calls.

Before sending `identity_assertion` or `service_auth`, tell the user the service name (API for Agents) and the scopes you will act under, and get their consent.

## Step 3. Register

`POST https://apiforagents.dev/agent/identity` with `Content-Type: application/json`.

### anonymous

```json
{ "type": "anonymous" }
```

Response: `registration_id`, `identity_assertion`, `assertion_expires`, `pre_claim_scopes` (catalog.read, data.run), `claim_url`, `claim_token`, `claim_token_expires`, `post_claim_scopes` (catalog.read, data.run, account.read, account.billing). Hold `claim_token` in memory only; it is returned once. Go to Step 5 to use the assertion now, or Step 4 to invite a person to take ownership.

### service_auth

```json
{ "type": "service_auth", "login_hint": "person@example.com" }
```

Response: `registration_id`, `claim_url`, `claim_token`, `claim_token_expires`, `post_claim_scopes`, and a `claim` block with `user_code`, `verification_uri`, `expires_in`, `interval`. No assertion yet. Go to Step 4b.

### identity_assertion

```json
{ "type": "identity_assertion", "assertion_type": "urn:ietf:params:oauth:token-type:id-jag", "assertion": "<ID-JAG JWT>" }
```

Mint the ID-JAG with `aud` = `https://apiforagents.dev/v1/`, a fresh `jti`, `exp` about five minutes out, `email_verified: true`, and `auth_time` no older than 3600 seconds. A clean match returns `identity_assertion` and `scopes`. A 401 `interaction_required` carries a `claim` block: the user must confirm the link (Step 4b). A 401 `login_required` means re-authenticate at your provider first.

## Step 4. Claim ceremony

### 4a. Get the materials (anonymous only)

```json
POST https://apiforagents.dev/agent/identity/claim
{ "claim_token": "clm_...", "email": "person@example.com" }
```

Response: `claim_attempt_id`, `status`, `expires_at`, and `claim_attempt` with `user_code`, `verification_uri`, `expires_in`, `interval`. The email binds the registration to that person; only they can complete it.

### 4b. Hand off to the user

Show the user both in one message:

> Open this link, sign in or sign up, and enter this 6-digit code: **123456**
> https://apiforagents.dev/claim?claim_attempt_token=...

The code goes into the page they land on, not back to you. The code lives 10 minutes and allows 5 wrong tries.

### 4c. Poll for completion

```
POST https://apiforagents.dev/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_...
```

`authorization_pending` means keep waiting, no faster than `interval` seconds. `slow_down` means you polled too fast. `expired_token` means the code lapsed: call 4a again with the same `claim_token` for a fresh code, and if that answers `claim_expired` (410), register again. Success is a token response with `access_token`, `expires_in`, `scope`, plus `identity_assertion` and `assertion_expires` for refreshes. For anonymous registrations the pre-claim tokens are revoked at this moment; use the new one.

## Step 5. Exchange the assertion

```
POST https://apiforagents.dev/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>&resource=https://apiforagents.dev/v1/
```

Response: `access_token`, `token_type`, `expires_in` (86400 seconds), `scope`. There is no refresh_token: re-exchange the same assertion until it expires, then register again. `invalid_grant` means the assertion is expired or revoked.

## Step 6. Use the access_token

`Authorization: Bearer <access_token>` on every call under `https://apiforagents.dev/v1/`. `GET https://apiforagents.dev/v1/me` says who you are and what you may do.

Scopes:
- `catalog.read`: Read platforms, operations, fields and prices, and quote any call for free.
- `data.run`: Run paid operations. Needs a claimed account with money on it.
- `account.read`: Read balance, usage and receipts.
- `account.billing`: Fetch a top-up link.

A run charges the quoted amount from the balance, asks the source, then settles to the rows that came back and refunds the rest; `cost.usd` is the final charge and `cost.balanceUsd` what is left. Every field is checked against the operation: a field it does not declare, or a number outside its range, answers 400 `invalid_request` naming the `field`, and nothing is charged. An unclaimed registration spends its trial first (5 calls, $0.10); after that, or with no balance, a paid call answers 402 `payment_required` with `top_up_url` for a person. `"estimateOnly": true` is always free, and so is `GET https://apiforagents.dev/v1/me`, which reports the balance. To fund the account, `POST https://apiforagents.dev/v1/topups` with `{"usd": 5}` (any amount from 5) returns `checkout_url`; hand it to the person, then poll `GET https://apiforagents.dev/v1/me` until `balance_usd` rises. Never retry a 402 before it does.

## Errors

| Code | Where | What to do |
| --- | --- | --- |
| `anonymous_not_enabled`, `service_auth_not_enabled`, `issuer_not_enabled` | /agent/identity | Pick another method. |
| `invalid_request` | /agent/identity | Fix the body or mint a fresh ID-JAG. |
| `interaction_required` (401) | /agent/identity | Surface the `claim` block to the user. |
| `login_required` (401) | /agent/identity | Re-authenticate at your provider, mint a fresh ID-JAG. |
| `invalid_claim_token` | /agent/identity/claim | Register again. |
| `claimed_or_in_flight` (409) | /agent/identity/claim | Already claimed, or the email does not match. |
| `claim_expired` (410) | /agent/identity/claim | Register again. |
| `invalid_grant`, `unsupported_grant_type`, `invalid_request` | /oauth2/token | Register again, or fix the grant. |
| `authorization_pending`, `slow_down`, `expired_token` | /oauth2/token (claim grant) | Wait, back off, or re-mint the code. |
| `insufficient_scope` (403) | the API | Run the claim ceremony. |
| `payment_required` (402) | the API | Give the person the top-up link in the body. |
| `rate_limited` (429) | anywhere | Honour `Retry-After`. |

Limits: 1 anonymous registration an hour per address, 5 unclaimed at once, one trial per address and 3 per network per day, while the day's trial budget lasts (registration works either way); unclaimed registrations expire after 24 hours. Register once and keep the identity_assertion; do not register per call.

## Revocation

- `POST https://apiforagents.dev/oauth2/revoke` with `token=<access_token>&token_type_hint=access_token` kills one token (200, idempotent). The assertion stays valid; exchange it again.
- The person can revoke the whole registration from their dashboard at https://apiforagents.dev/dashboard/agents. Your next exchange answers `invalid_grant`; register again and ask them to claim it.
- A trusted provider can POST a Security Event Token to `https://apiforagents.dev/agent/event/notify` to revoke everything issued for one of its users.

Full API reference: https://apiforagents.dev/docs. Machine files: https://apiforagents.dev/openapi.json, https://apiforagents.dev/llms.txt. Terms: https://apiforagents.dev/terms. Contact: contact@apiforagents.dev.
