# xSeek API reference

xSeek tracks how AI search engines (ChatGPT, Perplexity, Gemini, Google AI
Overview and others) answer questions about a brand: which companies they name,
which pages they cite, and what a site should publish next to get cited.

This file is the whole v1 API in one page, written to be pasted into an AI agent
or coding assistant. Everything below is generated from the same definitions that
render the HTML docs at https://www.xseek.io/api-docs, so it cannot drift from them.

- **Base URL:** `https://www.xseek.io`
- **Format:** JSON request and response bodies. UTF-8.
- **Endpoints documented here:** 38

## Authentication

Every v1 endpoint takes an API key as a bearer token:

```bash
curl -H "Authorization: Bearer $XSEEK_API_KEY" \
  "https://www.xseek.io/api/v1/websites/$WEBSITE_ID/metrics/ai-clicks?days=30"
```

Create a key in the dashboard under **Settings → API keys**. A key belongs to one
organization and can only read that organization's websites; asking for a
`websiteId` outside it returns `404`, not `403`, so an ID cannot be probed.

### Privileges

Keys carry explicit privileges. A call missing one returns `401` with a message
naming the privilege it wanted. Grant the narrowest set that works:

| Privilege | Grants |
| --- | --- |
| `ai_visits:push` | Write AI crawler visits |
| `ai_visits:read` | Read AI crawler visits and referral-click metrics |
| `llm_queries:read` | Read the LLM queries a site appears in |
| `websites:create` / `websites:read` / `websites:update` | Manage websites |
| `prompts:create` / `prompts:read` / `prompts:update` | Manage tracked prompts, topics, share of voice |
| `articles:read` / `articles:write` | Read and push Content Studio articles |
| `images:write` | Upload article images |
| `articles:publish` | Publish articles to the website (an API key with `articles:write` also publishes) |
| `brand:write` | Edit the brand profile, competitors, audiences and notes |
| `integrations:write` | Connect Google and set up tracking |

### Errors

| Status | Meaning |
| --- | --- |
| `400` | Validation failed. The body carries `details[]` with the offending field. |
| `401` | Missing, invalid, disabled, or under-privileged key. |
| `404` | The website or record does not exist **or** is not yours. |
| `500` | Server error. Safe to retry with backoff. |

## Reading the numbers correctly

Three rules the data follows. An agent that ignores them will report things that
are not true:

1. **An unmeasured value is `null`, never `0`.** If GA4 is not connected, the
   metrics endpoints return `available: false` with an `unavailableReason`
   instead of a row of zeros. A page with no Search Console row returns
   `impressions: null`. Do not coerce these to `0` before charting.
2. **`/ai-visits` and `/metrics/ai-clicks` are different things.** The first
   counts AI **crawlers** fetching your pages, where nobody landed on the site.
   The second counts **real people** who arrived from an AI assistant, measured
   as GA4 sessions. Labelling one as the other is the most common way to build a
   wrong dashboard.
3. **Share of voice is measured against the tracked set**, meaning your brand
   plus the competitors configured for that website, aliases included. Topics
   flagged `excludeFromSov` are always removed.

## Contents

- **Reporting metrics**
  - `GET /api/v1/websites/{websiteId}/metrics/ai-clicks`
  - `GET /api/v1/websites/{websiteId}/metrics/top-pages`
  - `GET /api/v1/websites/{websiteId}/metrics/share-of-voice`
- **Prompts & topics**
  - `GET /api/v1/websites/{websiteId}/prompts`
  - `POST /api/v1/websites/{websiteId}/prompts`
  - `GET /api/v1/websites/{websiteId}/prompts/{promptId}`
  - `PUT /api/v1/websites/{websiteId}/prompts/{promptId}`
  - `GET /api/v1/websites/{websiteId}/prompts/{promptId}/runs`
  - `GET /api/v1/websites/{websiteId}/sentiment/prompts`
  - `GET /api/v1/websites/{websiteId}/prompts/leaderboard`
  - `GET /api/v1/websites/{websiteId}/topics`
- **Action plan**
  - `GET /api/v1/websites/{websiteId}/opportunities`
- **Content Studio (articles)**
  - `GET /api/v1/websites/{websiteId}/articles`
  - `POST /api/v1/websites/{websiteId}/articles`
  - `GET /api/v1/websites/{websiteId}/articles/{articleId}`
  - `PATCH /api/v1/websites/{websiteId}/articles/{articleId}`
  - `GET /api/v1/websites/{websiteId}/articles/by-slug/{slug}`
  - `GET /api/v1/websites/{websiteId}/articles/{articleId}/comments`
  - `POST /api/v1/websites/{websiteId}/articles/{articleId}/comments`
  - `PATCH /api/v1/websites/{websiteId}/articles/{articleId}/comments`
- **Websites & data**
  - `GET /api/v1/websites/{websiteId}/llm-queries`
  - `GET /api/v1/websites/{websiteId}/llm-queries/by-page`
  - `GET /api/v1/websites`
  - `POST /api/v1/websites`
  - `PUT /api/v1/websites/{websiteId}`
  - `GET /api/v1/websites/{websiteId}/sources`
  - `GET /api/v1/websites/{websiteId}/sentiment/runs`
  - `GET /api/v1/websites/{websiteId}/ai-visits`
  - `POST /api/v1/websites/{websiteId}/ai-visits`
  - `GET /api/v1/websites/{websiteId}/keywords`
  - `GET /api/v1/websites/{websiteId}/keywords`
- **Technical**
  - `GET /api/v1/robots`
  - `GET /api/v1/tools/robots-txt`
  - `GET /api/v1/websites/{websiteId}/robots-scans`
  - `GET /api/v1/websites/{websiteId}/robots-scans/latest`
