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

# Changelog

> Latest updates and changes to the AthenaHQ API

This page documents all notable changes to the AthenaHQ API, including new features, improvements, and bug fixes. We recommend reviewing this changelog regularly to stay informed about updates that may affect your integration.

## September 2026

### Added: who made each activity entry

`GET /api/v1/activity` now returns `actor.user` on changes made in the app: the person's `id` and `email`. It is `null` for changes made through an API key or by the system. Keys scoped to specific websites see members of those websites only; other people appear as `user: null`. Existing fields are unchanged.

### New: activity feed for partner integrations

`GET /api/v1/activity` lists the changes made to prompts, topics and prompt tags across your organization, newest first by default, with the website, the kind of change, the affected ids and whether an API key made it. Filter by `website_id`, `entity_type`, `change_type` and a `since`/`until` window; page with `page_num` and `page_size`. To mirror prompts, pin a window per sync run (`since` = the previous run's `until`, `until` = now minus one minute, `order=asc`) and walk the pages: one call per run replaces re-listing every website. An entry with `entity_ids: null` means re-list that website. Available to partner API keys.

### New: Generate pitch reports through the API

`POST /api/v1/pitches` accepts `website_url`, with optional `country` and `language`. Use your global organization API key and an `Idempotency-Key` header. The API returns `202` with `pitch_id`, `status`, and a shareable `report_url`; poll `GET /api/v1/pitches/{pitch_id}` for completion. Reports appear in your Pitch Workspace. Each admitted attempt consumes one pitch allowance unit. Repeating the same request key reuses the report, including after completion or failure. Generation sends no email.

### Fixed: MCP API-key connections from Optimizely Opal

The MCP endpoint now accepts AthenaHQ API keys in `Authorization: Bearer <api-key>`, enabling clients such as Optimizely Opal that cannot send a custom `x-api-key` header. Keys retain their existing organization, website scope, and write permissions. OAuth access tokens and `x-api-key` connections continue to work. REST endpoints under `/api/v1` still require `x-api-key`. See [Connect Optimizely Opal](/api-reference/mcp#connect-optimizely-opal) for OAuth and API-key setup.

### Added: active period on prompt schedules

`GET /api/v1/prompt-schedules` (MCP: `get_prompt_schedules`) now returns `starts_on` and `ends_on` on every schedule: the first and last UTC calendar days (`YYYY-MM-DD`) a custom schedule fires, or `null` when that side has no bound. A schedule can be `is_active: true` and still not run because today is before `starts_on` or after `ends_on`; use the two dates to tell a seasonal schedule that has ended from one that is running. Default always returns `null` for both. Existing fields are unchanged.

### Improved: tracked URLs include their Content Hub destination

`POST /api/v1/content/track` (MCP: `track_content_urls`) now includes `destination_name` and `content_hub_url` on each created and skipped result. The destination names the tab displaying the row, and the link opens that item with its website and tab selected. Shared-pool rows use the default view's name when available, or “Shared pool” when no view exists.

Omitting `sheet_id` still tracks URLs in the shared pool, regardless of the tab currently open in your browser. When a URL is already tracked, its result links to its existing location; retrying with another `sheet_id` does not move it. Use the created and skipped counts separately when reporting results, and include the destination and item links.

### New: custom Content Hub column values when tracking URLs

`POST /api/v1/content/track` (MCP: `track_content_urls`) accepts an optional `fields` list per URL. Each entry names one of the customer-created columns on the destination tab, as shown in Content Hub (`column`, matched case-insensitively), and its `value`: a string for text columns, an http(s) URL for link columns, a number for number columns, an ISO 8601 date string for date columns (a date-only value is stored as UTC midnight, the same instant the Content Hub date picker records for that day), a boolean for checkbox columns, and an array of up to 50 option labels for multi-select columns. Multi-select options that do not exist yet are created; existing ones match case-insensitively.

Values are written together with the created row, so a link tracked by an automation no longer needs its columns filled by hand. Skipped URLs (already tracked) keep their existing values. An unknown column name, a generated column (Ask Athena, approval, OG image), or a value that does not fit the column type returns `400` naming the field, and nothing is tracked. Requests without `fields` behave exactly as before.

### New: `POST /api/v1/metrics/shopping` returns the Shopping Insights dashboard

The in-app Shopping page reports how a brand's products show up in AI shopping answers (product carousels), but none of it was reachable over the API, which only covered share of voice, mention rate, citation rate and position. **POST /api/v1/metrics/shopping** returns that page in one call: a `summary` (shopping answers, own and competitor product appearances, appearance rate, average positions), an `offer_summary` (tracked offers and average price), `top_own_products` and `top_competitor_products`, the `competitor_breakdown` leaderboard, the `merchant_breakdown` of retailers, the `position_distribution` rank ladder, a `price_comparison` of own versus competitor offers, and a daily `trend`. It runs the same queries as the page, so the numbers match what a user sees in the app.

The body follows the other metrics endpoints (`website_id` plus `filters`), with `start_date` and `end_date` as inclusive `YYYY-MM-DD` dates in UTC. Only the filters the page applies are accepted: `schedule_id`, `models` (`chatgpt` and `ai_mode`, the two models that answer with carousels), `competitor_ids`, `prompt_tags` with `prompt_tags_operator`, and `target_identifiers`. Any other filter key returns 400 rather than being ignored. `top_products_limit` (1 to 50, default 10) sizes both product lists. Rates are percentages from 0 to 100 and positions are 1-based. The endpoint requires a paid plan, matching the page. The same read is available over MCP as `get_shopping_metrics`.

The endpoint is additive: every existing endpoint is unchanged.

### Fixed: ad hoc response runs appear under Default

Runs started without a schedule now belong to Default, including runs started through the API or MCP. Previously Manual-tagged history is moved to Default during the data migration. The schedule catalog and response provenance emit only `default` or custom schedule selections. Existing `schedule_id: "manual"` filters remain accepted as an alias of `default`.

`POST /api/v1/response-streaming/start` also accepts an optional `schedule_id` UUID. An explicit schedule supplies provenance and prompt selectors; supplied model, fan-out, persona and location options apply to that run. An ad hoc run can coexist with a scheduled run of the same schedule.

### Changed: website provisioning skips competitors on the website's own URL

`POST /api/v1/websites/provision` now ignores a competitor whose URL is the website's own URL or a path under it, since such a record would attribute the website's own citations to the competitor. The skip is logged and reflected in the returned `competitorCount`. Request and response shapes are unchanged.

### New: credit usage and charge history over MCP

`get_credit_usage` returns organization credit totals, time buckets, and charged-pool breakdowns. `get_credit_usage_events` returns paginated charges, refunds, recorded actions, and originating-website attribution. Both support explicit UTC windows so calendar-month investigations and follow-up pages use the same interval.

Signed-in callers need organization billing access. API-key callers need a global organization key; website-restricted keys cannot read these organization-wide details. Existing balance tools and REST endpoints are unchanged.

### Changed: partner recommendation access uses Autumn entitlements

`POST /api/v1/seo/recommendations` now requires the organization's `seo_recommendations` entitlement in Autumn. Access is configured through the organization's plan or customer subscription. Deployment variables and a separate website activation allowlist are no longer required. Existing API-key scope and website content permissions still apply.

The entitlement is checked before accepting a job and again before queued analysis starts. Missing access returns `403`; an unavailable admission check returns `500` without creating work. Unstarted work stays pending when access cannot be verified, releasing its worker slot so other organizations can proceed. Grant and revocation propagation follows the shared entitlement cache: 15 minutes while healthy, with previously verified snapshots usable for up to one hour during an Autumn outage. Queue limits, request and response shapes, and authorized saved-result polling are unchanged.

### New: asynchronous SEO recommendations for activated partners

`POST /api/v1/seo/recommendations` accepts a draft or public HTTPS page and returns a saved job to poll with `GET /api/v1/seo/recommendations/{job_id}?website_id=...`. Results contain source-grounded SEO and AI-search recommendations, evidence and token usage. Reusing a request ID with identical input returns the same job; changed input returns `409`.

Each website can queue up to 1,000 outstanding jobs. Analysis is limited to four workers per website and 16 across websites, with fair dispatch and recovery for interrupted starts. Submissions and polling share the existing organization API request budget (default 60 requests/minute); submissions also have a 120 requests/minute website ceiling. Honor `Retry-After` on `429` and back off when polling pending jobs. Submitting requires the organization's `seo_recommendations` entitlement in Autumn and website content administration access. Polling requires website content view access.

Both operations are included in the public `/api/v1/openapi.json` specification for client generation, with the same request and response schemas shown in this reference.

### New: select one streaming schedule in response analytics

Response-derived metrics, responses, sources, and content attribution accept
`filters.schedule_id`: `default` or a custom schedule's UUID. Omitting
it selects Default, not a combined view. Default includes all pre-cutover history,
including ad hoc runs. The retired `manual` selection is a Default alias. Archived
schedules remain queryable, and there is no All schedules selection.

`POST /api/v1/looker-studio/data` applies the same `filters.schedule_id`
selection to report metrics and dimensions, with separate cached results per
schedule. Existing requests without the field continue to select Default.

Use `GET /api/v1/prompt-schedules?website_id=...` (MCP: `get_prompt_schedules`)
to discover names and selection values for your website. Pass the returned
`selection`, not a stock schedule's UUID. `GET /api/v1/date-range` accepts the
same selection as the `schedule_id` query parameter. Response rows now include
an additive `schedule` provenance field with that selection value, including
`default` for legacy history. The generic `query_metrics` and `query_rows` tools
also accept a top-level `schedule` selection. Existing fields are unchanged.
Website-level information, such as billing and AI crawler visits,
is not filtered by streaming schedule.

If your MCP client cached tool definitions or approvals, refresh the tool list
and re-approve the updated analytics tools when prompted.

### Changed: editing a brand fact's text creates a new fact and archives the current one

`PATCH /api/v1/knowledge-base/facts/{fact_id}` (and the MCP tool `update_brand_fact`) no longer rewrites a fact's text in place. A request that changes `text` inserts a new fact carrying the new text, the fact's pillar and provenance, and its current review status, and archives the fact you addressed. The response shape is unchanged, but `fact.id` is the new fact's id; `before` still describes the fact you addressed. Requests that change only `source_url` or `confidence` keep updating the same fact in place. Archived facts are not returned by `GET /api/v1/knowledge-base/facts` and a `PATCH` against one returns `404`. Facts captured from AI responses, facts hidden as duplicates, and rejected facts can no longer have their text changed through this endpoint and return `409`.

### New: Set, replace, or clear a website's partner `external_id` with `PATCH /api/v1/websites/{website_id}`

Partner integrations can now change the identifier mapped to a website after it was created, instead of only at `POST /api/v1/websites` or `POST /api/v1/websites/provision` time:

* Pass `external_id` as a non-empty string to set or replace the mapping.
* Pass `external_id: null` to clear it.
* An id already mapped to another website in your organization returns `409` with the current holder in `existing`, so moving an id between two websites is a clear on the old one followed by a set on the new one.

The endpoint requires a global partner API key and also works on deleted websites, since deleting a website does not release its `external_id`.

### Changed: Optimize drafts can be marked published before their review runs

`POST /api/v1/content/{content_id}/publish` (and the MCP tool `publish_content`) no longer returns `409` for an Optimize draft that is waiting at stage `pending` with no Athena review in flight. An Optimize draft is created from a page that is already live on your site, so its live URL can be recorded, and citation tracking started, without running the review first. The same rule now drives the Track control in the Content Hub editor. A `pending` stage on any other content type still means the article is generating and is still rejected, as is an Optimize draft whose review is currently running. Response shapes are unchanged.

### New: Filter responses by their search queries

`POST /api/v1/responses` and the MCP tool `get_responses` now support three response-only filters for inspecting the web and map searches an AI model used:

* `filters.has_search_queries` selects responses based on whether they used at least one non-empty search query.
* `filters.search_query_text` performs a case-insensitive text match across the queries used for each response.
* `filters.search_query_text_operator` accepts `contains` (the default) or `does_not_contain`.

The filters compose with the existing date, model, prompt, competitor, location, and prompt metadata filters. Omitting them preserves the previous behavior and response shape.

### Changed: group membership writes tolerate duplicates and large batches

Some group requests that previously failed with `500` now succeed. The notable cases:

* **POST /api/v1/groups** with the same website ID listed more than once in `website_ids` now creates the group with a single membership per distinct ID and returns `201`. The response's `websiteIds` lists each ID once.
* **PATCH /api/v1/groups/{group_id}** no longer returns `500` when two concurrent requests add the same website to a group; both return `200`, matching the documented behavior that websites already in the group are silently ignored.
* **PATCH** requests carrying very large `add_website_ids` or `remove_website_ids` arrays that previously failed against an internal statement limit are now processed in batches and succeed.

Every request that previously succeeded returns the same response as before; no endpoint paths, response shapes, or error strings changed.

## September 2026

### New: `GET /api/v1/content/items` lists every Content Hub item, unpublished drafts included

`POST /api/v1/content` lists content with citation and impression metrics only once its generation pipeline is finished, so a Content Hub sheet of freshly generated drafts came back empty over the REST API even though `GET /api/v1/content/sheets` listed the sheet. The MCP tool `list_content` already enumerated those rows, but it had no REST counterpart. **GET /api/v1/content/items** is that counterpart: it returns every content item a website has, published or not (in-flight drafts, briefs, snipes, optimize and slice runs, scheduled and failed items, plus tracked pages), with identity fields only (`content_id`, `title`, `type`, `stage`, `sheet_id`, `sheet_name`, `topic_name`, `url`, `prompt_ids`, `prompt_count`, timestamps) and no metrics.

Filter by `sheet_id`, `content_type`, `stage`, or `prompt_id`, and page with the usual `page_num` / `page_size`. `stage` is the raw pipeline workflow state, not a publication flag: `null` for pages with no pipeline record, `generated` for text that is ready but unpublished (manual editor items too), `done` once published or marked done. An item counts as published when its stage is `done` (or it is an external or imported page) and it has a `url`. The listing reads from the primary store and is uncached, so an item is listable the moment `POST /api/v1/content/generate` returns its `content_id`. Read a row's text with `GET /api/v1/content/{content_id}`.

The endpoint is additive: `POST /api/v1/content` and every other endpoint are unchanged.

## August 2026

### New: `mistral` model filter value

Every endpoint that accepts a `models` filter now also accepts `mistral`, covering Mistral responses. The value is additive: existing filter values, response shapes and status codes are unchanged, and omitting the filter behaves exactly as before.

**If you start runs without `selected_models`, this costs you more.** `POST /api/v1/response-streaming/start` treats an omitted `selected_models` as "every currently active model", and Mistral is now one of them. An integration that has always omitted the field will start producing Mistral responses it never asked for, billed at 5 credits each like Claude, DeepSeek and Meta AI. Pass an explicit `selected_models` map to keep the previous set.

Two things worth knowing before you filter on it. Mistral responses only exist for websites that have opted the model into their tracking, so an unfamiliar website will simply return no rows rather than an error. And Mistral is reached through Mistral's own API using its built-in web search, which accepts no country parameter, so its country attribution reflects the country stated in the prompt rather than a request originating there. The same is true of `claude`, `deepseek` and `meta_ai`.

### New: `meta_ai` model filter value

Every endpoint that accepts a `models` filter now also accepts `meta_ai`, covering Meta AI (Muse Spark) responses. The value is additive: existing filter values, response shapes and status codes are unchanged, and omitting the filter behaves exactly as before.

**If you start runs without `selected_models`, this costs you more.** `POST /api/v1/response-streaming/start` treats an omitted `selected_models` as "every currently active model", and Meta AI is now one of them. An integration that has always omitted the field will start producing Meta AI responses it never asked for, billed at 5 credits each like Claude and DeepSeek. Pass an explicit `selected_models` map to keep the previous set.

Two things worth knowing before you filter on it. Meta AI responses only exist for websites that have opted the model into their tracking, so an unfamiliar website will simply return no rows rather than an error. And Meta AI is reached through Meta's Model API rather than scraped from a country-local session, so its country attribution reflects the country stated in the prompt, not a request originating there. The same is true of `claude` and `deepseek`.

### New: `baseCountry` on website listings

**GET /api/v1/websites** (and the MCP tool `list_websites`) now includes each website's `baseCountry`: the country market the website targets, e.g. `"United States"`. Organizations that track one brand across several countries model each market as its own website, and until now nothing in the listing distinguished them, so same-name websites were impossible to tell apart without out-of-band knowledge. The value is editable in the website's brand profile (the "Base location" setting). Websites that never chose a country carry the default `"United States"`, so treat that value as unconfirmed for sites that do not look US-based.

The field is additive: no existing fields or status codes changed. The external-id lookup paths (the legacy `?external_id=` filter on this endpoint and `GET /api/v1/websites/by-external-id/{external_id}`) do not include the new field. If you generate a typed client from this spec, regenerating will pick up the new optional field.

### New: Knowledge Base pillar curation

Pillars stop being create-only and facts stop being stuck where they landed: four new endpoints close the Knowledge Base curation loop. All four require a website-admin API key and the Knowledge Base to be enabled for the organization.

* **PATCH /api/v1/knowledge-base/pillars/{pillar_id}** updates a pillar's `name`, `description`, or `status` (at least one required; only provided fields change). Renaming returns a `409` when another pillar already uses the name. Status accepts `published` or `archived`: archiving hides the pillar from listings and stops its facts feeding content generation while keeping them stored, so it is the safe alternative to deletion. Also available as the MCP tool `update_pillar`.
* **POST /api/v1/knowledge-base/facts/move** files 1-50 facts under a published pillar (also the MCP tool `move_brand_facts`). This works for unfiled facts, which until now were readable over the API but not fixable, and re-files facts from other pillars. Review status never changes, and only approved facts under a published pillar feed content generation, so moving is what puts approved facts to work. Missing ids are reported in `not_found_ids` rather than failing the call, so retries are idempotent.
* **POST /api/v1/knowledge-base/pillars/merge** folds 1-20 source pillars into a published target: every fact on the sources re-files onto the target, page links carry over keeping the stronger signal, and the source pillars are then deleted (also the MCP tool `merge_pillars`). Unlike the other Knowledge Base writes, retries are strict: missing source ids are rejected rather than skipped, so re-read the pillars before retrying a failed merge.
* **POST /api/v1/knowledge-base/pillars/delete** permanently deletes 1-20 pillars and every fact filed under them; facts are deleted, not unfiled, and there is no undo (also the MCP tool `delete_pillars`). The response reports each deleted pillar's name and fact count, since a deleted pillar cannot be re-fetched. Like `delete_brand_facts`, OAuth MCP connections gate on the signed-in user's admin role rather than the brand-knowledge write permission.

### New: Remove group member access with `DELETE /api/v1/groups/{group_id}/members/{user_id}`

The new endpoint deprovisions a user from a group in one request. It removes the direct group membership and explicit website memberships for websites currently in that group.

* The response reports whether the group membership was removed and how many website memberships were removed. Retries and well-formed missing targets are idempotent and return zero removal counts.
* Organization membership, membership in other groups, and website membership outside the group are preserved. Access to a shared website remains when another group still grants it.
* Organization members return `409` because their organization-derived access would remain. The endpoint requires a global API key and prohibits removing the API key's own user.

### Changed: Attribute direction now selects response-level perception

The four attribute endpoints keep their existing paths and response shapes, but attributes are now direction-neutral topics. The `positive` value in a response identifies the selected positive or negative observation series instead of permanently classifying the attribute name.

* `GET /api/v1/attributes`, `POST /api/v1/attributes/cumulative`, `POST /api/v1/attributes/competitors`, and `POST /api/v1/attributes/time-series` accept an optional top-level `positive` boolean.
* Omitting `positive` defaults to `true`, preserving existing requests. Pass `positive: false` for negative observations of the same attribute ID.
* Percentages remain directional prevalence among sampled, successfully analyzed responses that mention the entity. A response with both directions can count once in each series, and an analyzed response with no attribute claim remains in the denominator.

No response fields, endpoint paths, or envelopes changed.

### Changed: request bodies now enforce explicit size bounds

Website, provisioning, and prompt write endpoints previously accepted unbounded strings and arrays. They now validate size limits and return `400` when a field exceeds them. Every limit sits well above real production usage (the largest identifier set ever stored is 166 entries; the longest prompt is under 1,000 characters), so requests that were working before continue to work unchanged. The OpenAPI spec now declares the corresponding `maxLength` / `maxItems` values.

* `POST /api/v1/websites`: `url` up to 2,048 characters.
* `POST /api/v1/websites/provision`: `url` up to 2,048 characters and `name` up to 255; `identifiers` up to 200 entries of 500 characters each (also per competitor); `competitors` up to 500 entries, each with the same name/URL bounds; `prompts` up to 10,000 entries with `text` up to 2,000 characters. 500 is also the ceiling on live competitors per website across the whole API: create requests past it return `400`.
* `POST /api/v1/prompts` and `PATCH /api/v1/prompts/{prompt_id}`: prompt text up to 2,000 characters. The existing batch bound of 1,000 prompts per create call is unchanged.
* `POST /api/v1/prompts` is now also rate limited to 60 requests per minute per website, independent of your organization-level API rate limit. Each call accepts up to 1,000 prompts, so batching keeps effective throughput at roughly 60,000 prompts per minute per website; exceeding the window returns `429`.

If you generate a typed client from this spec, regenerating will pick up the new bounds as validation annotations. No field shapes or names changed; the only status-code additions are the `400` on oversized payloads described above and the `429` on `POST /api/v1/prompts` when the per-website rate limit is exceeded.

### Clarified: Response progress can reach 100% before analysis finishes

`GET /api/v1/response-streaming/status` keeps the same response shape and state values. Its documentation now makes the readiness boundary explicit: `progress` measures response collection, while `state` covers the whole run. A run can report `state: "running"` with `progress.percentage: 100` while downstream sentiment and attribute analysis finishes. Treat only `state: "completed"` as the signal that downstream analysis is ready. No client migration is required.

### New: Update a group member's role with `PATCH /api/v1/groups/{group_id}/members/{user_id}`

Group membership was write-once over the API: `POST /api/v1/invites/bulk` could add a user to a group at a given role, and `GET /api/v1/groups/{group_id}/members` could read it back, but there was no way to change that role afterwards. The only options were removing and re-inviting the user, or editing the role in the dashboard. The new endpoint closes that gap, mirroring the existing Update Website Member Role and Update Organization Member Role endpoints:

* Accepts either the coarse `role` (`admin` / `viewer`) or a `role_id`, which can be a system role or one of your organization's custom roles from `GET /api/v1/roles`. Provide exactly one. The response echoes both the assigned `role_id` and its slug, the same `role` value the member listing returns.
* It targets explicit group members only, the `access_type: "group"` entries in the member listing. Organization members have no group-level role to change, because their access to every group comes from their organization role; targeting one returns `409`. Note that changing that organization role would take effect across every group and website in the organization, not only the group you were working on.
* Requires a global API key, and cannot modify the key's own user. Unlike the organization endpoint there is no last-admin restriction: a group's only admin can be demoted, since organization admins keep admin access to every group in the organization.
* **Roles outside `admin` and `viewer` are honored for access but are not yet rendered in the dashboard.** Assigning a group member a `role_id` such as Editor or Billing Manager grants that role's real permissions on every website in the group, but the dashboard's group member list currently displays any non-admin role as "Viewer". Until that is addressed, prefer `admin` / `viewer` at group scope when a customer administrator also needs to see the role.

### New: `GET /api/v1/content/{content_id}` returns the prompts a page targets

The response carries two new fields, `prompts` and `prompts_total`. `prompts` lists the prompts the content was written to target, the same ids `POST /api/v1/content/generate` accepts as `prompt_ids`, each with its text, topic, and status. Until now that association was write-only over the API: you could set it when generating content and never read it back.

* Ordered oldest association first and capped at 100 entries, with the untruncated size in `prompts_total`. Bulk imports can attach thousands of prompts to one page, so check `prompts_total` before assuming the list is complete. Associations past the 100th are not currently retrievable.
* `status` is `active`, `paused`, or `deleted`. Deleted prompts are included because the page was written for them and the Content Hub still displays them. `GET /api/v1/prompts` does not list deleted prompts, so an id from here may not resolve there; the text is returned inline for that reason.
* An empty array means the page has no prompt targeting recorded. That is normal for imported and externally tracked pages.

This is a different question from `POST /api/v1/content/prompts`, which lists the prompts whose AI responses **cited** the page over a date range. Targeting comes from the content record and does not depend on any citation having happened, so a page with no citations yet still returns its prompts here. Both fields are additive; existing clients are unaffected.

### New: Lighter response payloads with `include_response` and `compact_sources`

`POST /api/v1/responses` (and the MCP tool `get_responses`) accepts two optional booleans that let you drop the two fields that dominate a row's size. Both default to today's behavior, so a call that omits them returns exactly the same body as before, and `page_size` is unchanged at a maximum of 100.

* **`include_response: false`** omits the `response` field. The answer text averages around 2.2 KB per row and is usually the single heaviest thing you page through.
* **`compact_sources: true`** replaces the `sources` array of per-URL objects with `source_domains`, a deduped and alphabetically sorted list of cited root domains, averaging around 3.1 KB less per row. Hosts are lower-cased with any leading `www.` removed, and subdomains are preserved, so `shop.example.com` stays distinct from `example.com`.

Together these are the intended way to export prompt fan-out variations at volume. Pair them with `filters.variation_filter: ["variation"]` and each row still carries the base prompt, the generated variation, the model, the `mentioned` and `cited` flags for your domain, and the cited domains, at roughly a tenth of the previous size.

**If you generate a typed client from this spec, read this before upgrading.** `response` and `sources` are no longer listed as `required` on the response row, because they are genuinely absent from the payload when you opt out. Regenerating your client will make both fields optional and may surface as a compile error even if you never set either flag. The runtime shape of a call that omits both flags has not changed. Opted-out fields are absent from the row rather than empty, since an empty `response` would read as "the model answered nothing". `GET /api/v1/responses/{response_id}` is unchanged and always returns the full row.

## July 2026

### New: Track existing URLs as content with `POST /api/v1/content/track`

Publishing closed one half of the attribution loop; this closes the other. Until now the API could only attach a URL to content Athena generated (`publish`), so pages that already existed, guest posts, partner placements, or any page written outside Athena, could not be tracked at all. The new track endpoint (also the MCP tool `track_content_urls`) registers a list of up to 500 URLs as tracked Content Hub pages in one call:

* Each URL becomes an external content row whose normalized URL is the citation/metric join key, so citations and mentions in your monitored AI responses start attributing immediately. There is no publish step, no generation, and no credit cost. Third-party domains work; tracking is not limited to your own site.
* The call is idempotent per URL: a URL already present on the website (any content type) is never duplicated. The response splits `created` and `skipped`, and each skipped entry carries the existing row's `content_id` and `type`, so you can tell an already-tracked page apart from a collision with first-party generated content before chaining other calls onto the id. Tracking-param variants of the same page collapse to one row.
* Rows are created without page text, which citation tracking does not need. The page body can be pulled on demand from the Content Hub (Fetch content, on external pages without a body).
* Optional `sheet_id` files the new rows under a Content Hub tab; optional per-URL `title` overrides the slug-derived default.

### Changed: `get_tracked_content` no longer hides external and imported pages once their body is fetched

Tracked external and imported pages disappeared from `POST /api/v1/content` (and the MCP tool `get_tracked_content`) as soon as their page text was scraped, because the fill stamped an internal pipeline status that the listing's published filter excluded. The pages were still tracked, their citations still attributed, and the Content Hub still showed them; only this listing dropped them. External and imported pages are now always listed. Expect row counts to increase on websites with imported content: pages that were silently hidden reappear with their metrics intact. Generated content is unchanged and still appears only once published.

### New: Edit and delete brand facts

The Knowledge Base write surface is no longer append-only: an integration that adds facts can now also fix or remove them. Both endpoints require a website-admin API key and the Knowledge Base to be enabled for the organization.

* **PATCH /api/v1/knowledge-base/facts/{fact_id}** updates a fact's `text`, `source_url`, or `confidence`. Only provided fields change, and `source_url: ""` clears the stored source. Editing text re-embeds the fact for semantic search; a `409` is returned when the new text collides with a sibling fact captured from the same AI response (duplicates across other facts are left to Knowledge Base dedup). Review status never changes through this endpoint. Also available as the MCP tool `update_brand_fact` on API-key connections and org-bound OAuth sessions with brand-knowledge write permission.
* **POST /api/v1/knowledge-base/facts/delete** permanently deletes 1-50 facts in one call (a single delete is a one-element array). There is no undo. Ids with no fact on the website are reported in `not_found_ids` instead of failing the call, so retries are idempotent. Deleted facts drop out of semantic search and the owning pillar's document regenerates without them. Also available as the MCP tool `delete_brand_facts`; on OAuth sessions it requires the signed-in user to hold the admin role, matching the dashboard's delete gate.

### New: website objects carry the partner `external_id` everywhere

For partner integrations, every endpoint that returns website objects now includes the partner-supplied identifier mapped to that website, so you can match sites by your own id instead of by URL (a site's `websiteUrl` changes when its primary URL is edited in Athena):

* `GET /api/v1/groups` and `GET /api/v1/groups/{group_id}`: each website object inside `websites` carries `externalId` (`null` when no mapping exists), alongside the group-level `externalId`. The `PATCH /api/v1/groups/{group_id}` response has the same shape. These per-website fields have been returned since early July and are now documented.
* `GET /api/v1/users/{user_id}/websites`: each item now includes `external_id` (snake\_case, matching that endpoint's field naming).

The field appears only on responses to partner API keys; direct-customer responses omit it entirely. Mappings are created by passing `external_id` to `POST /api/v1/websites` or `POST /api/v1/websites/provision`; `GET /api/v1/websites` and `GET /api/v1/websites/by-external-id/{external_id}` already exposed them.

### Changed: fact listings exclude Oracle-generated facts by default (new `include_oracle` parameter)

`GET /api/v1/knowledge-base/facts` (and the MCP tool `get_brand_facts`) now excludes facts produced by Oracle's automated analysis from every listing, including the default one. Those facts are never routed to pillars and on most websites dominated the corpus (often millions of rows, inserted continuously), burying the user-curated facts the endpoint exists to surface. The response shape is unchanged; only which rows match changed. To include Oracle-generated facts, pass the new `include_oracle=true` parameter; it composes with every other filter, so `include_oracle=true&unfiled=true` reproduces the previous unfiled behavior. `GET /api/v1/knowledge-base/facts/search` is unaffected and still searches the full approved corpus, so a fact returned by search (with `pillar: null`) is not guaranteed to appear in a default listing.

### New: Mark content as published with `POST /api/v1/content/{content_id}/publish`

Until now the content workflow ended one step short over the API: you could create a piece, revise it, approve its brief, and publish it to your site yourself, but there was no way to tell Athena the piece was live. The item stayed unpublished forever, and since the URL is the join key citations and mentions attach to, citation tracking never started.

The new publish endpoint (also the MCP tool `publish_content`) closes that loop. Call it after the piece is actually live, passing the exact public URL of the page:

* The URL is stored as the item's citation/metric join key (protocol and trailing slashes are stripped; a bare `acme.com/blog/post` is fine). Query strings and fragments are excluded from the join key for non-YouTube URLs, so pasting a UTM-tagged URL does not break attribution; the display URL keeps them. YouTube URLs keep their params, since the video id lives there.
* The publish time is stamped and the pipeline stage moves to `done`, so the item now counts as published in Content Hub and in `get_tracked_content`.
* A first publish requires a finished article (stage `generated`). Briefs, still-generating or failed pieces, and items with a scheduled CMS publish return `409` with the reason, so incomplete content cannot be marked live.
* Calling it again on an already-published item updates the tracked URL only, keeping the original publish time. The response's `already_published` flag tells you which of the two happened. External and imported pages are live by definition, so a call on them is always a URL-only update.

This is the only way to set a content URL over the API: the rename endpoint (`PATCH /api/v1/content/{content_id}`) remains deliberately title-only.

### New: `list_content` MCP tool for enumerating unpublished drafts

`get_tracked_content` lists content with citation and impression metrics only once its generation pipeline is finished, so a sheet full of in-progress drafts listed as empty: there was no way to enumerate a sheet's unpublished pipeline items without already holding their `content_id`s. Rather than blur that tool's "tracked" semantics with a flag, there is now a dedicated MCP tool, `list_content`, that enumerates every content item a website has, published or not: in-flight drafts, briefs, snipes, optimize and slice runs, scheduled and failed items, plus tracked pages.

It returns identity fields only (`content_id`, `title`, `type`, `stage`, `sheet_id`, `sheet_name`, `topic_name`, `url`, timestamps), no metrics, and supports `sheet_id`, `content_type`, and `stage` filters with the usual `page_num` / `page_size` pagination. Passing the `sheet_id` of a view-type tab (see `sheet_type` on `get_content_hub_sheets`) lists the shared main pool that views overlay, without the view's saved filters. `stage` is the raw pipeline workflow state, not a publication flag: `null` for pages with no pipeline record (most external and imported tracked pages), otherwise the stage the pipeline is at (`pending_brief`, `generated`, `done`, `scheduled`, `failed`, and so on). An item counts as published in Content Hub terms when its stage is `done` (or it is an external or imported page) and it has a `url`; manual editor items carry `generated`. The listing reads from the primary store and is uncached, so an item is listable the moment `create_content` returns its `content_id`, before any brief approval, generation, or publish. `get_tracked_content` and `POST /api/v1/content` are unchanged.

### New: Deterministic draft edits with `POST /api/v1/content/{content_id}/edit`

Until now the only way to change an article body over the API was `revise`, which hands your instruction to an AI model and saves whatever it writes. The new edit endpoint (also the MCP tool `edit_content`) is the deterministic counterpart: you send exact find-and-replace pairs and your replacement text lands verbatim, no model involved.

* Each edit's `find` text must match the current body exactly once. Zero matches (stale snapshot, wrong anchor) or multiple matches (ambiguous anchor) fail the whole call with `400` and nothing is saved; extend the `find` text until it is unique.
* Up to 20 edits per call, applied in order, with later edits seeing earlier results. The batch is recorded as one new version, restorable like any other.
* Optional `expected_version_number` (from the draft endpoint) makes the call fail with `409` when a new version was recorded after your read (a revision, edit, or restore). An app autosave changes the body without recording a version; that drift is caught by the anchors themselves, since a moved or vanished `find` text fails the call.

The `revise` and `brief/revise` descriptions now also state explicitly that their rewrites are AI-generated text, not your literal wording, so it is clear which of the two tools to reach for.

### Fixed: Narrow revise instructions could replace the whole article with the edited passage

`POST /api/v1/content/{content_id}/revise` (and the MCP tool `revise_content`) could destroy a draft when given a narrowly-scoped instruction such as "change only this sentence, alter nothing else": the model sometimes returned only the edited passage, and that fragment was saved as the new article. The endpoint now guards against this. A revision that comes back dramatically shorter than the current draft, without an instruction that calls for shortening, is retried with an explicit whole-document directive; if it still comes back as a fragment, the call fails with a `500` and the draft is left unchanged. Nothing is ever silently truncated, and earlier text remains restorable via the version endpoints.

The response now also includes `body_word_count` and `previous_body_word_count` (both integers), so callers can verify the size change themselves. The returned `body` is always the complete replacement article. Same additions apply to `POST /api/v1/content/{content_id}/brief/revise`'s underlying guard, though its response shape is unchanged.

### Changed: `filters.competitor_ids` on content endpoints is now strict

`POST /api/v1/content` and `POST /api/v1/content/prompts` accept `filters.competitor_ids`. Previously the filter also kept responses that mention no competitors at all, so filtered citation rates and response counts included competitor-free responses. The filter is now strict: a response counts only when it mentions at least one of the given competitor ids, matching how the same filter behaves on the responses and sources endpoints. Requests that omit the filter are unchanged. Both endpoints are also significantly faster.

### New: `fact_review` on content generation

`POST /api/v1/content/generate` (and the MCP tool `create_content`) now accepts an optional `fact_review` object for draft mode: `{ topic_ids, kept_claim_ids, excluded_claim_ids }`. When present, generation uses exactly the reviewed knowledge-base pillars and kept facts instead of computing a selection of its own, so what a reviewer approved is what the draft is written from. `pinned_claim_ids` no longer influences selection when a review is present (the review already reflects any pins); it is still recorded for display. Requires the knowledge base to be enabled: sending `fact_review` with `kb_mode: "none"` or a non-draft `mode` returns a validation error.

Content generation is also now strictly pillar-scoped: knowledge-base claims without a pillar are never used, whether selected automatically, pinned via `pinned_claim_ids`, or kept via `fact_review`. Ids that resolve to an unfiled claim are dropped and reported in the run's selection outcome. File a fact under a pillar to make it available to generation.

### New: `409` when deleting a website that holds your subscription

`POST /api/v1/websites/{website_id}/delete` now returns `409 Conflict` instead of silently succeeding when the target website carries your organization's subscription.

Plans attach per organization, but subscriptions bought before that change sit on one specific website. Deleting that website used to soft-delete the record while leaving the subscription and its credit balance stranded on it: billing continued, and every remaining website reported zero credits. The endpoint now refuses the delete and returns the reason, so the plan can be cancelled or moved first.

Deletes of websites that do not hold a subscription are unaffected, as are all `restore` calls. If you delete websites programmatically, handle `409` as "cancel or move billing first", not as a retryable error.

### Fixed: `cited` was always `false` on responses

`POST /api/v1/responses` and `GET /api/v1/responses/{response_id}` returned `cited: false` on every response, even when the response's own `sources[]` contained your website's domain. The flag was read from a stored value that the ingestion pipeline never populated. It is now derived from the response's sources at read time: `cited` is `true` when any source resolves to your website's root domain (protocol, `www.`, and letter-case insensitive). The fix applies to all historical responses, not just new ones, and the response shape is unchanged. Aggregate citation metrics (such as the sources endpoints) were computed independently and were always correct.

### Fixed: Content detail returned null text for brief-stage drafts

`GET /api/v1/content/{content_id}` (and the MCP tool `get_content_detail`) returned `status`, `brief`, `body`, and every other generation field as `null` for any draft whose article had not been written yet, even though the brief existed and was visible in the dashboard. The bug hid exactly the pieces most worth reading over the API: drafts parked at `generated_brief` awaiting brief approval. These now return their brief and status correctly. A genuinely unknown or inaccessible `content_id` continues to return `404`; `null` fields now only mean the field is actually empty.

### New: `stage` on the content status endpoint

`GET /api/v1/content/{content_id}/status` (and the MCP tool `get_content_status`) now returns a `stage` field alongside the normalized `status`. `status` is unchanged (`running | succeeded | failed`), but it cannot distinguish "actively generating" from "brief ready, waiting for approval": both report `running`, and a draft created with `auto_approve: false` waits at the brief stage indefinitely. `stage` exposes the raw pipeline stage (`scheduled`, `pending`, `pending_brief`, `generated_brief`, `pending_article`, `generated`, `done`, `failed`, or `null` when generation has not started), so a poller seeing `stage: "generated_brief"` knows to call `POST /api/v1/content/{content_id}/brief/approve` rather than keep waiting.

### New: Review and iterate on briefs and drafts

Content generation used to be one-shot: you could start a piece and poll it, but changing what came back meant generating a new one and paying for it again. You can now read a draft, revise it with a plain-language instruction, and step back through its history — for the brief as well as the article.

Every revision is recorded as a version, so nothing is overwritten irrecoverably. The brief and the article share one timeline: `get_content_versions` tells the whole story of a piece, from the brief it was planned with to the article it became.

**The brief phase.** Pass `auto_approve: false` to `POST /api/v1/content/generate` and the piece stops at `generated_brief` instead of writing the article. That gate already existed; these endpoints make it usable end to end.

* **GET /api/v1/content/{content_id}/draft** — The current text: article body, brief, meta description, status, and the latest version number. A narrow read for when you only want the text — `GET /api/v1/content/{content_id}` remains the full record. Also available as the MCP tool `get_content_draft`.
* **POST /api/v1/content/{content_id}/brief/revise** — Rewrites the brief from an instruction ("add a section on pricing objections"). Also `revise_brief`.
* **POST /api/v1/content/{content_id}/brief/approve** — Approves the brief and starts the article from it, as the brief currently stands. Consumes content credits. Also `approve_brief`.

**The article phase.**

* **POST /api/v1/content/{content_id}/revise** — Rewrites the article from an instruction. Iterate by calling it again; each call starts from the current text, so there is no version to pass. Also `revise_content`.
* **POST /api/v1/content/{content_id}/versions** — The version history, newest first, with each entry's label, how it was produced, and who made it. POST rather than GET because it takes pagination in the body. Also `get_content_versions`.
* **GET /api/v1/content/{content_id}/versions/{version_id}** — The full text of one version. Fetch two and diff them yourself to see what a pass changed. Also `get_content_version`.
* **POST /api/v1/content/{content_id}/versions/{version_id}/restore** — Brings an earlier version's text back as the current draft. Also `restore_content_version`.

Three things worth knowing:

* **Restoring never deletes.** The restored text is appended as a new version on top of the history, so the passes in between stay readable and restorable, and the history records that a restore happened.
* **Revising is not regenerating.** A revision edits the text you already have; it does not re-run research or scraping. Revising a brief after the article exists corrects the brief for a future regeneration — it does not rewrite the article.
* **A piece being generated cannot be revised.** While a generation is in flight these endpoints return `409`; poll `GET /api/v1/content/{content_id}/status` until it reports `succeeded`.

### New: Create saved views over MCP

You can now create saved views (dashboard filter presets) through the MCP server. A saved view captures a named set of filters and appears in the dashboard filter bar for every member of the website.

* **`create_saved_view`** (MCP tool) — Creates a saved view from a `name`, an optional `icon`, and a `filters` object. The `filters` payload is stored verbatim (keys are not case-mapped) and is capped at 64KB. There is deliberately no REST equivalent: creating filter presets is an assistant workflow, not a partner-integration one.

Views created on an API-key MCP connection have no owning user: `user_id` is `null` in `GET /api/v1/saved-views` responses (previously it was always a user ID) and the dashboard shows them as created via API. Such views can only be edited or deleted by website admins, in the dashboard. Views created over a sign-in MCP connection are attributed to the connected user, same as views created in the dashboard.

### New: AthenaHQ plugin for ChatGPT

AthenaHQ is now available as an official plugin in ChatGPT, so connecting no longer requires a custom MCP connector or developer mode. [Install the plugin](https://chatgpt.com/plugins/plugin_asdk_app_6a4da6c4bcfc81919397faceada2af69), sign in with your AthenaHQ account, and mention `@AthenaHQ` in any conversation to ask about share of voice, citations, sources, tracked prompts, and more. Plugin connections are read-only, same as other sign-in connections. See [MCP Server](/api-reference/mcp) for details.

### New: Attributes (brand-perception keywords)

This release introduced retrieval of brand-perception attributes and how often each appeared in AI responses. The August 2026 update changed attributes to neutral traits, such as **Pricing** or **Support responsiveness**, with positive and negative observations of the same trait. Current attributes do not have a fixed sentiment.

* **GET /api/v1/attributes** — Lists the attributes tracked for a website (`{ "id", "name", "positive" }`). Use it to discover attribute IDs. Also available as the MCP tool `get_attributes`.
* **POST /api/v1/attributes/cumulative** — Per attribute, how many responses mentioned it across the date range, and the percentage. Attributes nobody mentioned are returned with `response_count` 0 rather than omitted. Also available as the MCP tool `get_attribute_metrics`.
* **POST /api/v1/attributes/competitors** — The same breakdown per tracked competitor, so you can compare how your brand is described against theirs. Each competitor has its own denominator; competitors with no responses in the date range are omitted. Also available as the MCP tool `get_competitor_attribute_metrics`.
* **POST /api/v1/attributes/time-series** — The daily trend for one attribute, brand and competitors side by side. Requires an `attribute_id` from the list endpoint. Days with no data are returned as zeros, so the series is gap-free.

Two things worth knowing when reading this data:

* `positive` describes the selected directional observation series. Attribute names are neutral and the same attribute ID can have both positive and negative observations.
* `percentage` is the share of **analyzed** responses, not of all responses. Athena samples responses for attribute extraction, and un-analyzed ones are excluded from the denominator.

### New: Knowledge Base writes

You can now feed a website's brand Knowledge Base through the API. Write inputs are declarative — fact text, optional source URL, optional pillar — with no status or approval field: every fact runs the full ingestion pipeline (deduplication, approval gates, pillar routing) and the response reports what the pipeline decided, per fact. The `outcome` values (`approved`, `pending`, `duplicate`) form an extensible enum: new values may be added as the pipeline evolves, so treat unknown values as non-terminal. Both endpoints require a website-admin API key and the Knowledge Base to be enabled for the organization.

* **POST /api/v1/knowledge-base/facts** — Adds 1-50 brand facts in one call and returns one outcome per fact (`{ "index", "outcome", "fact_id", "pillar_id" }`), which doubles as the ingest report. Facts pinned to a `pillar_id` land exactly there; the rest are routed automatically and may land unfiled. Also available as the MCP tool `add_brand_facts` on API-key connections and org-bound OAuth sessions with brand-knowledge write permission.
* **POST /api/v1/knowledge-base/pillars** — Creates a pillar from a `name` and optional `description`. Get-or-create semantics: an existing name returns the pillar with `created: false`, keeping retries idempotent. Also available as the MCP tool `create_pillar`.

### New: Knowledge Base reads

You can now read a website's brand Knowledge Base through the API: the approved brand facts (statements like "Our Pro plan starts at \$99/month", each backed by a source) and the pillars that organize them. All four endpoints require the Knowledge Base to be enabled for the organization; when it isn't, they return a `403` with `"Knowledge Base is not enabled for this organization"`.

* **GET /api/v1/knowledge-base/facts/search** — Semantic search over a website's approved brand facts. Combines vector similarity with full-text search, so it also reaches facts not filed under any pillar. Also available as the MCP tool `search_brand_facts`.
* **GET /api/v1/knowledge-base/facts** — Lists brand facts, newest first, with offset paging and filters: pillar, review status (defaults to approved), source type, and `unfiled=true` for facts not under any published pillar. Also available as the MCP tool `get_brand_facts`.
* **GET /api/v1/knowledge-base/pillars** — Lists the website's pillars (excluding archived ones) with approved-fact counts, whether a synthesized document exists, and when each was last researched. Also available as the MCP tool `get_pillars`.
* **GET /api/v1/knowledge-base/pillars/{pillar_id}/document** — Fetches a pillar's synthesized markdown document; returns `document: null` when the pillar exists but has no document yet. Also available as the MCP tool `get_pillar_document`.

### New: Persona discovery

You can now discover a website's personas through the API. Persona IDs previously appeared as bare `persona_id` values in other responses with no way to resolve them to names.

* New **GET /api/v1/personas** endpoint lists a website's personas with per-persona assigned-prompt counts (`{ "id", "name", "description", "prompt_count" }`). Also available as the MCP tool `get_personas`.

### New: Content generation endpoints and MCP actions

You can now create and manage content in Athena programmatically, closing the loop from insight to action: spot a gap, then draft content for it without leaving the API or your AI assistant.

* **POST /api/v1/content/generate** — Starts the content pipeline in one of four modes: `draft` (write new content from `prompt_ids`), `snipe` (outrank a competitor URL), `optimize` (improve an existing URL for AI search), or `slice` (split one URL into several articles). Returns durable `content_ids` you can poll. Consumes content credits. Requires a website-admin API key. Also available as the MCP tool `create_content` on API-key connections.
* **GET /api/v1/content/{content_id}/status** — Returns the normalized generation status (`running`, `succeeded`, or `failed`) for one content item. Poll it per `content_id` returned by the generate endpoint. Also available as the MCP tool `get_content_status`.
* **PATCH /api/v1/content/{content_id}** — Renames a content item's title. The URL is the item's stable identity and cannot be changed. Requires a website-admin API key. Also available as the MCP tool `rename_content` on API-key connections (initially shipped as `update_content`; renamed because the tool only renames the title).
* **DELETE /api/v1/content/{content_id}** — Permanently deletes a content item (no undo). Requires a website-admin API key. Also available as the MCP tool `delete_content` on API-key connections.

Listing and reading content were already available (**POST /api/v1/content**, **GET /api/v1/content/{content_id}**, and the MCP tools `get_tracked_content` / `get_content_detail`).

### New: Topic management endpoints

You can now manage topics via the API, completing the programmatic prompt management round trip: create a topic, get back its `id`, and pass it as `prompts[].topic_id` when creating prompts.

* **GET /api/v1/topics** — Lists a website's active topics with the count of active prompts in each. Use it to resolve a topic name to its `id`. Also available as the MCP tool `get_topics`.
* **POST /api/v1/topics** — Creates a topic on a website from a `name` and optional `description`. Get-or-create semantics: if an active topic with the same name already exists, the existing topic is returned with `created: false` instead of failing, so retries and re-imports are idempotent. Requires a website-admin API key. Also available as the MCP tool `create_topic` on API-key connections.
* **PATCH /api/v1/topics/{topic_id}** — Updates a topic's `name` and/or `description`. Renames don't affect metrics or prompt grouping (everything joins on the topic's `id`). Requires a website-admin API key. Also available as the MCP tool `update_topic` on API-key connections.
* **DELETE /api/v1/topics/{topic_id}** — Soft-deletes a topic. By default its prompts are kept (they stay active and can be re-categorized); pass `delete_prompts=true` to also soft-delete every active prompt in the topic. Reversible: creating a topic with the same name restores it. Requires a website-admin API key. Also available as the MCP tool `delete_topic` on API-key connections.

### New: Prompt tag discovery

You can now discover a website's prompt tags through the API. Previously the `prompt_tags` filter on **GET /api/v1/prompts** and the `tag_ids` field on **POST /api/v1/prompts** required tag UUIDs that the API did not expose anywhere.

* **GET /api/v1/prompts** now includes a `tags` array on each prompt (`[{ "id", "name" }]`, sorted by name, empty when untagged). Existing fields are unchanged.
* New **GET /api/v1/prompt-tags** endpoint lists a website's tags with per-tag prompt counts (`{ "id", "name", "prompt_count" }`), so a tag picker can populate without fetching all prompts. Also available as the MCP tool `get_prompt_tags`.

### Changed: Provisioning responses report billing entity creation

Website and group creation responses now include `billingEntityCreated`: whether the billing entity that backs credits was provisioned. When `false`, the website or group was created but cannot hold credits yet: setting credits fails with a `400` until it is repaired (for groups, call **PATCH /api/v1/groups/{group_id}** with `billing_enabled: true`; for websites, contact AthenaHQ support). Previously a failed billing setup was silent and only surfaced when setting credits later.

Affects **POST /api/v1/websites** and **POST /api/v1/websites/provision** (always present), and **POST /api/v1/groups** (present when `billing_enabled` is requested).

### New: Write tools on the MCP server

The MCP server is no longer read-only. Connections authenticated with an **API key** now get write tools mirroring the corresponding REST endpoints 1:1: same field names (snake\_case), same validation, same audit logging. OAuth-connected MCP sessions remain read-only and do not see these tools.

* `create_prompts`, `update_prompt`, `set_prompts_status`, `delete_prompt`: the prompt management operations (POST/PATCH/DELETE `/api/v1/prompts...`).
* `create_location`, `update_location`, `delete_location`: the location management operations (POST/PATCH/DELETE `/api/v1/locations...`).
* `start_response_streaming`: starts a response-streaming run (POST `/api/v1/response-streaming/start`). Status polling was already available as `get_response_streaming_status`.

Read endpoints that previously had no MCP tool are now exposed too, completing 1:1 coverage of the procedure-backed API surface: `get_user_by_email` ([POST /api/v1/users/by-email](/api-reference/team-management/look-up-user-by-email), global API key required), `get_response_detail` (GET /api/v1/responses/{response_id}), `get_groups` and `get_group_detail` (GET /api/v1/groups..., global API key required), `check_ai_access` (POST /api/v1/ai-access/check), and `get_position_distribution` (POST /api/v1/metrics/position/distribution).

### New: Role management endpoints

You can now manage roles via the API — list the built-in system roles (Admin, Editor, Viewer, Billing) and your organization's custom roles, and create, update, or delete custom roles with per-category permission matrices. Creating custom roles is available on the Enterprise plan. All role endpoints require a **global API key**.

* **GET /api/v1/roles** — List all roles with their resolved permission matrices.
* **POST /api/v1/roles** — Create a custom role (Enterprise plan).
* **GET /api/v1/roles/{role_id}** — Fetch a single role.
* **PATCH /api/v1/roles/{role_id}** — Update a custom role's permissions.
* **DELETE /api/v1/roles/{role_id}** — Delete a custom role (fails if still assigned).

### Changed: Assign any role with `role_id`

Invites and organization member role updates now accept a `role_id` — a system role or one of your organization's custom roles — in addition to the coarse `role` (`admin`/`viewer`), which keeps working unchanged. `role` and `role_id` are mutually exclusive.

Affects **POST /api/v1/invites**, **POST /api/v1/invites/bulk**, and **PATCH /api/v1/organizations/members/{user_id}**.

### Changed: Mention rate and share of voice entries include relative mention rate

The mention rate and share of voice endpoints now return a `relative_mention_rate` field for every brand/competitor entry: the entity's mentions as a percentage (0-100) of responses that mention at least one tracked brand. This is the same "Relative" rate shown in the dashboard's mention rate card. The existing `mention_rate` field stays absolute (share of all responses), and all existing fields are unchanged.

Affects **[POST /api/v1/metrics/mention-rate/cumulative](/api-reference/metrics/cumulative-mention-rate)**, **[POST /api/v1/metrics/mention-rate/time-series](/api-reference/metrics/mention-rate-over-time)**, **[POST /api/v1/metrics/share-of-voice/cumulative](/api-reference/metrics/cumulative-share-of-voice)**, and **[POST /api/v1/metrics/share-of-voice/time-series](/api-reference/metrics/share-of-voice-over-time)** (per entry, per day on the time-series variants). Also returned by the MCP tools `get_mention_rate_cumulative`, `get_mention_rate_time_series`, `get_share_of_voice_cumulative`, and `get_share_of_voice_time_series`.

### New: Trigger response streaming via the API

You can now start a response-streaming run (query the AI models for a website's prompts and ingest the answers) and poll its progress via the API — the same run the dashboard's stream dialog kicks off.

* **[POST /api/v1/response-streaming/start](/api-reference/basics/start-response-streaming)** — Start a run for all active prompts or a selection (up to 1,000), with optional model selection, prompt variations, personas, and locations. Returns the `workflow_id` and `status: "running"`. Requires a website-admin API key.
* **[GET /api/v1/response-streaming/status](/api-reference/basics/get-response-streaming-status)** — Read the latest run's state (`idle` / `running` / `completed` / `failed` / `cancelled`) and queue progress (`pending`, `processing`, `completed`, `failed`, `total`, `percentage`). Pass `workflow_id` to read a specific run.

### New: Look up a user by email

You can now resolve a user's `user_id` from their email address, scoped to your organization. The email is sent in the request body so it never lands in logs.

* **[POST /api/v1/users/by-email](/api-reference/team-management/look-up-user-by-email)** — Returns the user's `user_id`, name, and `access_type` (`organization` or `website`) when the email belongs to your organization; `404` otherwise (indistinguishable from an unknown email, so it cannot be used to enumerate accounts). Requires a global API key.

### New: Remove a user from websites

You can now batch-remove a user's explicit website memberships in one call — the companion to listing a user's websites.

* **[DELETE /api/v1/users/{user_id}/websites](/api-reference/team-management/remove-user-from-websites)** — Remove the user from the websites named in the `website_ids` query parameter (comma-separated, up to 100). Idempotent: websites where the user has no explicit membership return status `not_a_member` instead of failing. Requires a global API key.

### Changed: Filter prompts by tag

[GET /api/v1/prompts](/api-reference/basics/get-prompts) now accepts an optional `prompt_tags` filter (one or more prompt tag IDs) plus a `prompt_tags_operator` (`is_any_of` (default) or `has_all_of`) to control how multiple tags combine. All tag IDs must belong to the same website as `website_id`. Also available on the MCP `get_prompts` tool.

### New: Read a single content item

You can now fetch the full detail of one tracked content item — including its actual text: the content brief (`brief`) and the article/page body (`body`, which holds generated drafts, optimize rewrites, snipe articles, authored text, and scraped tracked-page bodies). The response also carries `status`, so you can poll it while generation runs — `generated_brief` means the brief is ready while the article is still being written.

* **[GET /api/v1/content/{content_id}](/api-reference/content/get-content-detail)** — Full detail for one content item, including `brief`, `body`, cited source URLs, links, and `status`. Also available as the MCP tool `get_content_detail`.

### Changed: `manual` content type filter

The tracked-content endpoint's `content_type` filter now accepts **`manual`** — user-authored content created directly in the editor (the "Blank Editor" flow), as opposed to Athena-generated or imported content. Existing values are unchanged.

Affects **POST /api/v1/content** (`content_type` filter).

## June 2026

### Changed: Member `role` is now the role's slug

Member-list endpoints now return each member's `role` as its **slug** — the role's normalized name (lowercase, no spaces) — rather than only `admin`/`viewer`. Built-in roles are `admin`, `viewer`, `editor`, and `billing`; a custom role uses its own id (e.g. `contentmanager`). `admin` and `viewer` are unchanged, so integrations that check for them keep working; members that previously reported `viewer` because of a non-admin role (Editor, Billing, custom) now surface their real role.

Affects **GET /api/v1/organizations/members**, **GET /api/v1/websites/{website_id}/members**, **GET /api/v1/groups/{group_id}/members**, and **GET /api/v1/users/{user_id}/websites**.

### New: Manage prompts via the API

Prompts are no longer read-only. You can now create, update, pause/unpause, and delete prompts via the API — the same operations the dashboard performs, with the same geography and ownership validation.

* **[POST /api/v1/prompts](/api-reference/basics/create-prompts)** — Create one or more prompts (up to 1,000 per request), with optional topic, geography, persona/location/tag associations, and paused state.
* **[PATCH /api/v1/prompts/{prompt_id}](/api-reference/basics/update-prompt)** — Update a prompt's text, type, volume, topic, geography, or locations. Returns a before/after snapshot.
* **[PATCH /api/v1/prompts/status](/api-reference/basics/pause-or-unpause-prompts)** — Pause or unpause multiple prompts in one transaction.
* **[DELETE /api/v1/prompts/{prompt_id}](/api-reference/basics/delete-prompt)** — Delete a prompt (soft when it has responses, hard otherwise).

### New: Manage locations via the API

You can now create, update, and delete locations via the API. `country` is accepted case-insensitively and stored as its canonical label, and any attached prompts are validated against the location's country.

* **[POST /api/v1/locations](/api-reference/basics/create-location)** — Create a location with a country and optional prompt associations.
* **[PATCH /api/v1/locations/{location_id}](/api-reference/basics/update-location)** — Update a location's name, country, and/or prompt associations. Returns a before/after snapshot.
* **[DELETE /api/v1/locations/{location_id}](/api-reference/basics/delete-location)** — Soft-delete a location.

## May 2026

### New: MCP server for Claude, ChatGPT, and other AI assistants

You can now connect AthenaHQ to Claude, ChatGPT, and other AI assistants over the [Model Context Protocol](/api-reference/mcp). The read-only server exposes your metrics, prompts, responses, content, sources, competitors, and pitches as tools, so an assistant can answer questions about your AI search visibility in plain language.

* Connect **Claude.ai** or **ChatGPT** by adding a custom connector and signing in with your AthenaHQ account (no API key).
* Connect other MCP clients (including **Claude Code**) with an API key via the `x-api-key` header.

Server URL: `https://api.athenahq.ai/api/mcp`.

### New: Sources endpoints

You can now pull cited-sources data via the API (and via MCP — these are the `get_sources` / `get_source_pages` tools). Each row is classified as `owned` / `competitor` / `partner` / `third_party` based on your tracked competitors, configured partners (affiliates), and per-website source-type overrides.

* **[POST /api/v1/sources](/api-reference/sources/list-sources-by-root-domain)** — Domain-grouped sources for a website. Top cited root domains with citation, mention, brand-mention, and impression metrics for the date range. Supports filters, search, sort, and pagination.
* **[POST /api/v1/source-pages](/api-reference/sources/list-source-pages-per-url)** — Per-URL companion to the above. Returns one row per cited `normalized_url` with the same metrics plus a per-URL daily citation sparkline (`daily_mentions`).

### New: Pitch Workspace endpoints

You can now read pitch workspace reports via the API. Pitches are organization-level resources (not scoped to a single website), so these endpoints require a **global API key** — website-scoped keys are rejected with `403 Forbidden`.

* **[GET /api/v1/pitches](/api-reference/pitch-workspace/list-pitches)** — List non-deleted pitches in the calling organization, with company metadata and run status.
* **[GET /api/v1/pitches/{pitch_id}](/api-reference/pitch-workspace/get-pitch-report)** — Fetch a single pitch report: tracked competitors with mention/sentiment stats, generated prompts, positive/negative attributes for both the target and its competitors, top citing domains, and aggregate metrics (`total_responses`, `brand_mentions`, `sentiment_score`, `response_rate`).

### New: Content Hub endpoints

You can now read Content Hub data — tracked URLs, citation/impression metrics, and per-URL prompt breakdowns — via the API. These power GEO reporting that includes both 1st-party (Athena-created) and 3rd-party (link-building placements, Reddit, partner posts) content in one place.

* **[GET /api/v1/content/sheets](/api-reference/content/list-content-hub-sheets)** — List the Content Hub tabs configured for a website. Call this first to discover the `sheet_id`s you can filter by.
* **[POST /api/v1/content](/api-reference/content/list-tracked-content)** — Paginated tracked content with citations, citation %, estimated impressions, and total responses for the date range. Filter by `sheet_id`, `content_type`, `mentioned`, and the standard filter knobs.
* **[POST /api/v1/content/prompts](/api-reference/content/per-content-prompt-citation-breakdown)** — For a single content item, the list of prompts whose AI responses cite it — with per-prompt citation counts and percentages. Works for both topic-attached and 3rd-party content without a topic assignment.

### New: Saved views endpoint

You can now fetch saved views (filter presets) for a website via the API. Saved views capture a named set of dashboard filters that can be reused across the product.

* **[GET /api/v1/saved-views](/api-reference/basics/get-saved-views)** — List saved views for a website. Returns each view's filters, icon, and creator metadata.

### New: Group saved views endpoint

You can now fetch group-level saved views via the API. Group saved views capture filter state shared across the websites in a group rather than scoped to a single website.

* **[GET /api/v1/group-saved-views](/api-reference/basics/get-group-saved-views)** — List saved views for a group. Pass `group_id`; authorization runs against the group's org.

## April 2026

### Updated: Multi-country support on prompts

[GET /api/v1/prompts](/api-reference/basics/get-prompts) now returns a `countries` array on each prompt, listing every country targeted by that prompt along with an `is_primary` flag. The primary country is listed first; remaining countries follow in alphabetical order.

* `countries: [{ country: string, is_primary: boolean }]` — New field. Empty array when the prompt has no countries assigned.
* `country: string` — Deprecated, but still populated for backward compatibility. Returns the primary country (or the alphabetically first, or `"United States"` as a final fallback). New integrations should read `countries` instead.

### New: Delete and restore websites

You can now delete and restore websites via the API. Deleting removes a website from your organization and stops all associated processing; restoring brings it back exactly as it was, with all historical data preserved. This is the recommended flow for partner integrations that need to handle customer churn and reactivation.

* `POST /api/v1/websites/{website_id}/delete` — Deletes a website.
* `POST /api/v1/websites/{website_id}/restore` — Reactivates a deleted website.

Both endpoints require a global API key. Both are idempotent: re-calling them on a website that is already in the target state returns 200.

### Updated: Invite users to groups

[POST /api/v1/invites](/api-reference/team-management/create-invite) now accepts `type: "group"` with a `group_id`, granting the user access to every website in the group. [POST /api/v1/invites/bulk](/api-reference/team-management/bulk-invite-to-websites-or-groups) accepts a new `group_ids` array (mutually exclusive with `website_ids`, max 50) for inviting a user to multiple groups in one request.

Both branches behave consistently with the existing org/website invites: `invite_method: "direct"` adds the user immediately (creating a new user account automatically if the email has none) without sending any email, while `invite_method: "email"` creates a pending invite and sends an invitation. Group invites require a **global API key**. Users who are already members of the organization cannot be invited to individual groups — they already have cascade access to every group.

### New: Team management endpoints

You can now manage team members and pending invites programmatically. These endpoints unblock automated user provisioning, role changes, and cleanup without touching the AthenaHQ dashboard.

**List members and access**

* **[GET /api/v1/organizations/members](/api-reference/team-management/list-organization-members)** — Lists all members of your organization with their email, name, and role. Requires a global API key.
* **[GET /api/v1/websites/{website_id}/members](/api-reference/team-management/list-website-members)** — Lists all users with access to a website, tagged with `access_type` (`organization` for implicit access via org membership, `website` for explicit website membership). Accepts a global or website-scoped API key.
* **[GET /api/v1/users/{user_id}/websites](/api-reference/team-management/list-user-websites)** — Lists the websites a specific user has access to within your organization. Requires a global API key.

**Update and remove members**

* **[PATCH /api/v1/organizations/members/{user_id}](/api-reference/team-management/update-organization-member-role)** — Update an organization member's role. Requires a global API key.
* **[DELETE /api/v1/organizations/members/{user_id}](/api-reference/team-management/remove-organization-member)** — Remove a user from your organization. Also cleans up any stray website-member rows across all websites in the organization, in a single atomic transaction. Requires a global API key.
* **[PATCH /api/v1/websites/{website_id}/members/{user_id}](/api-reference/team-management/update-website-member-role)** — Update a website member's role. Accepts a global or website-scoped API key.
* **[DELETE /api/v1/websites/{website_id}/members/{user_id}](/api-reference/team-management/remove-website-member)** — Remove a user's explicit access to a website. Accepts a global or website-scoped API key.

**Revoke pending invites**

* **[DELETE /api/v1/invites/org/{invite_id}](/api-reference/team-management/revoke-organization-invite)** — Soft-revoke a pending organization invite. Requires a global API key.
* **[DELETE /api/v1/invites/website/{invite_id}](/api-reference/team-management/revoke-website-invite)** — Soft-revoke a pending website invite. Accepts a global or website-scoped API key.

Mutating endpoints include safety guards: you cannot modify the API key's owning user, the organization owner, or the last remaining admin of an organization.

### Updated: Direct invite responses now include `user_id`

[POST /api/v1/invites](/api-reference/team-management/create-invite) and [POST /api/v1/invites/bulk](/api-reference/team-management/bulk-invite-to-websites-or-groups) now return `user_id` in their success responses **for `invite_method: "direct"` only**. In direct mode, the user is looked up or created immediately, so returning the ID gives you everything you need to call the new member-management endpoints without a discovery round-trip. Email-method invites intentionally do not return `user_id` — doing so would let any API key holder enumerate account existence across tenants by email. This change is backward-compatible; existing integrations continue to work unchanged.

### New: Provision Website endpoint (Beta)

You can now create a fully configured website in a single API call, skipping the onboarding wizard. Optionally set up competitors, prompts, and a processing schedule — all in one request. This endpoint is currently in beta — the request and response schemas may change.

* **[POST /api/v1/websites/provision](/api-reference/basics/provision-website)** — Creates a website with optional competitors, prompts, and schedule. Requires a global API key.

### New: Get group credits

You can now retrieve the current credit balance for a group, including the monthly and one-off breakdown.

* **[GET /api/v1/credits/group](/api-reference/billing/get-group-credits)** — Returns the credit balance for a specific group. Requires a global API key.

## March 2026

### New: Get and update individual groups

You can now fetch a single group by ID and update it — rename the group, add websites, or remove websites.

* **[GET /api/v1/groups/{group_id}](/api-reference/groups/get-group)** — Retrieve a single group with its associated websites.
* **[PATCH /api/v1/groups/{group_id}](/api-reference/groups/update-group)** — Rename a group, add websites, and/or remove websites.

Both endpoints require a global API key.

### New: Bulk invite to websites

You can now invite a user to multiple websites in a single API call. The endpoint processes each website independently and returns per-website results, making it easy to onboard users across your organization.

* **[POST /api/v1/invites/bulk](/api-reference/team-management/bulk-invite-to-websites-or-groups)** — Invite a user to up to 50 websites at once. Supports both direct and email invite methods.

## February 2026

### New: Groups endpoints

You can now create and manage groups of websites via the API. Groups let you organize multiple websites under a single entity within your organization.

* **[GET /api/v1/groups](/api-reference/groups/list-groups)** — List all groups for your organization, including associated websites.
* **[POST /api/v1/groups](/api-reference/groups/create-group)** — Create a new group with one or more websites.

Both endpoints require a global API key.
