# Data Export API

Pull your CiteMetrix data - citations, ModelScore trends, competitors, hallucinations, and more - into Looker Studio, Tableau, or any BI tool via a read-only REST API.

A read-only REST interface for pulling your account’s AI-visibility data — citations, ModelScore™ trends, per-platform breakdowns, competitors, hallucinations, and annotations — into Looker Studio, Tableau, a data warehouse, or any tool that speaks HTTP. Every response is available as JSON or CSV.

## Before you begin

- **Plan requirement.** API access is included on the **Professional** and **Enterprise** plans (Business) and the **Agency Large** and **Agency Global** plans (Agency). If the _Data Export API_ screen doesn’t appear in your dashboard, your current plan doesn’t include it.
- **Read-only.** The API never changes your data. Every key is scoped to a single CiteMetrix account and can see only that account’s domains.
- **You’ll need** an API key (created in the dashboard) and any HTTP client — Looker Studio, Tableau, Python, `curl`, or a browser.

## Create an API key

1. Sign in to your CiteMetrix dashboard.
2. Open the account menu (top right) and choose **Data Export API**.
3. Enter a label describing where the key will be used (e.g. “Looker Studio”) and click **Create key**.
4. Copy the key immediately — for security it is shown **only once**. It looks like `cmx_live_…`. Store it in your secrets manager.
5. To retire a key, click **Revoke**. Revocation takes effect immediately; any integration using that key stops working.

**One key per integration.** Create a separate key for each tool and label it clearly, so you can revoke one integration without disrupting the others.

## Authentication

Send your key as a Bearer token in the `Authorization` header on every request:

```
Authorization: Bearer cmx_live_your_key_here
```

A missing, malformed, or revoked token returns `401 Unauthorized`. A valid key on a plan without API access returns `403 Forbidden`.

## Base URL

```
https://citemetrix.com/wp-json/citemetrix/v1/export/
```

All endpoints below are relative to this base. Every request is `GET` over HTTPS.

## Common request parameters

Every data endpoint accepts the same pagination, filtering, and format controls.

| Parameter | Type | Description |
| --- | --- | --- |
| `page` | integer | 1-based page number. Default `1`. |
| `per_page` | integer | Rows per page. Default `100`, maximum `1000`. |
| `domain` | integer | Restrict to one of your domain IDs (see `/export/whoami`). A domain not in your account returns `403 domain_out_of_scope`. Omit to include all your domains. |
| `from`, `to` | date | `YYYY-MM-DD` range on each endpoint’s primary date column. `to` is inclusive to end-of-day. |
| `format` | string | `json` (default) or `csv`. |

## Response format

**JSON** responses use a consistent envelope:

```
{
  "data": [ { …row objects… } ],
  "meta": { "page": 1, "per_page": 100, "total": 9273, "total_pages": 93 }
}
```

These headers accompany every list response, so a connector can paginate without parsing the body:

| Header | Meaning |
| --- | --- |
| `X-Total-Count` | Total matching rows across all pages. |
| `X-Page` | Current page. |
| `X-Per-Page` | Rows per page. |

**CSV** (`?format=csv`) returns the current page as a downloadable `text/csv` file with a header row; `X-Total-Count` is still sent so you can loop through pages. JSON object columns (in `/export/scores`) are emitted as embedded JSON strings in CSV.

## Rate limits

Each key may make up to **120 requests per minute**. Exceeding this returns `429 Too Many Requests` — wait a few seconds and retry. For large extracts, page through with `per_page=1000` rather than issuing many small requests.

## Endpoints

GET`/export/whoami`

Returns the account the key belongs to and the domain IDs it can access. Call this first to discover your domain IDs for the `domain` filter.

```
curl -H "Authorization: Bearer cmx_live_…" \
  https://citemetrix.com/wp-json/citemetrix/v1/export/whoami

{
  "api_version": "v1",
  "account": { "user_id": 42, "email": "you@example.com", "tier": "agency" },
  "domains": { "count": 3, "ids": [15, 22, 27] }
}
```

GET`/export/citations`

The citation fact table: one row per (query × AI platform) result. Date filter applies to `checked_at`. Extra filters: `platform`, `is_cited` (`0`/`1`).

**Columns:** id, domain_id, keyword_id, platform, query_text, is_cited, is_recommended, is_compared, is_only_option, citation_url, citation_type, has_link, position_in_response, total_sources, mention_count, sentiment, sentiment_score, intent_category, business_driver, brand_position, total_brands_mentioned, checked_at.

```
GET /export/citations?domain=15&platform=chatgpt&is_cited=1&from=2026-05-01&to=2026-05-31&per_page=1000
```