- **API keys**
  - `GET /api/v1/api-keys`
  - `POST /api/v1/api-keys`
- **Other**
  - `GET /api/v1/tools/llms-txt`

## Reporting metrics

### GET /api/v1/websites/{websiteId}/metrics/ai-clicks

AI referral clicks: total, share of total traffic, per-engine breakdown and a daily time series. These are real visitors arriving from an AI assistant, measured as GA4 sessions — not AI crawler visits. Requires GA4 to be connected; when it is not, the response returns available:false with a reason instead of zeros. Privilege: ai_visits:read

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `startDate` | string | no | Start date, YYYY-MM-DD. Defaults to 30 days ago |
| `endDate` | string | no | End date, YYYY-MM-DD. Defaults to yesterday, because the current day is still being written and would report a partial count |
| `days` | number | no | Trailing window shorthand, 1-365. Ignored when startDate is given |

**Response**

```json
{
  "success": true,
  "data": {
    "metric": "aiReferralClicks",
    "startDate": "2026-07-01",
    "endDate": "2026-07-31",
    "available": true,
    "unavailableReason": null,
    "totalAiClicks": 7751,
    "aiShareOfTraffic": 0.073,
    "totalSessions": 106178,
    "enginesCount": 13,
    "byEngine": [
      {
        "engine": "ChatGPT",
        "clicks": 6867,
        "share": 0.886
      },
      {
        "engine": "Gemini",
        "clicks": 1250,
        "share": 0.161
      }
    ],
    "timeseries": [
      {
        "date": "2026-07-01",
        "engines": [
          {
            "engine": "ChatGPT",
            "clicks": 210
          },
          {
            "engine": "Gemini",
            "clicks": 38
          }
        ]
      }
    ]
  }
}
```

---

### GET /api/v1/websites/{websiteId}/metrics/top-pages

Pages ranked by AI referral clicks, with Search Console impressions and the assistant sending the most traffic to each. Note that aiClicks and impressions do not cover the same days: clicks honour your date range, while impressions come from a fixed Search Console scan of the last 7, 30 or 90 days. The response says which one was used in impressionsWindowDays. A page with no Search Console row returns impressions null, not 0. Privilege: ai_visits:read

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `startDate` | string | no | Start date, YYYY-MM-DD. Defaults to 30 days ago |
| `endDate` | string | no | End date, YYYY-MM-DD. Defaults to yesterday, because the current day is still being written and would report a partial count |
| `days` | number | no | Trailing window shorthand, 1-365 |
| `pathPrefix` | string | no | Only pages under this path, e.g. /blog |
| `sortBy` | string | no | aiClicks (default) or impressions |
| `page` | number | no | Page number (default 1) |
| `pageSize` | number | no | Results per page (default 50, max 200) |

**Response**

```json
{
  "success": true,
  "data": {
    "metric": "aiReferralClicks",
    "startDate": "2026-07-01",
    "endDate": "2026-07-31",
    "available": true,
    "unavailableReason": null,
    "impressionsWindowDays": 30,
    "pages": [
      {
        "url": "/blog/ai-agent-customer-support",
        "aiClicks": 84,
        "impressions": 52500,
        "topEngine": "ChatGPT"
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 50,
      "total": 128,
      "totalPages": 3
    }
  }
}
```

---

### GET /api/v1/websites/{websiteId}/metrics/share-of-voice

Share of voice over time, optionally scoped to one topic or audience. The denominator is the tracked set (your brand plus its competitors, aliases included), which is the same figure the xSeek dashboard shows. Topics flagged excludeFromSov are always removed. Privilege: prompts:read

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `startDate` | string | no | Start date, YYYY-MM-DD. Defaults to 30 days ago |
| `endDate` | string | no | End date, YYYY-MM-DD. Defaults to yesterday, because the current day is still being written and would report a partial count |
| `days` | number | no | Trailing window shorthand, 1-365 |
| `granularity` | string | no | day (default) or week. On week, each bucket date is the Monday it belongs to, so the first bucket can start before startDate and covers only the days inside the range |
| `topicId` | string | no | Restrict to one topic. Get ids from /topics |
| `audienceId` | string | no | Restrict to one audience. Ignored when topicId is set |

**Response**

```json
{
  "success": true,
  "data": {
    "startDate": "2026-07-01",
    "endDate": "2026-07-31",
    "granularity": "day",
    "denominator": "trackedCompetitors",
    "vertical": {
      "id": "topic-uuid",
      "name": "Customer support",
      "kind": "topic"
    },
    "shareOfVoice": 0.11,
    "yourMentions": 902,
    "totalMentions": 8201,
    "timeseries": [
      {
        "date": "2026-07-01",
        "shareOfVoice": 0.11,
        "yourMentions": 29,
        "totalMentions": 264
      }
    ]
  }
}
```


## Prompts & topics

### GET /api/v1/websites/{websiteId}/prompts

List all prompts for a specific website

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Response**

```json
[
  {
    "id": "456",
    "websiteId": "123",
    "prompt": "What are the best practices for SEO optimization?",
    "competitors": [
      "competitor1.com",
      "competitor2.com",
      "competitor3.com"
    ],
    "mainCompany": "MyCompany Inc.",
    "audienceId": "audience-uuid",
    "topicId": "topic-uuid",
    "isActive": true,
    "frequency": "daily",
    "createdAt": "2024-01-01T00:00:00Z",
    "updatedAt": "2024-01-01T00:00:00Z"
  },
  {
    "id": "789",
    "websiteId": "123",
    "prompt": "How to improve website loading speed?",
    "competitors": [
      "fastsite.com",
      "speedysite.com"
    ],
    "mainCompany": "MyCompany Inc.",
    "audienceId": "audience-uuid",
    "topicId": null,
    "isActive": true,
    "frequency": "weekly",
    "createdAt": "2024-01-01T00:00:00Z",
    "updatedAt": "2024-01-01T00:00:00Z"
  }
]
```

