> ## Documentation Index
> Fetch the complete documentation index at: https://docs.searchable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Getting Started

> Authentication, rate limits, idempotency, pagination, and errors for the Searchable REST API

## Overview

The Searchable REST API (`/api/mcp/*` and `/api/v1/projects/{projectId}/audits`) exposes the same AI-visibility, sources, sentiment, traffic, and content data available in the Searchable dashboard and [MCP server](/integrations/mcp), over plain HTTP with a bearer API key. It's a **read-mostly** surface — every endpoint below is a GET except the three mutating POSTs (`/reports`, `/audits`, `/sitemap/refresh`).

Every endpoint in the left sidebar is interactive — send a real request from the browser using your own API key.

<CardGroup cols={2}>
  <Card title="Advanced API Usage" icon="book" href="/advanced/api-usage">
    Narrative walkthroughs + curl/JavaScript examples per data area.
  </Card>

  <Card title="Error Reference" icon="triangle-exclamation" href="/api/errors">
    Every error `code` this API returns, and how to fix it.
  </Card>
</CardGroup>

## Base URL

```
https://app.searchable.com
```

Every path in this reference (e.g. `/api/mcp/projects`) is relative to that base URL.

## Authentication

Create an API key in **Settings → Workspace → Integrations** in your Searchable dashboard. Keys start with `sea_` and are shown only once — store it securely. API access requires a paid Searchable plan; keys can't be created on the Free plan.

Send it as a bearer token on every request:

```
Authorization: Bearer sea_your_key_here
```

<Note>
  This REST surface authenticates with a static `sea_` API key only. The MCP server additionally supports an OAuth 2.1 login flow for interactive AI clients — see [MCP integration](/integrations/mcp) if you're connecting Claude, Cursor, or another MCP client instead of calling REST directly.
</Note>

### Scopes

Each key carries one or more scopes. Scopes are independent grants, not tiers — `write` does **not** implicitly include `read`:

| Scope   | Grants                                                                                |
| ------- | ------------------------------------------------------------------------------------- |
| `read`  | GET endpoints — visibility, sources, sentiment, traffic, content, audits, …           |
| `write` | The three action endpoints (`POST /reports`, `POST /audits`, `POST /sitemap/refresh`) |
| `admin` | Every `read` and `write` endpoint                                                     |

