> ## 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.

# Advanced API Usage

> Query your AI visibility data and generate reports programmatically with the Searchable API

## Overview

Searchable exposes three programmatic surfaces. Pick the one that fits your use case:

<CardGroup cols={3}>
  <Card title="MCP (AI assistants)" icon="robot" href="/integrations/mcp">
    30+ read tools for Claude, Cursor, and other MCP clients — the richest way to query your data.
  </Card>

  <Card title="REST API" icon="code" href="/api-reference/introduction">
    Fetch audits & issues and generate shareable reports with an API key — see the interactive
    reference + playground.
  </Card>

  <Card title="Ingest API" icon="database" href="/setup/rest-api">
    Send AI-bot traffic events from any language or platform.
  </Card>
</CardGroup>

<Info>
  API access requires a **paid Searchable plan** — API keys can't be created on the Free plan.
</Info>

## 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.

Send it as a Bearer token on every request:

```
Authorization: Bearer sea_your_key_here
```

| Base URL | `https://app.searchable.com` |
| -------- | ---------------------------- |

### Scopes

Each key carries one or more scopes. Read endpoints require `read` (the default); write endpoints require `write`.

| Scope   | Grants                                       |
| ------- | -------------------------------------------- |
| `read`  | Fetch audits, issues, and visibility data    |
| `write` | Everything in `read`, plus report generation |
| `admin` | All scopes                                   |

A key can also be **bound to a single project**. A project-bound key only works against that project; leave a key unbound to use it across all projects you can access.

## Fetch audits + issues

Retrieve the latest audit per page, when each was last completed, and optionally the open issues for each page.

```
GET /api/v1/projects/{projectId}/audits?includeIssues=true
```

```javascript theme={null}
const res = await fetch(
  "https://app.searchable.com/api/v1/projects/PROJECT_ID/audits?includeIssues=true",
  { headers: { Authorization: "Bearer sea_YOUR_KEY" } }, // requires the `read` scope
);
const { summary, audits } = await res.json();
// summary.totalPages     — pages with at least one completed audit (not all tracked pages)
// summary.lastCompletedAt — newest completed audit across all pages
// audits[].openIssues     — present only when includeIssues=true
```

## Share of voice & competitors

Brand vs competitor share of voice, and the project's real/tracked competitor roster (also available as the `get_share_of_voice` and `get_competitors` MCP tools). All four endpoints accept the same filter family: `days`, `platform`, `topicId`, `unbranded`/`branded`, `country`/`locationId`.

```
GET /api/mcp/projects/{projectId}/share-of-voice
GET /api/mcp/projects/{projectId}/share-of-voice/history
GET /api/mcp/projects/{projectId}/competitors
GET /api/mcp/projects/{projectId}/competitors/{competitorId}
```

```javascript theme={null}
const res = await fetch(
  "https://app.searchable.com/api/mcp/projects/PROJECT_ID/share-of-voice?days=30",
  { headers: { Authorization: "Bearer sea_YOUR_KEY" } }, // requires the `read` scope
);
const { brand, competitors, dateRange } = await res.json();
// brand.delta / competitors[].delta are day-over-day (today vs yesterday),
// not a comparison against a prior period the length of `days`.
// mentions/citations are always null on this endpoint — call
// GET .../competitors or GET .../competitors/{competitorId} for those counts.
```

Two things to know on the competitors endpoints: the list view's `mentions` is an **all-time** count that ignores `days` and the filters (mirrors the in-app Competitors list), and the detail view carries **two distinct metrics** — the top-level `citations` counts inline citations on responses that *mention* the competitor (any domain), while `history[].domainSources` counts source URLs (pages the engines consulted for generation, not inline citations) on the competitor's *own* domain.

## AI Traffic API

First-party AI-traffic measurement — crawler visits and AI-referral sessions sourced from the tracker/CDN pipeline (also available through the single consolidated `get_ai_traffic` MCP tool).