---

### POST /api/v1/websites/{websiteId}/prompts

Add a new prompt to a specific website.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `prompt` | string | yes | The prompt text to add |
| `competitors` | array | no | Array of competitor domain names or URLs (defaults to website competitors if not provided) |
| `mainCompany` | string | no | The main company name for comparison (defaults to website company name if not provided) |
| `isActive` | boolean | no | Whether the prompt is active for tracking (defaults to true) |
| `frequency` | string | no | How often to run this prompt (daily, weekly, monthly) |

**Response**

```json
{
  "id": "456",
  "websiteId": "123",
  "prompt": "What are the best practices for SEO optimization?",
  "competitors": [
    "competitor1.com",
    "competitor2.com",
    "competitor3.com"
  ],
  "mainCompany": "MyCompany Inc.",
  "isActive": true,
  "frequency": "daily",
  "createdAt": "2024-01-01T00:00:00Z",
  "updatedAt": "2024-01-01T00:00:00Z"
}
```

---

### GET /api/v1/websites/{websiteId}/prompts/{promptId}

Get a prompt details for a website

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |
| `promptId` | string | yes | The unique identifier of the prompt |

**Response**

```json
{
  "id": "456",
  "websiteId": "123",
  "name": "My Prompt",
  "prompt": "What are the best practices for SEO optimization?",
  "mainCompany": "MyCompany Inc.",
  "competitors": [
    "competitor1.com",
    "competitor2.com"
  ],
  "isActive": true,
  "frequency": "daily",
  "lastRun": "2024-01-02T00:00:00Z",
  "createdAt": "2024-01-01T00:00:00Z",
  "updatedAt": "2024-01-02T00:00:00Z"
}
```

---

### PUT /api/v1/websites/{websiteId}/prompts/{promptId}

Update an existing prompt for a website

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |
| `promptId` | string | yes | The unique identifier of the prompt |

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | Prompt display name (max 100 chars) |
| `prompt` | string | no | Prompt content |
| `mainCompany` | string | no | Main company to compare |
| `competitors` | array | no | Array of competitor names |
| `isActive` | boolean | no | Enable or disable prompt |
| `frequency` | string | no | Run frequency: daily, weekly, monthly |

**Response**

```json
{
  "id": "456",
  "websiteId": "123",
  "name": "My Prompt",
  "prompt": "Updated prompt text",
  "mainCompany": "MyCompany Inc.",
  "competitors": [
    "competitor1.com",
    "competitor2.com"
  ],
  "isActive": true,
  "frequency": "weekly",
  "lastRun": "2024-01-02T00:00:00Z",
  "createdAt": "2024-01-01T00:00:00Z",
  "updatedAt": "2024-01-03T00:00:00Z"
}
```

---

### GET /api/v1/websites/{websiteId}/prompts/{promptId}/runs

Get the latest runs for a prompt

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |
| `promptId` | string | yes | The unique identifier of the prompt |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | number | no | Page number (default 1) |
| `pageSize` | number | no | Results per page (default 10, max 100) |

**Response**

```json
[
  {
    "id": "run-1",
    "promptId": "456",
    "model": "gpt-5-nano",
    "mentions": [
      {
        "company": "MyCompany Inc.",
        "count": 5,
        "context": [
          "snippet 1",
          "snippet 2"
        ]
      }
    ],
    "createdAt": "2024-01-02T01:00:00Z"
  },
  {
    "id": "run-2",
    "promptId": "456",
    "model": "gpt-5-nano",
    "mentions": [
      {
        "company": "Rival Co",
        "count": 3,
        "context": [
          "snippet"
        ]
      }
    ],
    "createdAt": "2024-01-01T12:00:00Z"
  }
]
```

---

### GET /api/v1/websites/{websiteId}/sentiment/prompts

How the AI engines speak of the brand, per tracked prompt, worst first. Every answer that names a tracked brand is read once and labelled positive, neutral, mixed or negative, with the sentence that decided it; this rolls those readings up per prompt. Only readings at or above the confidence floor (0.6) count, the same floor the AI Visibility score uses. Pass company= to read a competitor instead of the brand.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `days` | number | no | Window in days (default 30, max 365) |
| `label` | string | no | positive \| neutral \| mixed \| negative: only prompts with at least one answer carrying this label |
| `company` | string | no | A tracked competitor name or alias; default is the site's own brand |
| `minRead` | number | no | Only prompts with at least this many read answers (default 1) |

**Response**

```json
{
  "success": true,
  "data": [
    {
      "promptId": "a1b2c3d4-…",
      "query": "Quels détaillants de piscines offrent le meilleur rapport qualité-prix en 2026?",
      "topic": {
        "id": "t1",
        "name": "Piscines"
      },
      "audience": null,
      "read": 12,
      "counts": {
        "positive": 5,
        "neutral": 4,
        "mixed": 1,
        "negative": 2
      },
      "negativeRate": 17,
      "positiveRate": 42,
      "sentiment": 0.25,
      "latestNegative": {
        "runId": "3004800c-…",
        "engine": "Copilot",
        "createdAt": "2026-08-28T15:53:00.000Z",
        "evidence": "Plusieurs grandes chaînes comme Club Piscine reçoivent des critiques mitigées sur le suivi après l'installation."
      }
    }
  ],
  "meta": {
    "websiteId": "…",
    "company": "Club Piscine",
    "days": 30,
    "label": null,
    "minConfidence": 0.6,
    "prompts": 41,
    "read": 575,
    "counts": {
      "positive": 334,
      "neutral": 144,
      "mixed": 92,
      "negative": 5
    },
    "negativeRate": 1,
    "labels": {
      "positive": "recommended, or credited with a specific strength that sets it apart",
      "neutral": "named among others with no evaluative language",
      "mixed": "a specific strength and a specific limitation",
      "negative": "dismissed, warned about, described as inferior or limited, or a claim it makes is contradicted"
    }
  }
}
```

