---
title: "SearchMention API Reference"
description: "One account token lets scripts and AI agents read every store: details, catalog, prompts, run status and the full AI visibility report. Starting runs shows the cost first."
url: https://searchmention.com/guides/api
updated: 2026-09-27
---

API

# SearchMention API Reference

Updated September 27, 2026

On this page

- [Get a token](#token)
- [Authentication](#auth)
- [Errors and limits](#errors)
- [Pagination](#pagination)
- [Endpoints](#endpoints)
- [Typical flow](#flow)
- [Store keys](#store-keys)

The SearchMention API lets your scripts and AI agents, such as Claude, read every store on your account: store details, what each store sells, the prompts you track, run progress and the full AI visibility report. One token covers every store you own. On an agency account, that is every client store.

## At a glance

Base URL `https://searchmention.com/api/v1` Auth `Authorization: Bearer sm_acct_…` Format JSON. Send `Accept: application/json` on every request. Access Read only by default. Starting prompt runs is opt-in per token.

## Get a token

In SearchMention, open [Account settings → API](https://searchmention.com/settings#api), give the token a name (for example “Claude” or “Weekly report”) and click **Create token**.

- Tick **Can edit prompts** if this token should add, edit, pause or delete prompts, add, edit or remove tracked competitors, and turn on report sharing, and **Can start prompt runs** only if it should be able to spend your query budget. With neither, the token is read only: it can’t change anything or start runs.
- The token is shown **once**. Copy it straight into your secret store; SearchMention can’t show it again.
- Revoke a token from the same page at any time. Anything using it stops working immediately.
- An account can hold up to **10 tokens**. Use one per script or agent so you can revoke one without breaking the others.

**Treat the token like a password.** It reads every store on your account. Keep it on a server or in your agent’s secret settings, never in browser code or a public repository.

## Authentication

Send the token as a bearer token on every request. Stores are addressed by their `uuid`, which [List stores](#list-stores) returns.

The examples on this page read two shell variables, so you can paste them as they are:

shell

```
export SEARCHMENTION_TOKEN="sm_acct_your_token_here"
export STORE="9b1f6c2e-4d7a-4e0b-a5f3-2c8d1e6b7a90"

curl "https://searchmention.com/api/v1/projects" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

## Errors and limits

Errors come back as JSON with an `error` message:

| Status | Example body | When |
|---|---|---|
| `401` | `{"error": "Invalid API token."}` | The token is missing, wrong or revoked, or it is a store key (`sm_live_…`) instead of an account token. |
| `403` | `{"error": "This token is missing the \"run\" scope."}` | The token wasn’t allowed to start runs or refresh a summary. A confirmed run blocked by your plan’s query budget is also `403` (see [Start a run](#start-run)). |
| `404` | `{"error": "Store not found."}` | The uuid isn’t one of your stores, or the store is archived. Unknown run ids return `Run not found.` |
| `409` | `{"error": "A run is already in progress.", …}` | A run is already going for that store, you asked for the report of a run that hasn’t finished, or a [summary refresh](#refresh-summary) can’t start yet. |
| `422` | `{"message": "…", "errors": {"per_page": ["…"]}}` | A query parameter is invalid, or a confirmed run can’t start because the store has no active prompts or engines. |

**Rate limits.** 120 requests a minute, 30 a minute for starting runs, and 10 a minute for summary refreshes. Over the limit you get `429` with a `Retry-After` header (seconds to wait).

## Pagination

List endpoints return rows in `data`, page numbers in `meta` and ready-made page URLs in `links`. Pass `page` and `per_page`, or follow `links.next` until it is `null`.

Pagination shape

```
{
  "data": [ … ],
  "meta": { "current_page": 1, "per_page": 50, "last_page": 3, "total": 128 },
  "links": {
    "first": "https://searchmention.com/api/v1/projects?page=1",
    "last": "https://searchmention.com/api/v1/projects?page=3",
    "previous": null,
    "next": "https://searchmention.com/api/v1/projects?page=2"
  }
}
```

Some lists add their own top-level keys, such as `source` on the catalog and `totals` on prompts.

## Endpoints

All paths are relative to `https://searchmention.com/api/v1`.

| Method | Path | What it returns |
|---|---|---|
| GET | [`/projects`](#list-stores) | Every store on the account |
| GET | [`/projects/{uuid}`](#get-store) | One store’s brand, market, competitors and prompt setup |
| GET | [`/projects/{uuid}/catalog/categories`](#catalog) | What the store sells, by category |
| GET | [`/projects/{uuid}/catalog/products`](#catalog) | What the store sells, by product |
| GET | [`/projects/{uuid}/prompts`](#prompts) | The full prompt library |
| POST | [`/projects/{uuid}/prompts`](#edit-prompts) | Add a prompt |
| PATCH | [`/projects/{uuid}/prompts/{id}`](#edit-prompts) | Edit, pause or resume a prompt |
| DELETE | [`/projects/{uuid}/prompts/{id}`](#edit-prompts) | Delete a prompt that never ran |
| POST | [`/projects/{uuid}/competitors`](#edit-competitors) | Track a competitor |
| PATCH | [`/projects/{uuid}/competitors/{domain}`](#edit-competitors) | Rename a competitor or change its aliases |
| DELETE | [`/projects/{uuid}/competitors/{domain}`](#edit-competitors) | Stop tracking a competitor |
| POST | [`/projects/{uuid}/runs`](#start-run) | A run’s cost, or a started run |
| GET | [`/projects/{uuid}/runs/{id}`](#run-status) | A run’s progress |
| POST | [`/projects/{uuid}/runs/{id}/summary`](#refresh-summary) | Rewrite a finished run’s summary |
| POST | [`/projects/{uuid}/next-steps`](#next-steps) | Suggest next steps from your last runs |
| GET | [`/projects/{uuid}/report`](#report) | The full AI visibility report as data |
| POST | [`/projects/{uuid}/report/share`](#share-report) | Turn on the public report link and get it |

### List stores

GET /projects

Every store on your account, newest first. Archived stores are left out.

| Query | What it does |
|---|---|
| `q` | Only stores whose name or domain contains this text |
| `page`, `per_page` | Default 50 per page, max 100 |

Each row has the store’s `uuid`, `name`, `domain`, `plan` and `status`, prompt counts, the `latest_run` (in any state, or `null`), the shared `report_url` (`null` when sharing is off) and `report_views`.

Request

```
curl "https://searchmention.com/api/v1/projects?q=trail" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

Response (trimmed)

```
{
  "data": [
    {
      "uuid": "9b1f6c2e-4d7a-4e0b-a5f3-2c8d1e6b7a90",
      "name": "Trail Co.",
      "domain": "trailco.example",
      "site_type": "online_store",
      "plan": "growth",
      "status": "active",
      "created_at": "2026-08-03T14:12:09+00:00",
      "prompts": { "total": 48, "active": 42 },
      "latest_run": {
        "id": 4821,
        "status": "completed",
        "prompts_executed": 168,
        "total_queries": 168,
        "started_at": "2026-09-22T09:00:04+00:00",
        "completed_at": "2026-09-22T09:14:51+00:00"
      },
      "report_url": "https://searchmention.com/reports/k3Jd9…",
      "report_views": {
        "count": 6,
        "first_viewed_at": "2026-09-22T10:02:11+00:00",
        "last_viewed_at": "2026-09-25T16:40:37+00:00"
      }
    }
  ],
  "meta": { "current_page": 1, "per_page": 50, "last_page": 1, "total": 1 },
  "links": { … }
}
```

### Get a store

GET /projects/{uuid}

Everything behind one store’s prompts, in `data`:

| Key | What it holds |
|---|---|
| `brand` | The name AI answers are checked for, its `aliases` (other `names` and `domains`) and `business_model`: `brand`, `retailer` or `mixed` |
| `market` | `country`, `language`, `currency`, and the `search_country` / `search_location` AI answers are localized to |
| `wizard` | The prompt setup you saved: brief, chosen categories and products, competitors, goals and notes. `null` until you’ve set up prompts |
| `detected_brief` | What SearchMention read from the storefront: summary, product lines, store categories, featured products, where it ships |
| `competitors` | Tracked competitors with `domain`, `name`, `country`, `reason` and `aliases` |
| `catalog` | Whether a product catalog is synced, product and category counts, and imported pages by type |
| `readiness` | The latest AI readiness score and pages scanned, or `null` |
| `prompts` | `total`, `active`, your plan’s `cap` and `slots_remaining` |
| `runs` | The `latest` run, whether one is `running`, the engines a run uses (`active_models`), `remaining_query_units` (`null` = unlimited) and `block_reason` if a run can’t start now |
| `report_url` | The shared report link, or `null` |

Store keys and platform credentials are never returned.

Request

```
curl "https://searchmention.com/api/v1/projects/$STORE" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

Response (trimmed)

```
{
  "data": {
    "uuid": "9b1f6c2e-4d7a-4e0b-a5f3-2c8d1e6b7a90",
    "name": "Trail Co.",
    "domain": "trailco.example",
    "platform": "shopify",
    "plan": "growth",
    "status": "active",
    "busy": false,
    "brand": {
      "name": "Trail Co.",
      "aliases": { "names": ["TrailCo"], "domains": [] },
      "business_model": "brand",
      "business_model_source": "brief"
    },
    "market": {
      "country": "US",
      "language": "en",
      "currency": "USD",
      "search_country": "US",
      "search_location": "United States"
    },
    "wizard": { "brief": { … }, "selected_categories": [ … ], "competitors": [ … ] },
    "detected_brief": { "status": "ready", "built_at": "2026-08-03T14:13:40+00:00", "data": { … } },
    "competitors": [
      {
        "domain": "ridgeline.example",
        "name": "Ridgeline Outfitters",
        "country": "US",
        "reason": "Sells ultralight packs at a similar price",
        "aliases": []
      }
    ],
    "catalog": { "synced": true, "product_count": 214, "category_count": 18, … },
    "readiness": { "overall_score": 72.5, "pages_scanned": 40, "completed_at": "2026-09-20T08:31:02+00:00" },
    "prompts": { "total": 48, "active": 42, "cap": 50, "slots_remaining": 8 },
    "runs": {
      "latest": { "id": 4821, "status": "completed", … },
      "running": false,
      "active_models": ["chatgpt", "ai_overview", "gemini", "perplexity"],
      "remaining_query_units": 3620,
      "block_reason": null
    },
    "report_url": "https://searchmention.com/reports/k3Jd9…"
  }
}
```

### Catalog: categories and products

GET /projects/{uuid}/catalog/categories

GET /projects/{uuid}/catalog/products

What the store sells, from the best source it has. The top-level `source` says which:

| `source` | Where the rows come from |
|---|---|
| `catalog` | Your synced product catalog |
| `pages` | Category or product pages found on the site’s sitemap |
| `detected` | Collections and products SearchMention read from the storefront. Often all a store added by URL has |

| Query | What it does |
|---|---|
| `q` | Category name, or product title, SKU or handle (page title or URL for `pages`) |
| `include_inactive` | Products only: `1` includes draft and archived catalog products |
| `page`, `per_page` | Default 100 per page, max 200 |

A category row has `name`, `url`, `parent`, `product_count` and `description`. A product row has `title`, `url`, `vendor`, `product_type`, `categories`, `tags`, `price`, `price_min`, `price_max`, `currency`, `status` and `description`. Fields a source doesn’t have are `null` or `[]`, and descriptions are plain text cut at 300 characters.

Request

```
curl "https://searchmention.com/api/v1/projects/$STORE/catalog/products?q=backpack&per_page=20" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

Response (trimmed)

```
{
  "data": [
    {
      "title": "Ridge 40L Ultralight Backpack",
      "url": "https://trailco.example/products/ridge-40l",
      "vendor": "Trail Co.",
      "product_type": "Backpacks",
      "categories": ["Backpacks", "Ultralight"],
      "tags": ["hiking", "ultralight"],
      "price": "189.00",
      "price_min": "189.00",
      "price_max": "209.00",
      "currency": "USD",
      "status": "active",
      "description": "A 40-litre frameless pack that weighs 780 g…"
    }
  ],
  "source": "catalog",
  "meta": { "current_page": 1, "per_page": 20, "last_page": 1, "total": 6 },
  "links": { … }
}
```

### List prompts

GET /projects/{uuid}/prompts

The whole prompt library, including paused prompts and prompts that have never run, so you can review a set before spending query budget on it.

| Query | What it does |
|---|---|
| `active` | `1` active only, `0` paused only. Leave out for both |
| `query_type` | `transactional`, `discovery`, `comparison`, `price`, `use_case`, `attribute` or `brand` |
| `q` | Only prompts containing this text |
| `page`, `per_page` | Default 100 per page, max 200 |

Each row has `id`, `prompt_text`, `category`, `query_type` and its label, buyer `intent` (`high`, `medium` or `low`), `is_active`, and `seeded` with `seeded_by`: the prompt names your brand or a tracked competitor, so a mention there isn’t organic. `latest_run` shows how many engines answered it and how many named you in the latest completed run, or `null` if it wasn’t in that run.

The response also carries `totals` for the whole library (not just the page), the `latest_run` it compares against, and `health`: the same checks the Prompts tab shows, such as catalog categories no prompt covers, prompts with an out-of-date year, and questions Google shows next to your prompts.

Request

```
curl "https://searchmention.com/api/v1/projects/$STORE/prompts?active=1" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

Response (trimmed)

```
{
  "data": [
    {
      "id": 118,
      "prompt_text": "best ultralight backpack for a weekend hike",
      "category": "Backpacks",
      "query_type": "discovery",
      "query_type_label": "Discovery",
      "intent": "low",
      "source": "manual",
      "is_active": true,
      "seeded": false,
      "seeded_by": [],
      "created_at": "2026-08-03T14:20:12+00:00",
      "latest_run": { "engines": 4, "mentioned": 2 }
    }
  ],
  "totals": { "total": 48, "active": 42, "seeded": 5, "cap": 50, "slots_remaining": 8 },
  "latest_run": { "id": 4821, "completed_at": "2026-09-22T09:14:51+00:00" },
  "health": { "issues": [ … ], "suggestions": [ … ] },
  "meta": { "current_page": 1, "per_page": 100, "last_page": 1, "total": 42 },
  "links": { … }
}
```

### Add, edit and delete prompts

POST /projects/{uuid}/prompts

PATCH /projects/{uuid}/prompts/{id}

DELETE /projects/{uuid}/prompts/{id}

Changes the prompt library with the same rules as the Prompts tab. It needs a token with **Can edit prompts**. Nothing changes while the store is busy (a run, import or prompt generation): you get `409`, so a run never asks a half-edited set.

| Call | Body | Result |
|---|---|---|
| `POST /prompts` | `prompt_text` (5–500 characters), `query_type` | `201`. If a paused prompt has the same text it’s resumed instead (`200`, `result: restored`). Same text as an active prompt: `409`. At your plan’s prompt limit: `403` |
| `PATCH /prompts/{id}` | Any of `prompt_text`, `query_type`, `is_active` | `200`. `is_active: false` pauses, `true` resumes (within the limit). Text another prompt uses: `422` |
| `DELETE /prompts/{id}` | — | `200`. Only for a prompt that never ran; one with results gets `409`, so pause it instead and it stays in past reports |

`query_type` is one of `discovery`, `use_case`, `attribute`, `transactional`, `price`, `comparison`, `brand`. Editing the text keeps the prompt’s id, so its earlier results stay with it; for a different question, pause the old prompt and add a new one.

Request

```
curl -X PATCH "https://searchmention.com/api/v1/projects/$STORE/prompts/118" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"prompt_text": "where to buy ultralight backpacks online", "query_type": "transactional"}'
```

Response (trimmed)

```
{
  "data": {
    "id": 118,
    "prompt_text": "where to buy ultralight backpacks online",
    "query_type": "transactional",
    "query_type_label": "Transactional",
    "intent": "high",
    "is_active": true,
    …
  },
  "result": "updated",
  "totals": { "active": 42, "cap": 50, "slots_remaining": 8 }
}
```

### Track, edit and remove competitors

POST /projects/{uuid}/competitors

PATCH /projects/{uuid}/competitors/{domain}

DELETE /projects/{uuid}/competitors/{domain}

Changes the store’s tracked competitors, the same way as the **Track** button on the Visibility page and the Prompts & Competitors screen. It needs a token with **Can edit prompts**. A competitor is addressed by its tracked domain, as `GET /projects/{uuid}` lists it. Like prompt edits, nothing changes while the store is busy (`409`).

| Call | Body | Result |
|---|---|---|
| `POST /competitors` | `domain`, optional `name` and `aliases` | `201`. Already tracked, or already counted as a tracked competitor (same brand on another domain): `409`. Your own domain: `422`. At your plan’s competitor limit: `403` |
| `PATCH /competitors/{domain}` | Any of `name`, `aliases` | `200`. `aliases` replaces the list; `[]` clears it |
| `DELETE /competitors/{domain}` | — | `200`. The competitor is no longer tracked |

`aliases` are other names AI uses for the competitor (up to 10). A domain in the list, like `velotric.com` for a competitor tracked as `velotricbike.com`, counts as another site of the same brand, so mentions of either count once. Changes apply to counts in your reports right away.

Request

```
curl -X PATCH "https://searchmention.com/api/v1/projects/$STORE/competitors/velotricbike.com" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name": "Velotric", "aliases": ["velotric.com"]}'
```

Response

```
{
  "data": {
    "domain": "velotricbike.com",
    "name": "Velotric",
    "country": null,
    "reason": null,
    "aliases": ["velotric.com"]
  },
  "result": "updated",
  "totals": { "tracked": 6, "cap": 10, "slots_remaining": 4 }
}
```

### Start a run

POST /projects/{uuid}/runs

Asks every active prompt on every active engine, like the dashboard’s **Run prompts** button. It needs a token with **Can start prompt runs**, and it takes two calls, so a script or agent never spends query budget by accident:

1. **Preview.** Without `confirm=true` nothing starts. You get `200` with the cost: `prompts` × `engines` = `query_units`, plus the budget left now and after the run.
2. **Confirm.** With `confirm=true` (JSON body or query string) the run starts and you get `202` with the new run and its `id`.

Runs draw on the same plan budget as the Run prompts button, so a token can never spend more than you could by clicking. `remaining_query_units` is `null` on plans without a limit.

1. Preview the cost

```
curl -X POST "https://searchmention.com/api/v1/projects/$STORE/runs" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

Response · 200

```
{
  "data": {
    "started": false,
    "prompts": 42,
    "engines": ["chatgpt", "ai_overview", "gemini", "perplexity"],
    "query_units": 168,
    "remaining_query_units": 3620,
    "remaining_after_run": 3452,
    "can_run": true,
    "block_reason": null,
    "message": null,
    "running_run_id": null
  }
}
```

2. Confirm and start

```
curl -X POST "https://searchmention.com/api/v1/projects/$STORE/runs" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"confirm": true}'
```

Response · 202 (trimmed)

```
{
  "data": {
    "started": true,
    "prompts": 42,
    "engines": ["chatgpt", "ai_overview", "gemini", "perplexity"],
    "query_units": 168,
    "remaining_query_units": 3620,
    "remaining_after_run": 3452,
    "can_run": true,
    "run": {
      "id": 4822,
      "kind": "batch",
      "status": "running",
      "started_at": "2026-09-27T10:05:00+00:00",
      "completed_at": null,
      "progress": { "queries_done": 0, "queries_total": 168, … },
      "report_ready": false
    }
  }
}
```

When a run can’t start, the preview says so with `can_run: false`, a `block_reason` and a readable `message`. Confirming anyway returns the error below with the same `data`:

| `block_reason` | Status | What to do |
|---|---|---|
| `no_active_prompts` | `422` | Turn on at least one prompt |
| `no_engines` | `422` | Enable at least one AI engine for the store |
| `subscription` | `403` | The free trial’s queries are used up. Subscribe to keep running prompts |
| `monthly_limit` | `403` | This month’s query budget is used up. It resets next month |
| `audit_recheck_wait` | `403` | The one-time AI audit ran. Its re-check opens 30 days after that run |
| `audit_used` | `403` | The one-time AI audit and its re-check are done. Subscribe to keep running prompts |
| `not_enough_query_units` | `403` | Some budget is left, but less than this run needs. Pause some prompts or wait for the reset |
| `run_in_progress` | `409` | Follow the run in `running_run_id` instead |

Response · 409

```
{
  "error": "A run is already in progress.",
  "data": {
    "started": false,
    "can_run": false,
    "block_reason": "run_in_progress",
    "message": "A run is already in progress.",
    "running_run_id": 4822,
    …
  }
}
```

### Get a run

GET /projects/{uuid}/runs/{id}

Progress of one run. Poll it every 30 seconds or so after starting a run.

- `status` is `running`, `completed` or `failed`. `kind` is `batch` for a full run; `prompt` and `recheck` are smaller runs started in the dashboard.
- `progress` counts `queries_done` of `queries_total` (one query is one prompt on one engine), `prompts_done` of `prompts_total`, answers so far (`results`) and how many named you (`mentioned`).
- `report_ready` turns `true` when the run has finished with answers, so [the report](#report) can be fetched.
- `analysis_ready` turns `true` a few seconds after that, once the written summary is in the report. `competitor_intel_status` tracks the competitor page checks that follow a run.
- `summary_refresh` follows a [summary refresh](#refresh-summary): `status` is `null` until you ask for one, then `queued`, `running`, `completed` or `failed`.

Request

```
curl "https://searchmention.com/api/v1/projects/$STORE/runs/4822" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

Response

```
{
  "data": {
    "id": 4822,
    "kind": "batch",
    "status": "completed",
    "started_at": "2026-09-27T10:05:00+00:00",
    "completed_at": "2026-09-27T10:19:42+00:00",
    "progress": {
      "queries_done": 168,
      "queries_total": 168,
      "prompts_done": 42,
      "prompts_total": 42,
      "results": 168,
      "mentioned": 61
    },
    "models_queried": 4,
    "engines": ["ai_overview", "chatgpt", "gemini", "perplexity"],
    "analysis_ready": true,
    "competitor_intel_status": "running",
    "summary_refresh": { "status": null, "requested_at": null, "finished_at": null },
    "report_ready": true
  }
}
```

### Refresh a run’s summary

POST /projects/{uuid}/runs/{id}/summary

Rewrites the summary of a finished run (headline, what’s working, what’s holding you back, what changed) from the answers it already has. Only a store’s first run gets next steps in its summary; after that, ask for them with [Suggest next steps](#next-steps). No AI engine is asked again, so it uses **no query units**. It needs a token with **Can start prompt runs**.

- Send `"recheck_prices": true` to fix price checks first: the prices AI quoted are read again from its answers (a range such as “$21–$23” is kept as a range instead of $21), and your pages AI linked are checked again against every price they sell at. Then the summary is rewritten. Default `false`: summary only.
- It runs in the background. You get `202` with the run, `summary_refresh.status` set to `queued`. Poll [Get a run](#run-status) until it is `completed` or `failed`, then fetch [the report](#report) with `run_id`.
- If a new summary can’t be written, the run keeps the one it had and the refresh shows `failed`. You can ask again.

Request

```
curl -X POST "https://searchmention.com/api/v1/projects/$STORE/runs/4822/summary" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"recheck_prices": true}'
```

Response · 202 (trimmed)

```
{
  "data": {
    "id": 4822,
    "kind": "batch",
    "status": "completed",
    "analysis_ready": true,
    "summary_refresh": {
      "status": "queued",
      "requested_at": "2026-09-27T11:02:10+00:00",
      "finished_at": null
    },
    "report_ready": true,
    …
  }
}
```

When a refresh can’t start you get `409` with a `reason`:

| `reason` | What to do |
|---|---|
| `run_not_ready` | Only a finished full run with answers has a summary to refresh |
| `run_in_progress` | The store has a run going (`running_run_id`). Try again when it finishes |
| `refresh_in_progress` | A refresh of this run is already queued or running. Poll [Get a run](#run-status) |

### Suggest next steps

POST /projects/{uuid}/next-steps

AI picks 1 to 3 next steps from what kept coming back over your last 4 comparable runs (the same engines and ChatGPT model): questions no engine mentioned you for in every run and who it mentioned instead, pages AI kept using in answers that didn’t mention you, competitors it named in most runs that you don’t track, your pages ChatGPT’s search kept finding without citing them, and your cited pages that are broken or show another price. Uses **no query units** and needs a token with **Can start prompt runs**.

- The result is saved with the date and the runs it covered, and stays until you ask again. It comes back in [the report](#report) as `suggested_next_steps`.
- `409` with `reason: no_run` before the store’s first finished run. `502` with `reason: failed` when the steps couldn’t be written; the previous ones are kept.

Request

```
curl -X POST "https://searchmention.com/api/v1/projects/$STORE/next-steps" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

Response · 200

```
{
  "data": {
    "suggested_at": "2026-10-21T15:04:12+00:00",
    "run_ids": [4719, 4760, 4791, 4822],
    "latest_run_id": 4822,
    "next_steps": ["Publish a guide answering “best energy gels for marathon runners” that compares Näak with GU and Maurten."],
    "candidates": [{ "id": "Q1", "type": "lost_question", "subject": "best energy gels for marathon runners", "ref": 337 }]
  }
}
```

### Get the report

GET /projects/{uuid}/report

The same data as the shareable AI visibility report, for the latest completed run. Pass `run_id` for an earlier run. It works with report sharing turned off.

| Query | What it does |
|---|---|
| `run_id` | Optional. A full run of this store. `404` if it isn’t one, `409` with its `status` if it hasn’t completed |

Before the store’s first completed run you get `404` with `This store has no completed run to report on yet.`

| Key | What it holds |
|---|---|
| `store`, `run` | The store and its market, and the run reported on with its engines |
| `previous_run` | The earlier run on the same engines that `metrics.changes` compares against |
| `metrics` | `mention_rate` (% of prompts where any engine named you), `avg_position`, `visibility_score`, `share_of_voice`, `sentiment`, the same per engine, `changes` since the previous run, and `over_time`: questions mentioned in each of your recent comparable runs and their `average` (`null` after one run) |
| `analysis_summary` | `headline`, `strengths`, `holding_back`, `next_steps` (a store’s first run only), `competitor_insight` and `changes`. `null` until written |
| `confirmed_changes` | Questions won or lost per engine, and competitors new in or gone from answers, that held for your last 2 runs after 2 runs the other way. `null` before 4 comparable runs |
| `suggested_next_steps` | The last [Suggest next steps](#next-steps) result with its date and runs. `null` if never asked |
| `run_changes`, `intent_performance` | New wins, losses and position moves since the previous run; mention rate by prompt type |
| `prompts` | Every prompt with each engine’s result, and `history`: per engine, mentioned or not in each of your last 4 comparable runs, oldest first (`null` after one run). `outcome` (`won`, `lost` or `no_answer`), your `brand_position` and `brand_sentiment`, `who_won` when you lost or `also_named` when you won, and the engine’s full answer in `response_text`. ChatGPT answers also carry `search` (whether it searched the web, the `queries` it ran and every page it read in `sources`) and `read_your_page` (`true` when ChatGPT read one of your pages but didn’t mention or cite you); both are `null` for other engines and answers before October 2026 |
| `cited_pages` | Your pages AI linked to, with the page check and the price check (below) and the prompts that linked each one |
| `competitors` | `tracked` competitors (mentions, wins, readiness, scored pages), `discovered` brands and stores AI named that you don’t track, and `cited_domains` |
| `sources` | Third-party pages behind the answers: `mentions_you` (pages that name your store) and `outreach` (pages AI relied on where you weren’t named, with the brands they recommended) |

**Price check on cited pages.** When an AI answer quotes a price for one of your pages, the report compares it with the price on the page: `ai_price` and `page_price` (with currencies and `_display` strings), `ai_price_max` when the AI gave a range (`ai_price` is the low end, the display reads “$21–$23”, and any page price inside the range is a match), `price_matches`, `ai_price_quote` (the AI’s own words), `matched_variant` when the AI’s price matches one variant, and `ai_price_is_compare_at` when the AI quoted your compare-at (before sale) price. Pages that no longer load show `unreachable` with a reason, and `redirect_host` when a URL now redirects to another site. When the store's host turned our check away (HTTP 429), the page shows `not_checked`: we don't know whether it loads, so it isn't marked unreachable and has no readiness or page price.

Request

```
curl "https://searchmention.com/api/v1/projects/$STORE/report?run_id=4822" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

Response (trimmed)

```
{
  "data": {
    "store": { "uuid": "9b1f6c2e-…", "name": "Trail Co.", "domain": "trailco.example", "business_model": "brand", … },
    "run": {
      "id": 4822,
      "status": "completed",
      "completed_at": "2026-09-27T10:19:42+00:00",
      "engines": ["ai_overview", "chatgpt", "gemini", "perplexity"],
      "prompts": 42
    },
    "previous_run": { "id": 4821, "completed_at": "2026-09-22T09:14:51+00:00" },
    "report_url": "https://searchmention.com/reports/k3Jd9…",
    "metrics": {
      "mention_rate": 38,
      "prompts_total": 42,
      "prompts_mentioned": 16,
      "avg_position": 2.4,
      "visibility_score": 31,
      "share_of_voice": { "pct": 22, … },
      "sentiment": { "score": 78, "label": "Positive", … },
      "engines": [
        { "engine": "chatgpt", "label": "ChatGPT", "mention_rate": 45, "mentioned": 19, "answers": 42, "asked": 42, "avg_position": 2.1, … }
      ],
      "changes": { "visibility": { "value": 4, "label": "+4pt", "direction": "up" }, … }
    },
    "analysis_summary": {
      "headline": "Trail Co. is named for ultralight packs but missing from price questions.",
      "strengths": [ … ],
      "holding_back": [ … ],
      "next_steps": [ … ],
      "competitor_insight": "…"
    },
    "prompts": [
      {
        "prompt_id": 118,
        "prompt_text": "best ultralight backpack for a weekend hike",
        "query_type": "discovery",
        "mentioned_anywhere": true,
        "results": [
          {
            "engine": "chatgpt",
            "label": "ChatGPT",
            "no_answer": false,
            "outcome": "won",
            "brand_mentioned": true,
            "brand_position": 2,
            "brand_sentiment": "recommended",
            "who_won": [],
            "also_named": [
              { "domain": "ridgeline.example", "name": "Ridgeline Outfitters", "position": 1, "tracked": true, … }
            ],
            "response_text": "For a weekend hike, look at… 2. Trail Co. Ridge 40L…",
            "executed_at": "2026-09-27T10:06:13+00:00"
          }
        ]
      }
    ],
    "cited_pages": [
      {
        "url": "https://trailco.example/products/ridge-40l",
        "title": "Ridge 40L Ultralight Backpack",
        "page_type": "product",
        "http_status": 200,
        "unreachable": false,
        "not_checked": false,
        "ai_price": 169.0,
        "ai_price_max": null,
        "ai_price_display": "$169.00",
        "page_price": 189.0,
        "page_price_display": "$189.00",
        "price_matches": false,
        "ai_price_quote": "the Ridge 40L sells for about $169",
        "matched_variant": null,
        "ai_price_is_compare_at": false,
        "prompts": [ { "prompt_id": 118, "prompt_text": "best ultralight backpack for a weekend hike", "engines": ["chatgpt"] } ]
      }
    ],
    "competitors": { "tracked": [ … ], "discovered": [ … ], "cited_domains": [ … ], "locked": false },
    "sources": { "mentions_you": [ … ], "outreach": [ … ] }
  }
}
```

Answers are the engines’ plain text, without the report page’s highlighting.

### Share the report

POST /projects/{uuid}/report/share

Turns on the public report link, the same as **Share** on the report page, and returns it. Anyone with the link can open the report without an account; it always shows the store’s latest completed run. It needs a token with **Can edit prompts**.

- If sharing is already on, you get the same link back with `"result": "already_shared"`, so links you already sent keep working. A new link gives `"result": "enabled"`.
- The store needs at least one completed run first, or you get `422` with `Run prompts at least once before sharing a report.`
- Turning sharing off stays on the report page, because it breaks every link already sent.

Request

```
curl -X POST "https://searchmention.com/api/v1/projects/$STORE/report/share" \
  -H "Authorization: Bearer $SEARCHMENTION_TOKEN" \
  -H "Accept: application/json"
```

Response

```
{
  "result": "enabled",
  "data": {
    "report_url": "https://searchmention.com/reports/k3Jd9…",
    "report_views": { "count": 0, "first_viewed_at": null, "last_viewed_at": null }
  }
}
```

## Typical flow

A weekly check-in, by a script or an agent:

1. **List stores** with `GET /projects` and pick a `uuid`.
2. **Review prompts** with `GET /projects/{uuid}/prompts`. Pause or edit anything off-target with [the prompt endpoints](#edit-prompts).
3. **Preview the cost** with `POST /projects/{uuid}/runs` (no `confirm`).
4. **Confirm the run** with `confirm=true` and keep the returned `run.id`.
5. **Poll** `GET /projects/{uuid}/runs/{id}` until `report_ready` is `true` and `competitor_intel_status` is `completed`, `failed` or `skipped`, so the summary and competitor page scores are in (stop if `status` is `failed`).
6. **Fetch the report** with `GET /projects/{uuid}/report?run_id={id}`.
7. **Share it** with `POST /projects/{uuid}/report/share` when someone else should see it.

**Using an AI agent?** Give it a token without **Can start prompt runs** if it should only read. If it may run prompts, ask it to show you the preview and wait for your yes before it confirms.

## Store keys are different

Each store also has its own key: `sm_live_…` on that store’s **Settings** page, or `sm_pixel_…`, which the Shopify app sets up when you connect. Store keys reach one store and are for storefront integrations such as AI Traffic tracking. They don’t work with this API, and an account token doesn’t work in those integrations. To connect a store, follow its setup guide:

- [Cloudflare Worker](https://searchmention.com/guides/cloudflare-worker-manual): AI Traffic for any store behind Cloudflare
- [Magento 2](https://searchmention.com/guides/magento-connector-manual): AI Traffic connector
- [WooCommerce](https://searchmention.com/guides/woocommerce-catalog-manual): catalog sync
- [Shopify](https://searchmention.com/guides/shopify-getting-started): app install

[All guides](https://searchmention.com/guides)