A key that only calls GET endpoints needs `read`. A key that also needs the three action endpoints needs `write` requested alongside `read` (or `admin`, which covers both). A key missing the required scope gets `403 { code: "missing_scope" }` — see [missing\_scope](/api/errors#missing-scope).

### Project binding

A key can optionally be **bound to a single project** at creation time. A project-bound key can only ever act on that one project — even a request for a project its owning user can otherwise access in the dashboard returns `404 { code: "not_found" }`, indistinguishable from a project that doesn't exist. Leave a key unbound to use it across every project you can access. Bind a key when handing it to a single-tenant integration; leave it unbound for an integration that manages multiple projects.

## Pagination

Two conventions appear across this API, depending on the endpoint:

**Offset pagination** (`limit` + `offset`) — the standard shape for most list endpoints:

```json theme={null}
{
  "limit": 20,
  "offset": 0,
  "returnedCount": 20,
  "totalCount": 143,
  "hasMore": true,
  "nextOffset": 20
}
```

Pass `nextOffset` as the next request's `offset` until `hasMore` is `false`. `totalCount` is occasionally `null` on endpoints where an exact cross-partition total isn't cheap to compute (e.g. `GET /traffic/bots`) — `hasMore`/`nextOffset` still work correctly in that case.

Not every paginated endpoint echoes the full block, though — check each operation's response schema in the sidebar for the authoritative shape:

* `GET /issues` and `GET /opportunities` return `limit`/`offset`/`returnedCount`/`totalCount` but omit `hasMore`/`nextOffset` — derive it yourself as `offset + returnedCount < totalCount`.
* `GET /competitors` returns only `totalCount` next to its array, with no `limit`/`offset` echo at all.

**Cursor pagination** (`cursor` + `nextCursor`) — used by the Sources domain's URL-listing endpoints (`GET /sources/domains/{domain}/urls`, `GET /sources/urls`), which read from a keyset-ordered store:

```json theme={null}
{ "urls": [ /* … */ ], "totalUrls": 812, "nextCursor": "eyJvZmZzZXQiOjIwfQ", "hasMore": true }
```

Pass the previous response's `nextCursor` as the next request's `cursor`. Omit `cursor` for the first page. Treat the cursor as opaque — don't parse or construct it.

`limit` defaults and maximums vary by endpoint (typically default 20-30, max 100-250) — see each operation's description in the sidebar.

## Rate limiting

Every API key is limited to **600 requests/minute** across this REST surface (`/api/mcp/*`, `/api/v1/*`, and the Looker Studio connector — MCP *tool calls* have their own separate concurrency limits, not this budget). Every response carries rate-limit headers, whether it succeeds or errors:

```
RateLimit: "default";r=598;t=42
RateLimit-Policy: "default";q=600;w=60
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 598
X-RateLimit-Reset: 1755000042
```

`RateLimit`/`RateLimit-Policy` follow the [IETF rate-limit header draft](https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-08.html) (`r` = remaining, `t` = seconds until reset, `q` = quota, `w` = window in seconds). `X-RateLimit-*` is kept alongside for clients that read the older convention.

The limiter fails open: if its backing store is briefly unavailable, requests are **allowed** and the rate-limit headers are simply omitted from that response — don't hard-require the headers in client code.

Exceeding the limit returns `429` with a `Retry-After` header (seconds) and a `rate_limited` problem+json body.

## Idempotency

The three mutating POST endpoints — `POST /reports`, `POST /audits`, and `POST /sitemap/refresh` — accept an `Idempotency-Key` header. Send the same key on a retry (after a timeout or a dropped connection) and the API replays the original response instead of repeating the side effect:

```javascript theme={null}
const idempotencyKey = crypto.randomUUID();

const res = await fetch(
  'https://app.searchable.com/api/mcp/projects/PROJECT_ID/reports',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer sea_YOUR_KEY',
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey,
    },
    body: JSON.stringify({ reportType: 'sentiment' }),
  },
);
// A retry with the SAME idempotencyKey returns the same body and adds:
//   X-Idempotent-Replay: true
```

Notes:

* Keys are scoped per API key and held for **24 hours**.
* Only a **successful (2xx)** response is stored — a failed attempt (4xx/5xx) is never replayed; retrying after a real failure with the same key simply runs the request again.
* Two **concurrent** requests with the same key never both execute: the second gets `409 { code: "idempotency_in_flight" }` with `Retry-After: 5` while the first is still running.
* A stored response body over 100KB isn't cached — the request still runs normally, and the response carries `X-Idempotent-Skipped: body-too-large` instead of `X-Idempotent-Replay`.
* No `Idempotency-Key` header — the request behaves exactly as before. Fully opt-in.

## Errors

Every non-2xx response is [`application/problem+json`](https://www.rfc-editor.org/rfc/rfc9457) — a machine-readable envelope that additively extends the API's original `{ error: "..." }` shape, so existing integrations that only read `error` keep working unmodified:

```json theme={null}
{
  "type": "https://docs.searchable.com/api/errors#not_found",
  "title": "Not Found",
  "status": 404,
  "code": "not_found",
  "error": "Project not found or access denied",
  "message": "Project not found or access denied",
  "requestId": "b3f1c2a0-4e9d-4a11-9c3a-8e2f6d1a7c55"
}
```

`code` is the field to branch on — it's stable across releases. Full catalog, per-code remediation, and additive fields (`requiresUpgrade`, `retryable`, `current`/`limit`, …): **[API Error Reference](/api/errors)**.

Beyond the statuses listed per operation, any endpoint may also return a generic `500` or `502` problem+json with `code: "internal_error"` on an unexpected internal failure (the `502` variant carries `retryable: true`) — treat both as transient and retry with exponential backoff.

Every response — success or error — also carries `X-Request-Id`. Include it when contacting **[support@searchable.com](mailto:support@searchable.com)** about a specific request.

## Typed clients

There's no official SDK — the OpenAPI spec **is** the contract. Call the API directly with `fetch`/`curl`/your HTTP client of choice, or generate TypeScript types straight from the spec with the MIT-licensed [`openapi-typescript`](https://openapi-ts.dev):

```bash theme={null}
npx openapi-typescript https://docs.searchable.com/api-reference/openapi.json -o searchable-api.d.ts
```

That produces a `paths` / `components` type tree covering every endpoint and schema on this page — pair it with a typed `fetch` wrapper (e.g. [`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/)) for end-to-end type safety with no generated SDK to maintain. An official SDK isn't planned unless customer demand shows up.

## Next steps

<CardGroup cols={2}>
  <Card title="Agent API" icon="robot" href="/api-reference/agent-api">
    Programmatic access to the Searchable agent, for approved customers.
  </Card>

  <Card title="Changelog" icon="clock-rotate-left" href="/changelog">
    What's new on this API, and the deprecation policy.
  </Card>
</CardGroup>