---

### GET /api/v1/websites/{websiteId}/prompts/leaderboard

Get leaderboard of companies by mentions for the website

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `lastDays` | number | no | Days to include (default 30, max 365) |

**Response**

```json
[
  {
    "rank": 1,
    "company": "MyCompany Inc.",
    "mentions": 42,
    "isCompetitor": true
  },
  {
    "rank": 2,
    "company": "Rival Co",
    "mentions": 36,
    "isCompetitor": true
  },
  {
    "rank": 3,
    "company": "Other Co",
    "mentions": 12,
    "isCompetitor": false
  }
]
```

---

### GET /api/v1/websites/{websiteId}/topics

Audiences and their topics for a website. Use these ids to scope /metrics/share-of-voice, and to resolve the topicId returned by /prompts. excludeFromSov marks informational topics where AI cites content without naming companies; they are dropped from every share-of-voice figure. Privilege: prompts:read

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Response**

```json
{
  "success": true,
  "data": {
    "audiences": [
      {
        "id": "audience-uuid",
        "name": "Founders",
        "description": "Early-stage SaaS founders",
        "topics": [
          {
            "id": "topic-uuid",
            "name": "Customer support",
            "description": null,
            "excludeFromSov": false
          }
        ]
      }
    ]
  }
}
```


## Action plan

### GET /api/v1/websites/{websiteId}/opportunities

Get content opportunities — queries where competitors are cited by AI but your site is not. Enriched with SEO data.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `value` | string | no | Filter by business value: critical, high, medium, low |
| `type` | string | no | Filter by type: comparison, feature, recommendation |
| `pageSize` | number | no | Number of results per page (default 5) |

**Response**

```json
{
  "success": true,
  "opportunities": [
    {
      "query": "best crm for small business",
      "frequency": 8,
      "businessValue": "high",
      "type": "recommendation",
      "models": [
        "openai",
        "perplexity"
      ],
      "searchVolume": 2400,
      "keywordDifficulty": 45,
      "relatedKeywords": [
        {
          "keyword": "crm software comparison",
          "searchVolume": 1200
        }
      ]
    }
  ]
}
```


## Content Studio (articles)

### GET /api/v1/websites/{websiteId}/articles

List articles in Content Studio for a website. Supports status and language filtering, pagination, and a public-blog view that returns published snapshots.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | no | Filter by status: draft, ready, published, archived |
| `view` | string | no | Set to "public" to return only blog-eligible articles using their published snapshots |
| `language` | string | no | Return only articles in this language, for a localised blog (e.g. "fr-CA" for /fr/blogs). A bare code matches any market of that language ("fr" finds "fr-CA"); a full tag matches exactly. Articles written before the language column have none, and are returned for the website's own language. Every article still carries the full "alternates" array whatever the filter, so hreflang stays complete. |
| `page` | number | no | Page number (default 1) |
| `pageSize` | number | no | Results per page (default 20, max 100) |

**Response**

```json
{
  "success": true,
  "data": {
    "articles": [
      {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "title": "Best AI visibility tools 2026 — full comparison",
        "slug": "best-ai-visibility-tools-2026-full-comparison",
        "metaDescription": "Side-by-side comparison of the top AI visibility platforms in 2026 — pricing, engines tracked, and which one fits your stack.",
        "status": "published",
        "qualityScore": 87,
        "opportunityId": "opp-abc123",
        "createdAt": "2026-05-12T14:08:00Z",
        "updatedAt": "2026-05-20T09:32:00Z",
        "publishedAt": "2026-05-15T10:00:00Z",
        "publishedUrl": "https://example.com/blog/best-ai-visibility-tools-2026-full-comparison"
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "total": 47,
      "totalPages": 3,
      "hasNextPage": true,
      "hasPreviousPage": false
    }
  }
}
```

---

### POST /api/v1/websites/{websiteId}/articles

Create or upsert an article. If an article with the same slug already exists for this website, it is updated in place (200) instead of duplicated (201). Auto-links to a matching current-month opportunity when opportunityId is omitted.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | yes | Article title |
| `slug` | string | no | URL slug (defaults to title slugified, max 80 chars) |
| `contentMarkdown` | string | no | Article body in Markdown |
| `metaDescription` | string | no | SEO meta description |
| `schemaMarkup` | string | no | JSON-LD schema markup to inject in <head> |
| `qualityScore` | number | no | AI-readiness quality score (0–100) |
| `status` | string | no | draft (default), ready, published, archived. Setting "published" snapshots the current fields to the public-blog view. |
| `opportunityId` | string | no | Link to a specific content opportunity. When omitted, the API auto-matches against the current month's opportunity batch. |
| `keywordTerm` | string | no | Target keyword (used by the auto-match logic) |
| `customGuidelines` | string | no | Editorial guidelines associated with the article |

**Response**

```json
{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "title": "Best AI visibility tools 2026 — full comparison",
    "slug": "best-ai-visibility-tools-2026-full-comparison",
    "status": "draft",
    "qualityScore": null,
    "createdAt": "2026-05-29T15:42:00Z"
  },
  "updated": false
}
```

---

### GET /api/v1/websites/{websiteId}/articles/{articleId}

Get full article details including the markdown body, schema markup, and generation metadata. ?view=public returns the published snapshot.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |
| `articleId` | string | yes | The unique identifier of the article |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `view` | string | no | Set to "public" to return the last published snapshot instead of the working draft |

**Response**

