# Zenrows auth.md

How to get a credential for the Zenrows Fetch API (and related surfaces), how
to self-provision with no human, and how a human later claims the account.

## Numbered flow (start here)

1. **Discover** — `GET https://app.zenrows.com/.well-known/oauth-authorization-server`
   (or `/.well-known/oauth-protected-resource`). Read the `agent_auth` block:
   `register_uri` (the signup `POST`; `signup_endpoint` is the same URL),
   `identity_types_supported`, `anonymous.credential_types_supported`
   (`api_key`), `claim_uri` (issues a fresh claim link), `skill` (this
   document) and `playbook` (the setup skill).
2. **Pick a method**
   - **Anonymous signup** (`identity_types_supported` includes `anonymous`) —
     headless agents, no human present. Continue at Step 3.
   - **MCP OAuth** — human can open a browser in an MCP client. Use the AS
     `registration_endpoint` / `authorization_endpoint` / `token_endpoint`
     (PKCE `S256`). See [MCP OAuth](#mcp-oauth-for-mcp-clients).
   - **Human dashboard / CLI** — `https://app.zenrows.com/register` or
     `npx -y @zenrows/cli` (with no key, the CLI signs up on its first
     `fetch`, `extract`, `batch create` or `browser`; pass an existing key
     instead, since every signup counts against the per-IP cap).
   - **WorkOS ID-JAG / `identity_assertion`** — **not supported**, and there
     is no revocation or events endpoint. Use anonymous signup (or MCP OAuth /
     dashboard) instead.
3. **Register (anonymous)** — `POST` the `register_uri` (empty body). Store
   `apiKey`; surface `claimUrl` to a human.
4. **Claim (when a human wants ownership)** — relay `claimUrl`. If it expired
   or was lost, `POST` the `claim_uri` with your key for a fresh one; poll
   `GET /api/agent/account` for `claimed`. See [Claiming](#claiming-the-account).
5. **Call the API** — `GET https://api.zenrows.com/v1/?apikey=KEY&url=URL`
   (URL-encode `url`). MCP stdio/remote use the same key as Bearer / env.

Scraping API auth is **not** a separate OAuth token exchange — the API key
**is** the credential.

## Scraping API authentication

```
GET https://api.zenrows.com/v1/?apikey=KEY&url=URL
```

## Anonymous, claimable signup

An agent can create its own Free plan account with a single unauthenticated
request. No email, password, or human interaction is required at signup time.

### Request

```
POST https://app.zenrows.com/api/agent/signup
Content-Type: application/json

(empty body)
```

- No authentication required.
- Optional provenance headers (CLI / toolkit): `X-ZR-Agent-Id`, `X-ZR-Client`,
  `X-ZR-Source`, `X-ZR-CLI-Version` — anonymous, no PII.

### Response — `201 Created`

```json
{
  "apiKey": "<api-key>",
  "accountId": "<uuid>",
  "claimUrl": "https://app.zenrows.com/claim/<token>",
  "trialEndsAt": "<iso-8601 timestamp>"
}
```

| Field | Meaning |
| --- | --- |
| `apiKey` | Use immediately as `apikey=` on the scraping API (and as MCP Bearer / `ZENROWS_API_KEY`). |
| `accountId` | Identifier of the created account. |
| `claimUrl` | Human-facing URL to claim the account (set email + password). |
| `trialEndsAt` | End of the first monthly period (signup + 1 month). The Free plan renews monthly; this value is not refreshed. |

The account is **anonymous and unclaimed** until a human claims it. It is on
the Free plan: 5,000 credits per month, 5 concurrent requests, renews
monthly, no top-ups.

### Errors

| `error` | HTTP | Retry | Action |
| --- | --- | --- | --- |
| `rate_limited` | 429 | Later | Wait the `Retry-After` seconds. Default cap: **3 agent signups per client IP per 24 hours** (admin-configurable). Body: `{"error":"rate_limited","message":"…"}`. |
| `signup_disabled` | 503 | No | Kill-switch. Stop and tell the human; retry only if they ask. Do not invent another signup URL. |
| `signup_failed` | 500 | Backoff once | Retry once, then surface to a human. |

Trusted client IP is `CF-Connecting-IP` (else remote addr) — not `X-Forwarded-For`.

## Claiming the account

Surface `claimUrl` to your human. Claiming is **browser-only**:

- `GET https://app.zenrows.com/claim/{token}` — claim form.
- `POST https://app.zenrows.com/claim/{token}` — human sets email + password.

**Agents must never POST `/claim/{token}`**, pay, or upgrade on a human's
behalf — only relay the link.

Claiming preserves usage/history and the plan, and adds no credits; **the
same `apiKey` keeps working**. Claim tokens expire after **30 days**; expired
or already-used tokens return **404**.

### Getting a fresh claim link (`claim_uri`)

```
POST https://app.zenrows.com/api/agent/claim
X-API-Key: <apiKey>

(empty body)
```

`200` for an unclaimed agent account:

```json
{
  "status": "unclaimed",
  "claimUrl": "https://app.zenrows.com/claim/<token>?utm_source=agent",
  "expiresAt": "<iso-8601 timestamp, 30 days out>"
}
```

Every call mints a **new** link and the previous one stops working, so call
it only when the link you hold expired or was lost, then relay the new one.
Do not poll it; poll claim status (below) instead.

| `error` | HTTP | Action |
| --- | --- | --- |
| `invalid_key` | 401 | Key missing or unknown. Needs the human. |
| `already_claimed` | 409 | A person owns the account. Nothing to relay. |
| `not_agent_account` | 409 | Not an agent-signup account; it already has an owner. |
| — | 429 | Too many links requested for this key. Wait the `Retry-After` seconds; do not loop. |

### Claim status (machine-readable)

```
GET https://app.zenrows.com/api/agent/account
X-API-Key: <apiKey>
```

`200`:

```json
{
  "accountId": "<uuid>",
  "claimed": false,
  "isAgent": true,
  "trialEndsAt": "<iso-8601|null>"
}
```

`401` + `{"error":"invalid_key"}` if the key is missing or unknown. Response
never includes email or secrets. A key that is unknown, revoked or rotated
needs the human; never sign up again to get past a 401, exhausted credits
(`AUTH004`) or a cap.

## Discovery

Authoritative on `app.zenrows.com`. `www.zenrows.com` may serve its own
protected-resource document or redirect to app's; either way it points at the
`https://app.zenrows.com` authorization server, and `agent_auth` is always in
app's authorization-server metadata:

- `GET https://app.zenrows.com/.well-known/oauth-protected-resource` — RFC 9728
  + `agent_auth` (`register_uri`, `identity_types_supported`,
  `anonymous.credential_types_supported`, `claim_uri`, `skill`, `playbook`).
- `GET https://app.zenrows.com/.well-known/oauth-authorization-server` — RFC 8414
  + same `agent_auth` block. Top-level `registration_endpoint` is **OAuth DCR**
  for MCP clients — not WorkOS ID-JAG registration.
- `GET https://app.zenrows.com/.well-known/agent-skills/index.json` — skills index
  (`/agent-skills` redirects here).
- `GET https://app.zenrows.com/.well-known/mcp.json` — MCP server card.
- `GET https://app.zenrows.com/agent-onboarding/SKILL.md` — Fetch playbook.
- `GET https://app.zenrows.com/agent-onboarding/auth.md` — this document.

## MCP OAuth (for MCP clients)

When a **human can click** in an MCP client that supports OAuth:

1. Read AS metadata (`authorization_endpoint`, `token_endpoint`,
   `registration_endpoint`, PKCE `S256`).
2. Dynamic client registration → authorize → human **Log in** or **Create Free
   account** (a normal Free plan account — **not** an unclaimed agent account)
   → Allow.
3. Exchange `code` for token. The access token **is** the Zenrows API key.

Prefer anonymous signup when **no human** is present. Remote MCP:
`https://mcp.zenrows.com/mcp`. Stdio MCP (`npx -y @zenrows/mcp`) can also
auto-provision via signup when no `ZENROWS_API_KEY` is set.