GET`/export/scores`

Daily ModelScore™ and sub-scores per domain. Date filter applies to `score_date`.

**Columns:** id, domain_id, score_date, model_score, citation_score, brand_demand_score, authority_score, technical_score, total_citations, total_queries, citation_rate, plus JSON objects platform_breakdown, sentiment_breakdown, gsc_data, ga4_data, and created_at. The four breakdown fields are nested objects in JSON, JSON strings in CSV.

GET`/export/platforms`

Per-platform breakdown, aggregated across your citations by domain and platform. Date filter applies to `checked_at`. Extra filter: `platform`.

**Columns:** domain_id, platform, total_queries, total_citations, citation_rate (percent), recommended, avg_sentiment_score.

GET`/export/competitors`

Competitors you track per domain. Extra filter: `status` (`active`, `paused`, `deleted`).

**Columns:** id, domain_id, competitor_domain, competitor_name, status, created_at, updated_at.

GET`/export/hallucinations`

Detected AI hallucinations about your brand. Date filter applies to `detected_at`. Extra filters: `platform`, `severity` (`minor`, `moderate`, `critical`), `status`, `hallucination_type`.

**Columns:** id, domain_id, citation_id, fact_id, platform, severity, hallucination_type, status, claim_text, fact_text, ai_explanation, query_text, remediation_actions, cluster_id, cluster_label, reviewed_at, detected_at, created_at.

GET`/export/annotations`

Timeline annotations your team has added. Date filter applies to `annotation_date`. Extra filter: `annotation_type` (`content_update`, `competitor`, `algorithm`, `campaign`, `other`).

**Columns:** id, domain_id, user_id, annotation_date, note, annotation_type, created_at.

## Platform slugs

The `platform` field and filter use these lowercase slugs:

```
chatgpt   gemini   perplexity   claude   copilot   grok   google_aio   mistral   deepseek
```

## Error responses

| HTTP | code | Meaning |
| --- | --- | --- |
| 401 | `missing_token` / `bad_token` / `invalid_token` | No, malformed, or revoked key. |
| 403 | `api_not_entitled` | Valid key, but the plan doesn’t include API access. |
| 403 | `domain_out_of_scope` | The requested `domain` is not in your account. |
| 429 | `rate_limited` | More than 120 requests in a minute.

Errors use the standard WordPress REST shape: `{ "code": "…", "message": "…", "data": { "status": 403 } }`.

## Connect your BI tool

### Looker Studio

Use a JSON/REST community connector. Set the request URL to an endpoint such as `…/export/citations?per_page=1000`, add the request header `Authorization: Bearer cmx_live_…`, and point the connector at the `data` array. Paginate by incrementing `page` until `page > total_pages`.

### Tableau (Web Data Connector)

Point a Web Data Connector at the endpoint, add the `Authorization: Bearer` header, and map the JSON `data` array to your table schema using the column lists above. Drive pagination from `X-Total-Count` / `meta.total_pages` in the connector’s `getData` loop.

### Google Sheets (Apps Script)

Because `IMPORTDATA` can’t send headers, use Apps Script’s `UrlFetchApp`:

```
function loadCitations() {
  var res = UrlFetchApp.fetch(
    'https://citemetrix.com/wp-json/citemetrix/v1/export/citations?format=csv&per_page=1000',
    { headers: { Authorization: 'Bearer cmx_live_…' } });
  var rows = Utilities.parseCsv(res.getContentText());
  SpreadsheetApp.getActiveSheet()
    .getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}
```

### Python

```
import requests

BASE = "https://citemetrix.com/wp-json/citemetrix/v1/export"
HEADERS = {"Authorization": "Bearer cmx_live_…"}

def fetch_all(endpoint, **params):
    params.setdefault("per_page", 1000)
    page, rows = 1, []
    while True:
        params["page"] = page
        r = requests.get(f"{BASE}/{endpoint}", headers=HEADERS,
                         params=params, timeout=60)
        r.raise_for_status()
        body = r.json()
        rows += body["data"]
        if page >= body["meta"]["total_pages"]:
            break
        page += 1
    return rows

citations = fetch_all("citations", domain=15, is_cited=1)
print(len(citations), "rows")
```

## Good practices

- One key per integration; label them clearly; revoke unused keys.
- Page with `per_page=1000` and stop when `page >= total_pages`.
- For incremental syncs, store the last `checked_at` / `score_date` you pulled and pass it as `from` next run.
- Treat keys like passwords: store them in a secrets manager, never commit them to source control.