```json
{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "title": "Best AI visibility tools 2026 — full comparison",
    "slug": "best-ai-visibility-tools-2026-full-comparison",
    "metaDescription": "Side-by-side comparison of the top AI visibility platforms in 2026.",
    "content": null,
    "contentMarkdown": "# Best AI visibility tools 2026\n\nThe landscape of AI visibility platforms...",
    "brief": "Compare the top 8 AI visibility platforms by pricing, engines tracked, and integration depth.",
    "schemaMarkup": "{\"@context\":\"https://schema.org\",\"@type\":\"Article\",...}",
    "status": "published",
    "qualityScore": 87,
    "generationSteps": null,
    "opportunityId": "opp-abc123",
    "createdAt": "2026-05-12T14:08:00Z",
    "updatedAt": "2026-05-20T09:32:00Z",
    "publishedAt": "2026-05-15T10:00:00Z"
  }
}
```

---

### PATCH /api/v1/websites/{websiteId}/articles/{articleId}

Update an article. Any field can be omitted. Setting status="published" snapshots the current title/content/meta/schema into the public-blog view. Changes to content, title, or quality score also write an entry to the revision history.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |
| `articleId` | string | yes | The unique identifier of the article |

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | no | Updated title (also re-slugifies the URL) |
| `contentMarkdown` | string | no | Updated Markdown body |
| `metaDescription` | string | no | Updated meta description |
| `schemaMarkup` | string | no | Updated JSON-LD schema |
| `status` | string | no | draft, ready, published, archived |
| `qualityScore` | number | no | AI-readiness quality score (0–100) |
| `opportunityId` | string | no | Link to a content opportunity (pass null to unlink) |
| `publishedUrl` | string | no | External URL where the article was published. Setting this with no prior publishedAt flips status to "published" automatically. |
| `generationSteps` | object | no | Generation pipeline metadata |
| `generationThinking` | object | no | Generation thinking trace |
| `revisionNote` | string | no | Optional note attached to the revision history entry |

**Response**

```json
{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "title": "Best AI visibility tools 2026 — full comparison",
    "slug": "best-ai-visibility-tools-2026-full-comparison",
    "status": "published",
    "qualityScore": 92,
    "publishedUrl": "https://example.com/blog/best-ai-visibility-tools-2026",
    "updatedAt": "2026-05-29T16:01:00Z"
  }
}
```

---

### GET /api/v1/websites/{websiteId}/articles/by-slug/{slug}

Fetch a single article by its slug. Mirrors the by-id endpoint shape — used by the public blog runtime to look up articles without exposing internal IDs.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |
| `slug` | string | yes | The article slug |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `view` | string | no | Set to "public" to return the published snapshot instead of the working draft |

**Response**

```json
{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "title": "Best AI visibility tools 2026 — full comparison",
    "slug": "best-ai-visibility-tools-2026-full-comparison",
    "metaDescription": "Side-by-side comparison of the top AI visibility platforms in 2026.",
    "content": null,
    "contentMarkdown": "# Best AI visibility tools 2026\n\nThe landscape of AI visibility platforms...",
    "brief": "Compare the top 8 AI visibility platforms by pricing, engines tracked, and integration depth.",
    "schemaMarkup": "{\"@context\":\"https://schema.org\",\"@type\":\"Article\",...}",
    "status": "published",
    "qualityScore": 87,
    "generationSteps": null,
    "opportunityId": "opp-abc123",
    "createdAt": "2026-05-12T14:08:00Z",
    "updatedAt": "2026-05-20T09:32:00Z",
    "publishedAt": "2026-05-15T10:00:00Z"
  }
}
```

---

### GET /api/v1/websites/{websiteId}/articles/{articleId}/comments

List editorial comments attached to an article. Comments anchor to a specific text range (selectedText + startOffset/endOffset) so editors can resolve them inline.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |
| `articleId` | string | yes | The unique identifier of the article |

**Response**

```json
{
  "success": true,
  "data": {
    "comments": [
      {
        "id": "c1d2e3f4-1111-2222-3333-aabbccddeeff",
        "articleId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "userId": "user-xyz",
        "userName": "Marc-Olivier",
        "selectedText": "AI visibility platforms typically charge per query",
        "startOffset": 1240,
        "endOffset": 1290,
        "comment": "Rephrase — pricing model is a turn-off in the lede.",
        "resolved": false,
        "createdAt": "2026-05-22T11:14:00Z"
      }
    ]
  }
}
```

---

### POST /api/v1/websites/{websiteId}/articles/{articleId}/comments

Create an editorial comment on a text range. Used by external review tools to flag changes for the editor to apply.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |
| `articleId` | string | yes | The unique identifier of the article |

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `selectedText` | string | yes | The text the comment is anchored to |
| `startOffset` | number | yes | Character offset where the highlighted text starts in contentMarkdown |
| `endOffset` | number | yes | Character offset where the highlighted text ends |
| `comment` | string | yes | The comment body |

**Response**

```json
{
  "success": true,
  "data": {
    "id": "c1d2e3f4-1111-2222-3333-aabbccddeeff",
    "articleId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "userId": "org-owner-user-id",
    "selectedText": "AI visibility platforms typically charge per query",
    "startOffset": 1240,
    "endOffset": 1290,
    "comment": "Rephrase — pricing model is a turn-off in the lede.",
    "resolved": false,
    "createdAt": "2026-05-29T16:22:00Z"
  }
}
```

---

### PATCH /api/v1/websites/{websiteId}/articles/{articleId}/comments

Resolve or un-resolve an existing comment. Pass the comment id in the body alongside the new resolved state.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |
| `articleId` | string | yes | The unique identifier of the article |

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `commentId` | string | yes | The unique identifier of the comment to update |
| `resolved` | boolean | yes | New resolved state (true = resolved, false = re-opened) |