```
GET /api/mcp/projects/{projectId}/traffic/overview
GET /api/mcp/projects/{projectId}/traffic/bots
GET /api/mcp/projects/{projectId}/traffic/referrals
GET /api/mcp/projects/{projectId}/traffic/top-cited-pages
GET /api/mcp/projects/{projectId}/traffic/sitemap-coverage
GET /api/mcp/projects/{projectId}/traffic/human
GET /api/mcp/projects/{projectId}/traffic/correlation
GET /api/mcp/projects/{projectId}/traffic/attribution
GET /api/mcp/projects/{projectId}/traffic/logs
```

```javascript theme={null}
const res = await fetch(
  "https://app.searchable.com/api/mcp/projects/PROJECT_ID/traffic/overview?days=30&groupBy=botCategory",
  { headers: { Authorization: "Bearer sea_YOUR_KEY" } }, // requires the `read` scope
);
const { groupBy, crawlers, referrals, topPages, dateRange } = await res.json();
// crawlers.total / referrals.total are window totals; byPlatform breaks each
// down by AI platform (openai, anthropic, google, perplexity, microsoft, …).
// Because groupBy=botCategory, crawlers.byCategory is also present.
// topPages is crawl-ranked (top 10) — its `referrals` field is always null;
// call top-cited-pages with a platform for per-page AI-referral counts.
```

`overview`, `bots`, and `correlation` accept optional `groupBy=platform|botCategory|botName`. Omitting it preserves the legacy response. `botCategory` adds `byCategory`; `botName` adds `byBot` with `botId`, `botName`, `botVendor`, and `botCategory`; and correlation's `platform` option adds the per-page `byPlatform` split. Existing `byPlatform` arrays on overview and bots remain present for every grouping. These arrays classify **crawl requests only** — correlation does not attribute a referral session to a particular crawler.

The public crawler category set is:

* `ai_training` — model-training collection, such as GPTBot and ClaudeBot
* `ai_search` — AI retrieval/search fetching, such as OAI-SearchBot, Claude-SearchBot, and PerplexityBot
* `ai_assistant` — a distinct user-triggered assistant fetch, such as ChatGPT-User, Claude-User, and Perplexity-User
* `ai_agent` — autonomous agents acting on a user's behalf
* `search_engine` — conventional search indexing

The underlying registry currently also stores `ad_inventory`, `ad_landing`, and `agentic-commerce`.
Those categories are dashboard-hidden and filtered out of these public crawler aggregates. The
registry column is free text, so the stored set can grow; if a new category becomes
dashboard-visible, it becomes an additive public output value.

That makes `ai_training → ai_search → ai_assistant` available as a per-page crawl funnel. `ai_search` is evidence of retrieval/search crawling, not proof that the page appeared as a citation in an answer; use the citation/visibility APIs for that outcome.

