# Docs

> API for Agents · https://apiforagents.dev/docs · HTML twin: https://apiforagents.dev/docs

## Quickstart

### 1. Register

Standard agent registration (the auth.md protocol): one POST returns an identity, and the token endpoint turns it into a bearer. No card, no console, no person yet, and $0.10 of free credit so the first real calls work. The full recipe is at https://apiforagents.dev/auth.md.

```bash
curl -X POST https://apiforagents.dev/agent/identity \
  -H "Content-Type: application/json" -d '{"type": "anonymous"}'
# then exchange the identity_assertion for a bearer token
curl -X POST https://apiforagents.dev/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=$ASSERTION"
```

Python:

```python
import requests

reg = requests.post("https://apiforagents.dev/agent/identity", json={"type": "anonymous"}).json()
tok = requests.post("https://apiforagents.dev/oauth2/token", data={
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": reg["identity_assertion"]}).json()
KEY = tok["access_token"]          # catalog.read until a person claims it
CLAIM = reg["claim_token"]         # keep in memory for the claim ceremony
```

Node:

```js
const reg = await (await fetch("https://apiforagents.dev/agent/identity", {
  method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: "anonymous" }),
})).json();
const tok = await (await fetch("https://apiforagents.dev/oauth2/token", {
  method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: reg.identity_assertion }),
})).json();
const key = tok.access_token; // catalog.read until a person claims it
```

Response:

```
200 { "registration_id": "reg_…", "identity_assertion": "<jwt>", "pre_claim_scopes": ["catalog.read"],
      "claim_token": "clm_…", "post_claim_scopes": ["catalog.read", "data.run", "account.read", "account.billing"] }
200 { "access_token": "afa_…", "token_type": "Bearer", "expires_in": 86400, "scope": "catalog.read" }
```

### 2. Quote

Any call with estimateOnly returns the exact dollar amount it would charge and takes nothing. Do this before every paid call.

```bash
curl -X POST https://apiforagents.dev/v1/linkedin \
  -H "Authorization: Bearer $AFA_KEY" -H "Content-Type: application/json" \
  -d '{"op": "profiles", "subjects": ["satyanadella", "jeffweiner08"], "estimateOnly": true}'
```

Python:

```python
import requests, time
H = {"Authorization": f"Bearer {KEY}"}

q = requests.post("https://apiforagents.dev/v1/linkedin", headers=H,
                  json={"op": "profiles", "subjects": ["satyanadella", "jeffweiner08"], "estimateOnly": True})
print(q.json()["cost"]["wouldChargeUsd"])   # 0.0072
```

Node:

```js
const q = await fetch("https://apiforagents.dev/v1/linkedin", {
  method: "POST",
  headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
  body: JSON.stringify({ op: "profiles", subjects: ["satyanadella", "jeffweiner08"], estimateOnly: true }),
});
const { cost } = await q.json(); // cost.wouldChargeUsd === 0.0072
```

Response:

```
200 { "cost": { "usd": 0, "wouldChargeUsd": 0.0072 },
      "coverage": [ { "requested": 2, "returned": 0 } ] }
```

### 3. Get claimed, then funded

Paid calls need a person. Ask for a six-digit code and a link, show both to them, and poll. Once they type the code the account is theirs and your next token carries data.run. The first top-up is $5; a 402 on any call carries the link for it.

```bash
# ask for a code and a link, then show both to the person
curl -X POST https://apiforagents.dev/agent/identity/claim \
  -H "Content-Type: application/json" \
  -d '{"claim_token": "'$CLAIM'", "email": "you@example.com"}'
# poll until they have typed the code
curl -X POST https://apiforagents.dev/oauth2/token \
  -d "grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=$CLAIM"
```

Python:

```python
c = requests.post("https://apiforagents.dev/agent/identity/claim",
                  json={"claim_token": CLAIM, "email": "you@example.com"}).json()["claim_attempt"]
print(f"Open {c['verification_uri']} and enter {c['user_code']}")   # tell the person
while True:
    t = requests.post("https://apiforagents.dev/oauth2/token", data={
        "grant_type": "urn:workos:agent-auth:grant-type:claim", "claim_token": CLAIM}).json()
    if "access_token" in t: KEY = t["access_token"]; break
    time.sleep(c["interval"])        # authorization_pending until the person is done
```

Node:

```js
const c = (await (await fetch("https://apiforagents.dev/agent/identity/claim", {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ claim_token: reg.claim_token, email: "you@example.com" }),
})).json()).claim_attempt;
console.log(`Open ${c.verification_uri} and enter ${c.user_code}`); // tell the person
// poll /oauth2/token with grant_type=urn:workos:agent-auth:grant-type:claim every c.interval seconds
```

Response:

```
200 { "claim_attempt": { "user_code": "123456", "verification_uri": "https://apiforagents.dev/claim?claim_attempt_token=…", "expires_in": 600, "interval": 5 } }
200 { "access_token": "afa_…", "scope": "catalog.read data.run account.read account.billing", "identity_assertion": "<jwt>" }
```

### 4. Run

Same request without the flag. The charge equals the quote. A subject that does not come back is refunded and named in coverage.

```bash
curl -X POST https://apiforagents.dev/v1/linkedin \
  -H "Authorization: Bearer $AFA_KEY" -H "Content-Type: application/json" \
  -d '{"op": "profiles", "subjects": ["satyanadella", "jeffweiner08"]}'
```

Python:

```python
import requests, time
H = {"Authorization": f"Bearer {KEY}"}

r = requests.post("https://apiforagents.dev/v1/linkedin", headers=H,
                  json={"op": "profiles", "subjects": ["satyanadella", "jeffweiner08"]})
body = r.json()
rows, cost = body["data"], body["cost"]["usd"]   # read coverage and notes first
```

Node:

```js
const r = await fetch("https://apiforagents.dev/v1/linkedin", {
  method: "POST",
  headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
  body: JSON.stringify({ op: "profiles", subjects: ["satyanadella", "jeffweiner08"] }),
});
const { data, coverage, cost } = await r.json(); // read coverage and notes first
```

Response:

```
200 { "data": [ … ], "coverage": [ … ], "provenance": [ … ], "confidence": { … },
      "cost": { "usd": 0.0072, "balanceUsd": 4.9928 }, "notes": [] }
```

## The playground

The playground runs any endpoint by hand: pick a platform and what you want from it, fill a short form named in plain words (or press Use sample input), see the free quote on the Run button, run it, then read the answer as a table or JSON and download it as CSV or JSON. The same call is written out as cURL, Python and Node while you type. Open it at https://apiforagents.dev/dashboard/run (sign in first).

## All 26 platforms

Every platform is live on the same key. Each reference lists every request and response field and an example per operation:

- [Instagram](https://apiforagents.dev/platforms/instagram.md): 8 operations
- [LinkedIn](https://apiforagents.dev/platforms/linkedin.md): 13 operations
- [YouTube](https://apiforagents.dev/platforms/youtube.md): 4 operations
- [X / Twitter](https://apiforagents.dev/platforms/x_twitter.md): 5 operations
- [Reddit](https://apiforagents.dev/platforms/reddit.md): 5 operations
- [TikTok](https://apiforagents.dev/platforms/tiktok.md): 9 operations
- [Facebook](https://apiforagents.dev/platforms/facebook.md): 10 operations
- [Amazon](https://apiforagents.dev/platforms/amazon.md): 4 operations
- [Shopify](https://apiforagents.dev/platforms/shopify.md): 2 operations
- [Farfetch](https://apiforagents.dev/platforms/farfetch.md): 4 operations
- [GOAT](https://apiforagents.dev/platforms/goat.md): 4 operations
- [StockX](https://apiforagents.dev/platforms/stockx.md): 4 operations
- [Blinkit](https://apiforagents.dev/platforms/blinkit.md): 4 operations
- [Swiggy Instamart](https://apiforagents.dev/platforms/instamart.md): 4 operations
- [Zepto](https://apiforagents.dev/platforms/zepto.md): 4 operations
- [Indeed](https://apiforagents.dev/platforms/indeed.md): 2 operations
- [Naukri](https://apiforagents.dev/platforms/naukri.md): 2 operations
- [Upwork](https://apiforagents.dev/platforms/upwork.md): 2 operations
- [Contacts](https://apiforagents.dev/platforms/contacts.md): 1 operations
- [Explore](https://apiforagents.dev/platforms/explore.md): 3 operations
- [Google Maps](https://apiforagents.dev/platforms/google_maps.md): 4 operations
- [Yelp](https://apiforagents.dev/platforms/yelp.md): 4 operations
- [Tripadvisor](https://apiforagents.dev/platforms/tripadvisor.md): 8 operations
- [Airbnb](https://apiforagents.dev/platforms/airbnb.md): 3 operations
- [Booking.com](https://apiforagents.dev/platforms/booking.md): 4 operations
- [Google](https://apiforagents.dev/platforms/google.md): 5 operations

## Typed fields and details

Each row in data has two parts. The typed fields are the stable schema of the operation: the same names and types whichever source answered, and always present. A field the source had nothing for is null, an empty list is [], and a key is never just missing, so code written against a field keeps working.

details is always the last field and always present ({} when empty). It holds everything else the source sent for that row, under our own field names. Its keys depend on the source that answered, so read them defensively. When a fact becomes common across sources it becomes a typed field.

Every operation's request fields and response fields, with their types and an example response, are on its platform's reference (Platforms, then API reference), and in openapi.json.

## Quote first, then pay per record

- Quote any call first: the same request with "estimateOnly": true returns cost.wouldChargeUsd, the most it can charge, and takes nothing. Quotes are free.
- Cap a call with "maxUsd": a call that would cost more is refused with budget_exceeded before anything is held.
- When a call runs, the quoted amount is held, the charge settles to what came back (never above the quote), and the rest returns to your balance in the same call.
- A subject that does not come back is named in coverage with its reason and is not charged. A call that fails on our side costs nothing.
- The same request from your account within a day is answered free from what you already fetched.
- Prices are per record in US dollars, listed on the pricing page. Some calls also carry a minimum, such as a page of search results, which is why the quote, not the list, is the number to trust.
- No subscription. Top up from $5; the balance never expires. New accounts and agents start with $0.10 free.
- List prices run from $0.000045 to $0.013 per record. Every price: https://apiforagents.dev/pricing.md

## Request vocabulary

- `op`: The operation. One per call. Each platform page lists its own.
- `subjects`: The handles, URLs or ids the call is about, batched. Up to ten per call on most ops.
- `limit`: How many rows per subject. Every listing op has a cap; the quote uses the cap, the charge uses what came back.
- `query`: Free text for the search ops.
- `postedWithin`: 1h, 24h, 7d, 30d, 90d, 1y or any, for anything with a date.
- `sort`: relevance, newest, oldest, top, where the source offers a choice.
- `estimateOnly`: true returns the price and charges nothing.
- `maxUsd`: A ceiling for this call. The engine refuses above it with budget_exceeded.
- `freshness`: auto reads your own recent answer for free; fresh forces a capture; cached refuses to spend.

## Response envelope

```json
{
  "data": { "companies": [
    { "universalName": "acme-corp", "name": "Acme", "employeesCount": 342,
      "website": null, …, "details": { "tagline": "…" } }
  ] },
  "coverage": [
    { "platform": "linkedin", "requested": 3, "returned": 2,
      "omitted": [ { "subject": "gone-corp", "reason": "not_found" } ] }
  ],
  "provenance": [
    { "subject": "acme-corp", "measuredAt": "2026-10-13T09:41:00Z",
      "source": "capture" }
  ],
  "confidence": { "acme-corp": "measured", "globex": "measured" },
  "cost": { "usd": 0.0072, "balanceUsd": 4.9928,
            "chargedFor": ["linkedin:companies:acme-corp", "linkedin:companies:globex"] },
  "notes": []
}
```

- `data`: The rows, keyed by what they are (profiles, companies, posts…). Every row carries the operation's typed fields, always present and null when the source had nothing, with details last.
- `coverage`: What you asked for against what came back, and why anything is missing.
- `provenance`: When each subject was measured and whether it was a fresh capture or a cached read.
- `confidence`: Measured, derived or unknown. Never a blend.
- `cost`: What this call charged in dollars, what is left, and which subjects you paid for.
- `notes`: Plain sentences when something needs saying. Empty when nothing does.

## Errors

Branch on `error.code`. `fix` is the next step; `details` carries the numbers.

```
HTTP 402
{ "error": {
    "code": "insufficient_funds",
    "message": "This call needs $0.12. The account holds $0.03.",
    "fix": "Top up at the link, or lower limit to 20.",
    "details": { "neededUsd": 0.12, "balanceUsd": 0.03,
                 "topUp": "https://apiforagents.dev/pay/acc_7f3k" },
    "docs": "https://apiforagents.dev/docs.md#insufficient_funds" } }
```

| Code | Status | Meaning |
|---|---|---|
| invalid_request | 400 | A field is missing or malformed. details names it. Fix the request; nothing was charged. |
| unauthorized | 401 | No key, or a revoked one. Create an account or mint a new key. |
| insufficient_funds | 402 | The balance cannot cover the quote. details carries neededUsd, balanceUsd and a topUp link. |
| budget_exceeded | 402 | The call would cost more than maxUsd. Raise maxUsd or lower limit. Nothing was charged. |
| subject_rejected | 422 | The source refused one of your own values, named in details. Change it; the rest of the call ran. |
| rate_limited | 429 | Too many calls at once for this key. retryAfterSeconds says when. |
| source_unavailable | 503 | The source is down or refusing. Charged nothing. Try later; the response says when. |

## Limits and conduct

- Sixty calls a minute per key. The 429 says when to retry.
- One anonymous registration an hour per address, five unclaimed at once, one $0.10 trial per address and 3 per network per day while the day's trial budget lasts; an unclaimed registration expires after a day.
- Ten subjects per call on lookups; every listing op has a cap printed on its platform page.
- Handles come from evidence: a search result, a pasted URL, an earlier answer. A handle typed from a name bills real money and can return a different person.
- Public pages only. Nothing behind a login is read, stored or sold.
- One account per person or company. Accounts made to dodge a limit are closed and the balance returned.