**Response**

```json
{
  "success": true
}
```


## Websites & data

### GET /api/v1/websites/{websiteId}/llm-queries

List LLM queries for a specific website with pagination and search

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | number | no | Page number (default 1) |
| `pageSize` | number | no | Results per page (default 20, max 100) |
| `search` | string | no | Search within query text (case-insensitive) |
| `sortBy` | string | no | Sort by impressions or position (default impressions) |
| `sortDir` | string | no | Sort direction asc\|desc (default desc) |

**Response**

```json
{
  "success": true,
  "data": {
    "rows": [
      {
        "query": "xseek vs profound",
        "impressions": 540,
        "position": 5.6,
        "pages": 2,
        "websiteId": "website-uuid"
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "total": 54,
      "totalPages": 3
    }
  }
}
```

---

### GET /api/v1/websites/{websiteId}/llm-queries/by-page

Find queries that gave impressions for a specific page URL

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | Full page URL to match |
| `match` | string | no | Match mode: exact\|contains (default exact) |
| `page` | number | no | Page number (default 1) |
| `pageSize` | number | no | Results per page (default 20, max 100) |
| `sortBy` | string | no | Sort by impressions or position (default impressions) |
| `sortDir` | string | no | Sort direction asc\|desc (default desc) |

**Response**

```json
{
  "success": true,
  "data": {
    "rows": [
      {
        "query": "xseek pricing",
        "impressions": 120,
        "position": 4.1,
        "websiteId": "website-uuid"
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "total": 3,
      "totalPages": 1
    }
  }
}
```

---

### GET /api/v1/websites

List all websites for your organization

**Response**

```json
[
  {
    "id": "123",
    "name": "My Website",
    "url": "https://example.com",
    "companyName": "My Company Inc.",
    "competitors": [
      "competitor1.com",
      "competitor2.com"
    ],
    "robotsUrl": "https://example.com/robots.txt",
    "sitemapUrl": "https://example.com/sitemap.xml",
    "createdAt": "2024-01-01T00:00:00Z"
  }
]
```

---

### POST /api/v1/websites

Create a new website

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | The name of the website |
| `url` | string | yes | The URL of the website |
| `competitors` | array | no | Array of competitor domain names or URLs |
| `robotsUrl` | string | no | The robots.txt URL |
| `sitemapUrl` | string | no | The sitemap XML URL |

**Response**

```json
{
  "id": "123",
  "name": "My Website",
  "url": "https://example.com",
  "companyName": "My Company Inc.",
  "competitors": [
    "competitor1.com",
    "competitor2.com"
  ],
  "robotsUrl": "https://example.com/robots.txt",
  "sitemapUrl": "https://example.com/sitemap.xml",
  "createdAt": "2024-01-01T00:00:00Z"
}
```

---

### PUT /api/v1/websites/{websiteId}

Update an existing website

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | The name of the website |
| `url` | string | no | The URL of the website |
| `companyName` | string | no | The company name |
| `competitors` | array | no | Array of competitor domain names or URLs |
| `robotsTxtUrl` | string | no | The robots.txt URL |
| `sitemapUrl` | string | no | The sitemap XML URL |

**Response**

```json
{
  "id": "123",
  "name": "My Website",
  "url": "https://example.com",
  "companyName": "My Company Inc.",
  "competitors": [
    "competitor1.com",
    "competitor2.com"
  ],
  "robotsUrl": "https://example.com/robots.txt",
  "sitemapUrl": "https://example.com/sitemap.xml",
  "createdAt": "2024-01-01T00:00:00Z",
  "updatedAt": "2024-01-01T00:00:00Z"
}
```

---

### GET /api/v1/websites/{websiteId}/sources

Get sources cited by AI in prompt responses for a website with pagination, sorting, and filtering

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | number | no | Page number (default 1) |
| `pageSize` | number | no | Results per page (default 20, max 100) |
| `sortBy` | string | no | Sort field: citationCount, lastSeen, createdAt, domain, url (default citationCount) |
| `sortOrder` | string | no | Sort order: asc or desc (default desc) |
| `search` | string | no | Filter results by substring match in URL, domain, or title (case-insensitive) |

**Response**

```json
{
  "success": true,
  "data": {
    "sources": [
      {
        "id": "source-1",
        "url": "https://example.com/article",
        "domain": "example.com",
        "title": "Article Title",
        "citationCount": 15,
        "lastSeen": "2024-01-02T00:00:00Z",
        "createdAt": "2024-01-01T10:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "total": 45,
      "totalPages": 3,
      "hasNextPage": true,
      "hasPreviousPage": false
    }
  }
}
```

---

### GET /api/v1/websites/{websiteId}/sentiment/runs

The answers behind a sentiment label, newest first, each with the sentence that decided the label and the sources the engine cited in that answer. The drill-down from /sentiment/prompts: from a prompt that reads negative, list its negative answers, then the pages they leaned on. The full answer text is returned only with includeAnswer=true.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no | positive \| neutral \| mixed \| negative; omit for every read answer |
| `promptId` | string | no | Only answers to this prompt |
| `company` | string | no | A tracked competitor name or alias; default is the site's own brand |
| `days` | number | no | Window in days (default 30, max 365) |
| `page` | number | no | Page number (default 1) |
| `pageSize` | number | no | Results per page (default 20, max 100) |
| `includeAnswer` | boolean | no | Return the full answer text on each run (default false) |

**Response**