`bots` returns a paginated per-page crawler-activity list — `items[]` and `pagination` (`limit`/`offset`/`returnedCount`/`hasMore`/`nextOffset`; `totalCount` is always `null` since the ranking query doesn't cheaply support an exact cross-platform total). `referrals` returns a daily AI-referral session timeseries by platform plus window `totals`. `top-cited-pages` **requires** an explicit `platform` (one of `openai`, `anthropic`, `google`, `perplexity`, `microsoft`) and returns `400` without one. `platform` and `groupBy` are validated strictly: an unrecognized value returns `400 invalid_argument` listing the supported keys, never a silent empty result.

AI Traffic endpoints return `409 { code: "traffic_not_connected", message, howToFix }` when the project lacks the source required by that report — `howToFix` links straight to **AI Traffic → Setup** in the dashboard. Connect a source there, then retry.

## Shopping visibility

AI shopping/product visibility (Peec-parity) — products surfaced in AI shopping/product-carousel responses (also available as the `get_shopping_visibility` MCP tool, `view=summary|timeseries`, or pass `productId` for detail).

```
GET /api/mcp/projects/{projectId}/shopping
GET /api/mcp/projects/{projectId}/shopping/products/{productId}
GET /api/mcp/projects/{projectId}/shopping/timeseries
```

```javascript theme={null}
const res = await fetch(
  "https://app.searchable.com/api/mcp/projects/PROJECT_ID/shopping?days=30",
  { headers: { Authorization: "Bearer sea_YOUR_KEY" } }, // requires the `read` scope
);
const { available, products, summary, dateRange } = await res.json();
// available:false (200, not an error) means no shopping/product data yet —
// this is a data-presence state, not a broken integration. It appears
// automatically once tracked prompts trigger an AI shopping carousel.
// products[].platforms is a single-element array (the product's one
// dominant platform) — call the product-detail endpoint below for a real
// per-platform breakdown.
```

`productId` is the product's raw title (the `id` returned by the list endpoint above) — percent-encode it in the URL. The detail endpoint returns occurrences, vendor/pricing rows, a **real** per-platform breakdown, and up to 50 recent responses that surfaced the product. Requires a plan with **Shopping Analytics** (Scale or higher) — returns `403 { requiresUpgrade: true }` if the workspace's plan doesn't include it and the project has data to fetch (an empty project still returns `available:false` first).

## Ads

Sponsored ads surfaced in AI answers — who is advertising, the actual ad creatives, ad-heavy prompts, and the frequency trend (also available as the `get_ads` MCP tool, `view=advertisers|creatives|prompts|timeseries`).

```
GET /api/mcp/projects/{projectId}/ads/advertisers
GET /api/mcp/projects/{projectId}/ads/creatives
GET /api/mcp/projects/{projectId}/ads/prompts
GET /api/mcp/projects/{projectId}/ads/timeseries
```

```javascript theme={null}
const res = await fetch(
  "https://app.searchable.com/api/mcp/projects/PROJECT_ID/ads/advertisers?days=30",
  { headers: { Authorization: "Bearer sea_YOUR_KEY" } }, // requires the `read` scope
);
const { advertisers, brand, totals, pagination } = await res.json();
// advertisers are ranked by ad volume; `brand` is your own ad share of voice
// and rank among advertisers (null when you're not advertising). adCount /
// appearances count ads we scraped in AI answers, not ad-platform impressions.
```

`/ads/advertisers` is offset-paginated (`{limit, offset, returnedCount, totalCount, hasMore, nextOffset}`); pass an advertiser's `advertiserKey` into `/ads/creatives` or `/ads/timeseries` to drill into one advertiser. `/ads/creatives` also accepts `promptId` and a free-text `search`; `/ads/prompts` accepts `sortBy` (`promptText|adUnits|coveragePct|engines|totalResponses`) + `sortDir`. All windows follow `days` (default 30, max 365). Ad data appears once tracked prompts surface sponsored ad units. `promptId` takes a prompt ID, not prompt text — an unrecognized ID matches nothing rather than erroring, so an empty result can mean an unresolved filter as well as no captured data. These endpoints take no topic or location filters; use the `get_ads` MCP tool for those.

## Brand profile & domain authority

The knowledge-base brand profile plus AI-observed brand facts, and the domain's Moz authority history (also available via the `get_brand_profile` MCP tool, which takes `include: "domain_authority"` for the Moz block).

```
GET /api/mcp/projects/{projectId}/brand-profile
GET /api/mcp/projects/{projectId}/domain-authority
```

```javascript theme={null}
const res = await fetch(
  "https://app.searchable.com/api/mcp/projects/PROJECT_ID/brand-profile",
  { headers: { Authorization: "Bearer sea_YOUR_KEY" } }, // requires the `read` scope
);
const { profile, facts, factCount } = await res.json();
// profile is the user-curated knowledge base (null until onboarding creates
// one): brand name, headline, category, positioning, description, topics,
// business type, connected entities, competitor geography/context.
// facts[] is the latest weekly batch of statements AI platforms actually make
// about the brand — filter with ?platform= and ?verificationStatus=
// (unverified | pass | fail).
```

`domain-authority` returns the latest Moz snapshot (`domainAuthority`, `pageAuthority`, `spamScore`, `linkingRootDomains`, `fetchedAt`) plus `history[]` over `days` (default 365, max 1095 — snapshots are roughly monthly). It reads the app-maintained cache only and never triggers a live Moz fetch: `latest` is the newest snapshot regardless of the window, and `latest: null` means the app hasn't fetched DA for this project yet.

## Prompt catalog

The project's prompt configuration — what it monitors, what's paused, and what the AI has suggested but nobody has reviewed yet (also available as the `list_prompts` MCP tool). This is configuration data, not performance: for per-prompt visibility scores use `/visibility/prompts`.

```
GET /api/mcp/projects/{projectId}/prompts
```

```javascript theme={null}
const res = await fetch(
  "https://app.searchable.com/api/mcp/projects/PROJECT_ID/prompts?status=tracked",
  { headers: { Authorization: "Bearer sea_YOUR_KEY" } }, // requires the `read` scope
);
const { summary, prompts, pagination } = await res.json();
// summary always reports all three counts: { tracked, untracked, suggested }.
// Tracked/untracked prompts carry type, intent, branded flag, topics,
// tracked locations, and keyword volume/difficulty.
```

`status` selects the slice: `tracked` (default — actively monitored), `untracked` (paused), `all` (both), or `suggested` — the pending review queue of AI-proposed prompts (from research runs and the agent) that nobody has accepted or rejected yet, each with the `reason` it was proposed and its `source`. Accepted suggestions graduate into the tracked catalog and leave the queue. `topicId` restricts any slice to one topic; `limit` (default 50, max 200) + `offset` paginate with the standard `pagination` shape.

## Prompt answers

Raw, per-response AI-answer data for one prompt (Profound-parity) — platform, response date, response text, brand/competitor mentions, and the source URLs the engine consulted (`sources[]` — pages used for generation, not inline citations), one entry per AI response (also available as the `get_prompt_answers` MCP tool).

```
GET /api/mcp/projects/{projectId}/prompts/{promptId}/responses
```

```javascript theme={null}
const res = await fetch(
  "https://app.searchable.com/api/mcp/projects/PROJECT_ID/prompts/PROMPT_ID/responses?days=30&limit=10",
  { headers: { Authorization: "Bearer sea_YOUR_KEY" } }, // requires the `read` scope
);
const { prompt, responses, pagination } = await res.json();
// responses[].text is hard-truncated at 2000 characters; responses[].truncated
// signals the cut. citations only includes source-type citations (the same
// filter /api/analytics/visibility/query-detail applies).
```

`limit` is clamped to 1–50 (default 10); `?limit=0` clamps to 1, it never silently falls back to the default. `pagination` follows the standard `{limit, offset, returnedCount, totalCount, hasMore, nextOffset}` shape (see [Pagination](/integrations/mcp#pagination)). Returns `404` if `promptId` doesn't exist or belongs to a different project.

## Generate a shareable report

Generate and publish a report, and get back its public share URL. Also available as the `generate_report` MCP tool (requires the **write** scope and an explicit `confirm: true`); this REST endpoint is the direct path.

```
POST /api/mcp/projects/{projectId}/reports
GET /api/mcp/projects/{projectId}/reports
```

```javascript theme={null}
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",
  },
  body: JSON.stringify({
    reportType: "combined", // "sentiment" | "visibility" | "combined" (default "sentiment")
    timeRange: "30d", // "7d" | "30d" | "90d" | "365d" (default "30d")
    status: "published", // "published" | "draft" (default "published")
    // optional filters: platforms, topicIds, locationIds (string arrays)
    // optional: title, unbrandedOnly, brandedOnly
  }),
});
const { shareUrl, shareToken, reportType, success } = await res.json();
```

`POST` requires the `write` scope and a plan that includes report sharing.

`GET /reports?limit=20&offset=0` (requires only the `read` scope) lists previously generated reports for the project, newest first — `{ reports: [{id, reportType, title, status, shareUrl, createdAt}], pagination }`. It lists every currently-public report in the project (any project member's, not just reports the calling key's user created); drafts and unshared reports are excluded — a report saved with `status: "draft"` won't appear until published, and one that's later unshared drops off too.

## Period comparison

`GET /visibility`, `GET /visibility/topics`, `GET /share-of-voice`, and `GET /sentiment` answer "vs last period" in one call — pass `compare=previous_period` or `compare=previous_year`:

```bash theme={null}
curl -H "Authorization: Bearer $SEARCHABLE_API_KEY" \
  "https://app.searchable.com/api/mcp/projects/PROJECT_ID/visibility?days=30&compare=previous_period"
```

Windows are whole UTC calendar days and never overlap: `previous_period` spans the same number of calendar days as the current window and ends the day **before** the current window's first day; `previous_year` is the current window's calendar days shifted back exactly 365 days. The `comparison.dateRange` in the response is exactly the range the previous-period query ran over. The response gains an additive `comparison` block — nothing else changes shape:

```json theme={null}
{
  "summary": { "visibilityScore": 45.5, "...": "..." },
  "comparison": {
    "mode": "previous_period",
    "dateRange": { "from": "2026-06-11T00:00:00.000Z", "to": "2026-07-11T23:59:59.999Z" },
    "previous": { "visibilityScore": 40, "totalResponses": 180, "...": "..." },
    "delta": { "visibilityScore": 5.5, "totalResponses": 20 }
  }
}
```

`delta` is current minus previous for every metric computable on both sides — a metric that can't be compared is absent, never a fabricated `0`. On `/share-of-voice`, competitor rows additionally carry `previousSov` / `sovDelta` inline (matched by entity display name — the share-of-voice entity identity); on `/visibility/topics`, each topic row carries `comparison.previousMentionedPercentage` / `mentionedPercentageDelta`. On the MCP `get_visibility` tool, `compare` works with `group_by=summary` and `platform`; `prompt` and `location` return `invalid_argument` rather than silently ignoring it.

## Per-URL metrics rollup

One page's three AI series in one call — AI-answer activity citing the URL, AI-referral sessions landing on it, and AI-crawler hits fetching it:

```bash theme={null}
curl -H "Authorization: Bearer $SEARCHABLE_API_KEY" \
  "https://app.searchable.com/api/mcp/projects/PROJECT_ID/pages/metrics?url=/pricing&days=90"
```

`url` is a full URL or a bare path (resolved against the project's domain); a query string is kept, so `/watch?v=abc` and `/watch?v=xyz` are distinct pages. The citation series reports two distinct numbers — `inlineCitations` (the AI answer linked the URL in its text) and `sourceUses` (a model consulted the URL as a source), with daily `sourceUses` buckets. The two traffic series are scoped to the verified project domain the URL's host resolves to and return `available: false` with a `reason`: `traffic_not_connected` when the [LLM Analytics integration](/setup/overview) isn't connected, or `external_url` for a host outside the project's verified domains (external pages get cited too, so the citation series still answers). A citation-lookup failure likewise degrades just that series (`citation_lookup_failed`) rather than reading as a fake zero.

## Query visibility, sources & sentiment

For visibility scores, share of voice, sources, sentiment, shopping visibility, AI ads, raw AI answers, AI traffic, and GA4/GSC data, connect the **[MCP server](/integrations/mcp)** — it exposes read-only tools over the same data and handles auth for you. It's the fastest path for read-heavy automation and works directly inside Claude, Cursor, and other assistants.

## Send AI-bot traffic

To report request events from your own app or CDN (so crawlers like GPTBot and ClaudeBot show up in your dashboard), use the **[REST Ingest API](/setup/rest-api)**. That endpoint uses a separate ingest key (`sk_live_…`) and a signed edge path — see its page for the full contract.

## Rate limiting

Every API key is limited to **600 requests/minute** across the REST surface (`/api/mcp/*`, `/api/v1/*`, and the Looker Studio connector — MCP tool calls have their own separate concurrency limits, not this budget). Two exceptions: `/api/v1/chat` and `/api/v1/chat/stop` don't count against this budget and don't emit the rate-limit headers below (`/api/v1/chat` has its own chat-specific limiter). Every other 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; `X-RateLimit-Reset` is an absolute Unix-epoch second.

The limiter fails open: if its backing store is briefly unavailable, requests are **allowed** (never refused for infrastructure reasons) and the rate-limit headers are omitted from those responses — 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 — see the [error reference](/api/errors#rate-limited).

```javascript theme={null}
const res = await fetch("https://app.searchable.com/api/mcp/projects", {
  headers: { Authorization: "Bearer sea_YOUR_KEY" },
});
if (res.status === 429) {
  const retryAfter = Number(res.headers.get("Retry-After") ?? "60");
  // wait retryAfter seconds, then retry
}
```

## 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 (e.g. after a timeout or a dropped connection) and the API replays the original response instead of repeating the side effect (generating a second report, starting a second audit run, or a second sitemap sync):

```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, so 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. Wait, then retry with the same key — you'll receive the first request's stored response once it completes (or a fresh run if it failed). See the [error reference](/api/errors#idempotency-in-flight).
* A stored response body over **100KB** is not cached — the request still runs normally, and the response carries `X-Idempotent-Skipped: body-too-large` instead of `X-Idempotent-Replay`.
* Keys are **not** fingerprinted to the request body: reusing a key with a *different* body returns the stored response from the first request. Use a fresh key for each distinct operation.
* No `Idempotency-Key` header — the request behaves exactly as before; this is fully opt-in.

## Error handling

Full reference with remediation for every error code: **[API Error Reference](/api/errors)**.

| Status | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Bad request — check parameters / body (`code: "invalid_argument"` and related validation codes)                                                                                                                                                                                                                                                                                                                                                    |
| `401`  | Missing or malformed API key (`code: "unauthorized"`)                                                                                                                                                                                                                                                                                                                                                                                              |
| `403`  | Insufficient scope, plan gate, quota exceeded, or a paused/pitch project (`missing_scope`, `plan_upgrade_required`, `quota_exceeded`, `project_not_runnable`, `pitch_not_supported`)                                                                                                                                                                                                                                                               |
| `404`  | Project not found, or the key isn't allowed to reach it (`code: "not_found"`)                                                                                                                                                                                                                                                                                                                                                                      |
| `409`  | Integration not connected — GSC/GA4 endpoints return `{ code: "gsc_not_connected" \| "ga4_not_connected", message, howToFix }` when the project has no linked Search Console site / GA4 property (connect in **Settings → Integrations**); AI Traffic endpoints return `{ code: "traffic_not_connected", message, howToFix }` when no crawler-log, CDN, or tracker source is connected (connect in **AI Traffic → Setup**). Retry after connecting |
| `429`  | Rate limit exceeded — see [Rate limiting](#rate-limiting) above (`code: "rate_limited"`, `Retry-After` header)                                                                                                                                                                                                                                                                                                                                     |
| `500`  | Server error — retry with exponential backoff (`code: "internal_error"`)                                                                                                                                                                                                                                                                                                                                                                           |
| `504`  | The query or report generation timed out — retryable (`code: "query_timeout"`)                                                                                                                                                                                                                                                                                                                                                                     |

Every error body is `application/problem+json`: alongside the original `error` string (unchanged, for existing integrations), it now also carries `type`, `title`, `code`, `message`, and `requestId`. See the [error reference](/api/errors) for the full body shape and every code.

## API key best practices

<Check>Never commit API keys to version control — use environment variables</Check>
<Check>Scope each key to the minimum it needs (`read` unless it must write)</Check>
<Check>Bind a key to a single project when it only ever touches that project</Check>
<Check>Use separate keys for dev and prod, and rotate them regularly</Check>
<Check>Revoke compromised keys immediately in Settings → Integrations</Check>

## Need more?

Higher rate limits, custom endpoints, or a dedicated integration? Contact **[support@searchable.com](mailto:support@searchable.com)**.

## Next steps

<CardGroup cols={2}>
  <Card title="MCP Integration" icon="robot" href="/integrations/mcp">
    Connect Claude, Cursor, and other assistants to your Searchable data.
  </Card>

  <Card title="REST Ingest API" icon="braces" href="/setup/rest-api">
    Send AI-bot traffic events from any stack.
  </Card>
</CardGroup>