```json
{
  "success": true,
  "data": [
    {
      "runId": "3004800c-…",
      "promptId": "a1b2c3d4-…",
      "query": "Quels détaillants de piscines offrent le meilleur rapport qualité-prix en 2026?",
      "engine": "Copilot",
      "createdAt": "2026-08-28T15:53:00.000Z",
      "company": "Club Piscine",
      "label": "negative",
      "sentiment": -1,
      "confidence": 1,
      "evidence": "Plusieurs grandes chaînes comme Club Piscine reçoivent des critiques mitigées sur le suivi après l'installation.",
      "sources": [
        {
          "id": "s1",
          "url": "https://www.trevi.com/…",
          "domain": "trevi.com",
          "title": "…",
          "citationOrder": 1
        }
      ]
    }
  ],
  "meta": {
    "websiteId": "…",
    "company": "Club Piscine",
    "days": 30,
    "label": "negative",
    "promptId": null,
    "minConfidence": 0.6,
    "count": 2,
    "page": 1,
    "pageSize": 20,
    "total": 2,
    "totalPages": 1
  }
}
```

---

### GET /api/v1/websites/{websiteId}/ai-visits

Get aggregated AI bot visit traffic by URL with search, pagination, and filters

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | number | no | Page number (default 1, max 1000000) |
| `pageSize` | number | no | Results per page (default 10, max 100) |
| `botName` | string | no | Filter by specific bot name (e.g., GPTBot, ClaudeBot) |
| `searchQuery` | string | no | Search within visited URLs (case-insensitive, at most 1000 characters once URL-encoded) |
| `startDate` | string | no | Start of the window, an ISO 8601 date-time with Z or an offset (e.g., 2024-01-01T00:00:00Z). A date alone (2024-01-01) is rejected |
| `endDate` | string | no | End of the window, inclusive, same format as startDate (e.g., 2024-01-31T23:59:59Z). Must be on or after startDate |

**Response**

```json
{
  "success": true,
  "data": [
    {
      "url": "https://example.com/page",
      "visitCount": 42,
      "lastVisitDate": "2024-01-02T01:00:00.000Z",
      "botNames": [
        "GPTBot",
        "ClaudeBot"
      ]
    }
  ],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "totalCount": 150,
    "totalPages": 8
  }
}
```

---

### POST /api/v1/websites/{websiteId}/ai-visits

Publish a new AI bot visit for a specific website

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `botName` | string | yes | Name of the bot (e.g., GPTBot) |
| `userAgent` | string | yes | Full User-Agent string of the bot |
| `url` | string | yes | Visited URL |
| `ip` | string | no | Visitor IP address if available |
| `referer` | string | no | HTTP referer if available |

**Response**

```json
{
  "id": "visit-uuid",
  "organizationId": "org-uuid",
  "botName": "GPTBot",
  "userAgent": "Mozilla/5.0 (... GPTBot ...)",
  "url": "https://example.com/page",
  "ip": "203.0.113.10",
  "referer": "https://ref.example",
  "createdAt": "2024-01-02T01:00:00Z",
  "websiteId": "website-uuid"
}
```

---

### GET /api/v1/websites/{websiteId}/keywords

Search keywords with volume, difficulty, and related keywords from DataForSEO. Uses the website language and location settings by default.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | yes | Search query (comma-separated for multiple keywords, max 10) |
| `language` | string | no | Language code (defaults to website language, e.g., en, fr, es) |
| `location` | number | no | DataForSEO location code (defaults to website location, e.g., 2840 for US, 2124 for Canada) |

**Response**

```json
{
  "results": [
    {
      "query": "ai search optimization",
      "matchedKeyword": "ai search optimization",
      "searchVolume": 880,
      "keywordDifficulty": 39,
      "cpc": 14.5,
      "competition": 0.25,
      "relatedKeywords": [
        {
          "keyword": "answer engine optimization",
          "searchVolume": 1900,
          "keywordDifficulty": 30
        },
        {
          "keyword": "ai visibility tools",
          "searchVolume": 1000,
          "keywordDifficulty": 3
        }
      ]
    }
  ]
}
```

---

### GET /api/v1/websites/{websiteId}/keywords

Get keyword search history for the website (no query parameter returns history)

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Response**

```json
{
  "history": [
    {
      "id": "search-uuid",
      "query": "ai search optimization",
      "language": "en",
      "location": 2840,
      "matchedKeyword": "ai search optimization",
      "searchVolume": 880,
      "keywordDifficulty": 39,
      "createdAt": "2026-03-30T12:00:00Z"
    }
  ]
}
```


## Technical

### GET /api/v1/robots

Get known AI bot user agents and IP ranges for detection (Public - No authentication required)

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `format` | string | no | Response format: full (default), compact, or patterns-only |
| `company` | string | no | Filter by company name (e.g., openai, anthropic, google) |

**Response**

```json
{
  "success": true,
  "data": {
    "companies": {
      "openai": {
        "userAgents": [
          {
            "name": "GPTBot",
            "userAgent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.0; +https://openai.com/gptbot)",
            "pattern": "GPTBot",
            "patternFlags": "i",
            "description": "OpenAI GPTBot crawler"
          }
        ],
        "ips": []
      },
      "anthropic": {
        "userAgents": [
          {
            "name": "anthropic-ai",
            "userAgent": "Mozilla/5.0 (compatible; anthropic-ai/1.0; +http://www.anthropic.com/bot.html)",
            "pattern": "anthropic-ai",
            "patternFlags": "i",
            "description": "Anthropic AI crawler for Claude"
          }
        ],
        "ips": []
      }
    },
    "totalCompanies": 15,
    "totalAgents": 42,
    "lastUpdated": "2024-01-02T12:00:00.000Z"
  }
}
```

---

### GET /api/v1/tools/robots-txt

Scan robots.txt for a given URL and return allowed/disallowed AI bots

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | Target site URL (e.g., https://example.com) |

**Response**

```json
{
  "success": true,
  "data": {
    "id": "scan-id",
    "url": "https://example.com",
    "scanDate": "2024-01-02T12:00:00Z",
    "robotsTxtContent": "User-agent: *\nDisallow: /admin",
    "allowedBots": [
      "gptbot",
      "perplexitybot"
    ],
    "disallowedBots": [
      "claudebot"
    ],
    "status": "success",
    "errorMessage": null,
    "ipAddress": "203.0.113.10",
    "userAgent": "Mozilla/5.0"
  }
}
```

---

### GET /api/v1/websites/{websiteId}/robots-scans

List robots.txt scans for a website (most recent first)

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Response**

```json
[
  {
    "id": "scan-uuid",
    "websiteId": "website-uuid",
    "organizationId": "org-uuid",
    "scanDate": "2024-01-02T12:00:00Z",
    "robotsTxtContent": "User-agent: *\nDisallow: /admin",
    "allowedBots": [
      "gptbot"
    ],
    "disallowedBots": [
      "badbot"
    ],
    "status": "success",
    "errorMessage": null
  }
]
```

---

### GET /api/v1/websites/{websiteId}/robots-scans/latest

Get the latest robots.txt scan for a website

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | The unique identifier of the website |

**Response**

```json
{
  "id": "scan-uuid",
  "websiteId": "website-uuid",
  "organizationId": "org-uuid",
  "scanDate": "2024-01-02T12:00:00Z",
  "robotsTxtContent": "User-agent: *\nDisallow: /admin",
  "allowedBots": [
    "gptbot"
  ],
  "disallowedBots": [
    "badbot"
  ],
  "status": "success",
  "errorMessage": null
}
```


## API keys

### GET /api/v1/api-keys

List API keys for your organization

**Response**

```json
[
  {
    "id": "key-uuid",
    "name": "Server Key",
    "createdAt": "2024-01-01T00:00:00Z",
    "lastUsedAt": "2024-01-02T12:34:56Z",
    "enabled": true
  }
]
```

---

### POST /api/v1/api-keys

Create a new API key for your organization

**Request body** (`object`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | A display name for the API key |
| `note` | string | no | Optional note for the key |

**Response**

```json
{
  "id": "key-uuid",
  "name": "Server Key",
  "key": "tb_sk_************************",
  "createdAt": "2024-01-01T00:00:00Z"
}
```


## Other

### GET /api/v1/tools/llms-txt

Generate LLMs.txt content for a given URL

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | Target site URL (e.g., https://example.com) |

**Response**

```json
{
  "success": true,
  "data": "Website description and rules for LLMs"
}
```


## Building an agent against this API

A minimal loop that answers "how visible are we, and what should we publish
next". Every call below is documented above.

```http
# 1. Which websites can this key see?
GET  /api/v1/websites

# 2. Visibility over the last 30 days
GET  /api/v1/websites/{websiteId}/metrics/share-of-voice?days=30
GET  /api/v1/websites/{websiteId}/metrics/ai-clicks?days=30

# 3. Where the visibility comes from
GET  /api/v1/websites/{websiteId}/prompts
GET  /api/v1/websites/{websiteId}/sources
GET  /api/v1/websites/{websiteId}/metrics/top-pages?days=30

# 4. What to do about it
GET  /api/v1/websites/{websiteId}/opportunities

# 5. Write the answer back
POST /api/v1/websites/{websiteId}/articles
```

### Worked example

```javascript
const BASE = 'https://www.xseek.io';
const headers = { Authorization: `Bearer ${process.env.XSEEK_API_KEY}` };

async function get(path) {
  const res = await fetch(`${BASE}${path}`, { headers });
  if (!res.ok) throw new Error(`${res.status} on ${path}: ${await res.text()}`);
  return res.json();
}

const websiteId = '<uuid>';

// Visibility. shareOfVoice is 0..1, or null when nothing was measured.
const sov = await get(`/api/v1/websites/${websiteId}/metrics/share-of-voice?days=30`);
if (sov.data.shareOfVoice === null) {
  console.log('No measurements in this window.');
} else {
  console.log(`Share of voice: ${(sov.data.shareOfVoice * 100).toFixed(1)}%`);
}

// Real visitors from AI assistants. Check availability before reading counts.
const clicks = await get(`/api/v1/websites/${websiteId}/metrics/ai-clicks?days=30`);
if (!clicks.data.available) {
  console.log(`AI clicks unavailable: ${clicks.data.unavailableReason}`);
} else {
  for (const e of clicks.data.byEngine) {
    console.log(`${e.engine}: ${e.clicks} clicks`);
  }
}

// The action plan, already reasoned and ranked.
const plan = await get(`/api/v1/websites/${websiteId}/opportunities`);
for (const o of plan.data ?? []) console.log(o.title, '-', o.rationale);
```

### Notes for agents

- **Date ranges** on the `/metrics` endpoints default to the trailing 30 days
  and end **yesterday**, because the current day is still being written and would
  report a partial count. Pass `startDate` and `endDate` (both `YYYY-MM-DD`) to
  override, or `days` for a trailing window. `/ai-visits` is the exception: it
  takes full date-times (`2026-09-08T00:00:00Z`) and has no default range.
- **Time series** return `engines: [{ engine, clicks }]` per day rather than one
  key per engine name, so the shape stays typeable when a new engine appears.
  Pivot client-side if your chart wants columns.
- **Pagination**, where present, uses `page` and `pageSize` and returns a
  `pagination` object with `total` and `totalPages`.
- **Impressions on `/metrics/top-pages`** come from a fixed Search Console scan
  of the last 7, 30 or 90 days, so on a custom range they do not cover the same
  days as `aiClicks`. The response says which window was used in
  `impressionsWindowDays`.
- **Be polite.** These endpoints read from live analytics providers. Cache
  results for a daily sync rather than polling.

---

Generated from the xSeek v1 API definitions. HTML version: https://www.xseek.io/api-docs
