Tools reference
Every tool the Heading MCP gives a client, with its arguments, what it returns and an example call
The Heading MCP gives every connection the same 52 tools, whichever way it connects. Every tool except list_brands takes a required brandId, from list_brands. Your client may show each tool by its label or by its name; each section below is headed with both.
A read changes nothing, and your client can run it without asking you. A write or a delete is marked so in its listing, and most clients ask you before they run one. A write needs a credential that can write: see Writes and limits.
Lists return up to 1,000 rows a page, 100 when you name no limit, with total and a nextCursor to pass back as cursor. When a call is refused, the answer says why and how to fix it; see Errors.
Brands
Where every conversation starts: the brands this connection can reach, and their settings.
Brands (list_brands)
list_brands. Read. Changes nothing.
The brands this connection can work on, each with its id, name and domains, its profile (summary, positioning, description), the AI platforms it is tracked on, its collection cadence, its primary, measured and target markets, and its prompt quota (used and total). Every other brand tool takes a brandId, and this is where the ids come from: call it first in any conversation that has not already established which brand is in scope. A brand missing from this list cannot be reached through any tool.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with brands, nextCursor, total.
Example call
{
"limit": 100
}Change brand settings (update_brand)
update_brand. Write. Most clients ask you before they run it. Any team member with access to the brand, for some fields; the rest are for team owners and admins.
Changes the brand's settings, exactly as brand settings and the brand profile in the dashboard change them. Send only the fields to change; at least one is needed. name renames the brand, and because Heading finds the brand in answers by its name, every stored answer is checked again under the new name. description is the brand's own one-line description; null clears it. profile.summary (what the business is and who it serves) and profile.positioning (how it wants AI assistants to describe it) replace those profile sections, recorded as the team's own edit. targetMarkets is the COMPLETE list of markets the business says it sells in and replaces the stored list (an empty list clears it): it is a declaration, so it adds no prompt and starts no tracking; a target market with no prompts shows as a coverage gap. trackedPlatforms is the complete set of AI platforms to ask the brand's prompts on, and changes what every visibility figure is measured over from the next collection day; each brand tracks up to its plan's allowance (3 of the 6 unless more were bought), and a set over it is refused with quota_exceeded, naming the allowance, so nothing is changed. Platform changes are recorded in the measurement changelog that list_changes reads. cadence is daily, or weekly on one fixed UTC weekday. Any member with access to the brand can change the profile sections; only team owners and admins can change the name, description, target markets, tracked platforms or cadence, and a member's call that names any of those is refused whole, naming them, so nothing is changed. The change is made in two steps: the settings first (all of them or none), then the profile sections. If the profile save fails after the settings were saved, the answer says so: changed lists only what was saved and notSaved names the profile sections. Call list_brands first to read the current settings; the answer is the brand after the change and which fields moved. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
cadence | one of daily, weekly | No | How often Heading collects answers: daily, or weekly on one fixed UTC weekday. |
description | string or null | No | The brand's own one-line description. null clears it. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
name | string | No | The brand's new name. |
profile | object | No | Profile sections to replace. Each one sent replaces that section whole. |
profile.positioning | string | No | How the brand wants AI assistants to describe it. |
profile.summary | string | No | What the business is and who it serves. |
targetMarkets | list of object | No | The COMPLETE list of markets the business says it sells in. It replaces the stored list; an empty list clears it. |
targetMarkets[].areas | list of string | No | Places INSIDE that market the business named, such as Berlin. Omit when the whole country is the target. Never put a country here. |
targetMarkets[].market | string | Yes | A market tag: a language, a hyphen and a country code, such as en-GB or de-DE. Use the country's main language unless the team says otherwise. |
trackedPlatforms | list of one of chatgpt, perplexity, ai_overview, ai_mode, gemini, copilot | No | The complete set of AI platforms to track, by id as list_brands reports them. It replaces the current set. |
Returns an object with brand, changed, notSaved, replayed.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "settings-2026-11-05",
"name": "Example Ltd",
"profile": {
"positioning": "The accounting CRM built for five person firms."
},
"trackedPlatforms": [
"chatgpt",
"perplexity",
"gemini"
]
}Prompts
The tracked prompts: the questions Heading asks the AI platforms on every collection day.
Tracked prompts (list_prompts)
list_prompts. Read. Changes nothing.
The brand's tracked prompts: the questions Heading asks the AI platforms every collection day. Each row carries the prompt's id, its exact text, its market, its query type (branded or unbranded), its status, its tags, its target page and its figures across every tracked platform on its latest measured UTC day (Mentioned %, mentions, citations, average rank among tracked brands, sentiment, and the day itself), which are the figures the Prompts page shows. An unmeasured prompt states null, never zero. The answer also states how many prompts the brand holds in each status and its prompt quota (used, total and free slots), whatever the filters. Visibility is measured over the active prompts, so this list defines what every figure means. Filter by status, tags, market, query type or text; page with limit and cursor. Use it before adding prompts, to see what is tracked and how many slots are free, and to find the prompt ids other tools take.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
market | string | No | Only prompts asked in this market, as a market tag such as en-GB. Omit for every market. |
queryType | one of all, branded, unbranded | No | branded (prompts that name the brand), unbranded (prompts that do not) or all. Defaults to all. |
search | string | No | Only prompts whose text contains this, ignoring case. |
status | one of active, inactive, suggested | No | Only prompts in this status: active (tracked), inactive (paused) or suggested. Omit for every status. |
tagIds | list of string | No | Only prompts carrying at least one of these tags, by id. A tag id the brand does not hold answers not_found. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with counts, prompts, quota, nextCursor, total.
Example call
{
"brandId": "brand_1",
"limit": 100,
"market": "en-GB",
"queryType": "unbranded",
"search": "pricing",
"status": "active",
"tagIds": [
"tag_1"
]
}Add tracked prompts (create_prompts)
create_prompts. Write. Most clients ask you before they run it. Any team member with access to the brand.
Adds up to 200 prompts to the brand's tracked portfolio in one call. Each prompt starts active: Heading asks it on every tracked platform from the brand's next collection day (today, when today is one), and it counts in every visibility figure from then on, so this changes what the figures measure. Give each prompt's exact text as a person would ask it, and optionally its market (defaults to the brand's primary market), its query type (otherwise decided from whether the text names the brand), tag ids from list_prompts or the brand's tag list, and a target page on the brand's own site. The batch is all or nothing against the prompt quota: if the new prompts do not all fit, nothing is added and the answer is quota_exceeded, stating how many were asked and how many slots are free, so you choose what to drop. A prompt the brand already tracks in that market is reported under alreadyTracked and takes no slot; a prompt the brand removed earlier comes back with its history. Every addition is recorded in the prompt changelog under the person the call acts as. Send an idempotencyKey to make a retry safe: the same key with the same arguments writes once and returns the first answer. Call list_prompts first to see the quota and what is tracked.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
prompts | list of object | Yes | The prompts to add, in any order. |
prompts[].market | string | No | The market to ask it in, as a market tag such as en-GB. Defaults to the brand's primary market. |
prompts[].queryType | one of branded, unbranded | No | branded when the prompt names the brand, otherwise unbranded. Omit to decide from the text. |
prompts[].tagIds | list of string | No | Tags to put on the prompt, by id. A tag id the brand does not hold refuses the whole call. |
prompts[].targetPage | string | No | The page on the brand's own site the team wants cited for this prompt. |
prompts[].text | string | Yes | The prompt, exactly as a person would ask it. |
Returns an object with alreadyTracked, created, quota, replayed.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "import-2026-11-05-1",
"prompts": [
{
"market": "en-GB",
"queryType": "unbranded",
"tagIds": [
"tag_1"
],
"targetPage": "https://example.com/pricing",
"text": "which crm is cheapest for a five person team"
},
{
"market": "de-DE",
"text": "bestes crm fuer kleine kanzleien"
}
]
}Change tracked prompts (update_prompts)
update_prompts. Write. Most clients ask you before they run it. Any team member with access to the brand.
Changes up to 200 of the brand's prompts in one call, by the ids list_prompts returns. For each prompt you may pause it or resume it (status paused or active; active also starts tracking a suggested prompt), replace its tags (tagIds) or add and remove some (addTagIds, removeTagIds), set its query type (branded or unbranded), and set or clear its target page (a page on the brand's own site, or null). Changing the text or the market does not move the prompt: a market is part of a prompt's identity, so Heading re-creates it as a new prompt with the new text or market, carries its tags, query type and target page across, and removes the old one, whose past answers stay in every figure; the answer gives the new id. Pausing takes a prompt out of the measured set from that day and resuming puts it back, so status and text edits change what the figures measure. Each prompt answers on its own: updated, unchanged (it already held those values) or failed with its own reason, and a failed prompt writes nothing. The quota rule: every prompt this call would start counting (a paused or suggested prompt set to active) needs a free slot. Pauses in the same call run first and free their slots; if the rest still do not fit, nothing in the call is written and the answer is quota_exceeded with the counts, so you choose what to leave out. Every status change, re-creation and removal is recorded in the prompt changelog under the person the call acts as. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
prompts | list of object | Yes | The prompts to change, each with only the fields to change. Each prompt id appears once. |
prompts[].addTagIds | list of string | No | Tags to add to the prompt, by id; tags it already carries are kept. Not with tagIds. |
prompts[].market | string | No | A new market, as a market tag such as en-GB. Re-creates the prompt in that market; the old prompt is removed with its history kept. |
prompts[].promptId | string | Yes | The prompt to change, by the id list_prompts returns. |
prompts[].queryType | one of branded, unbranded | No | branded when the prompt names the brand, otherwise unbranded. Recent days' branded and unbranded figures are recalculated. |
prompts[].removeTagIds | list of string | No | Tags to take off the prompt, by id. Not with tagIds. |
prompts[].status | one of active, paused | No | active tracks the prompt (a paused one resumes, a suggested one starts); paused stops tracking it. Paused is the status list_prompts shows as inactive. |
prompts[].tagIds | list of string | No | The prompt's whole tag set, by id, replacing the tags it carries; an empty list clears them. Not with addTagIds or removeTagIds. |
prompts[].targetPage | string or null | No | The page on the brand's own site the team wants cited for this prompt; null clears it. |
prompts[].text | string | No | New wording, exactly as a person would ask it. Re-creates the prompt with this text; the old prompt is removed with its history kept. |
Returns an object with quota, replayed, results.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "cleanup-2026-11-05",
"prompts": [
{
"addTagIds": [
"tag_1"
],
"promptId": "prompt_1",
"queryType": "unbranded",
"status": "active",
"targetPage": "https://example.com/pricing"
},
{
"market": "de-DE",
"promptId": "prompt_2",
"text": "bestes crm"
},
{
"promptId": "prompt_3",
"tagIds": [],
"targetPage": null
}
]
}Remove tracked prompts (delete_prompts)
delete_prompts. Delete. Most clients ask you before they run it. Any team member with access to the brand.
Removes up to 200 prompts from the brand's portfolio, by the ids list_prompts returns, as the Prompts page's remove does. A prompt that was ever measured is soft-deleted: it leaves the list, the quota and every figure from today, and its past answers stay in every past figure, so nothing already measured is rewritten. A suggested prompt that was never measured has no history and is deleted outright. An active prompt may be removed; it stops counting from today. Each prompt answers on its own: soft_deleted, deleted, not_found (no live prompt with that id on this brand, including one removed before), or duplicate (the id was already named earlier in the call). A prompt removed by mistake comes back with its history when create_prompts adds the same text in the same market. Every removal is recorded in the prompt changelog under the person the call acts as. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
promptIds | list of string | Yes | The prompts to remove, by the ids list_prompts returns. |
Returns an object with quota, replayed, results.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "cleanup-2026-11-05",
"promptIds": [
"prompt_1",
"prompt_2"
]
}Tags
The brand's tags and tag folders, which group its prompts.
Tags and tag folders (list_tags)
list_tags. Read. Changes nothing.
The brand's tags and tag folders, as the dashboard's tag manager shows them. A tag is a label on prompts, used to segment the portfolio (by topic, product line, funnel stage); a prompt can carry several. Each tag row carries its id, its name, its folder (or null), how many of the brand's prompts carry it and whether it is a system tag. Every folder is listed in the answer whatever the page, with its order and how many tags it holds. Tags come in folder order, then by name, with tags in no folder last. Filter by folder or by text in the name; page with limit and cursor. Use it to find the tag ids that list_prompts, create_prompts and saved views take, and before creating tags so a name is not repeated.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
folderId | string | No | Only tags in this folder, by id. A folder id the brand does not hold answers not_found. |
search | string | No | Only tags whose name contains this, ignoring case. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with folders, tags, nextCursor, total.
Example call
{
"brandId": "brand_1",
"folderId": "folder_1",
"limit": 100,
"search": "pricing"
}Create tags (create_tags)
create_tags. Write. Most clients ask you before they run it. Team owners and admins only.
Creates up to 100 tags on the brand in one call, each optionally inside a folder and with a colour and a description. A new tag carries no prompts; put it on prompts with create_prompts, or in the dashboard. Tag names are unique per brand ignoring case: a name the brand already uses, or one repeated in the call, refuses the whole call with conflict and nothing is created. A folder id the brand does not hold refuses it with not_found. Only team owners and admins can create tags, as in the dashboard. Send an idempotencyKey to make a retry safe. Call list_tags first to see the names and folders in use.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
tags | list of object | Yes | The tags to create. |
tags[].color | string | No | The tag's colour in the dashboard, as #rrggbb such as #6e79d6. Omit for the default. |
tags[].description | string | No | What the tag is for, up to 500 characters. |
tags[].folderId | string | No | The folder to put the tag in, by the id list_tags returns. Omit for no folder. |
tags[].name | string | Yes | The tag's name, unique on the brand ignoring case. |
Returns an object with created, replayed.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "tags-2026-11-05",
"tags": [
{
"color": "#46a758",
"folderId": "folder_1",
"name": "Payroll"
},
{
"description": "Invoices and receipts",
"folderId": "folder_1",
"name": "Invoicing"
}
]
}Change tags (update_tags)
update_tags. Write. Most clients ask you before they run it. Team owners and admins only.
Renames tags, moves them between folders, or changes their colour or description, for up to 100 tags in one call. Only the fields you send change. Every prompt keeps the tags it carries: a renamed tag is the same tag. Pass folderId null to take a tag out of its folder, and color or description null to clear it. A system tag cannot be renamed or moved (forbidden), though its colour and description can change; a new name the brand already uses, or one given twice in the call, is a conflict; a tag or folder id the brand does not hold is not_found. Any fault refuses the whole call and nothing changes. Sending a tag's current values changes nothing and is safe to repeat. Only team owners and admins can change tags, as in the dashboard. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
tags | list of object | Yes | The tags to change, each named by id. |
tags[].color | string or null | No | The tag's new colour as #rrggbb; null for the default. Omit to keep it. |
tags[].description | string or null | No | The tag's new description; null to clear it. Omit to keep it. |
tags[].folderId | string or null | No | The folder to move the tag into, by id; null takes it out of any folder. Omit to leave it where it is. |
tags[].name | string | No | The tag's new name. Omit to keep its name. |
tags[].tagId | string | Yes | The tag to change, by the id list_tags returns. |
Returns an object with replayed, updated.
Example call
{
"brandId": "brand_1",
"tags": [
{
"folderId": "folder_2",
"tagId": "tag_1"
},
{
"folderId": null,
"tagId": "tag_2"
},
{
"color": "#6e79d6",
"description": "Pricing questions",
"tagId": "tag_3"
}
]
}Delete tags (delete_tags)
delete_tags. Delete. Most clients ask you before they run it. Team owners and admins only.
Deletes up to 100 tags. The prompts that carried a tag keep being tracked and lose only that tag; the answer states, per tag, how many prompts lost it and how many saved views named it (a saved view skips a tag that no longer exists). This cannot be undone. A system tag cannot be deleted (forbidden), and a tag id the brand does not hold is not_found; either refuses the whole call and nothing is deleted. Only team owners and admins can delete tags, as in the dashboard. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
tagIds | list of string | Yes | The tags to delete, by the ids list_tags returns. |
Returns an object with deleted, replayed.
Example call
{
"brandId": "brand_1",
"tagIds": [
"tag_1",
"tag_2"
]
}Create tag folders (create_tag_folders)
create_tag_folders. Write. Most clients ask you before they run it. Team owners and admins only.
Creates up to 100 tag folders on the brand in one call. A folder groups tags: inside one folder, filtering by two of its tags matches prompts carrying either; across folders, both must match. A new folder is empty and sorts after the existing ones unless you give an order. Folder names are unique per brand ignoring case: a name in use, or one repeated in the call, refuses the whole call with conflict and nothing is created. Only team owners and admins can create folders, as in the dashboard. Send an idempotencyKey to make a retry safe. Put tags in a folder with create_tags or update_tags.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
folders | list of object | Yes | The folders to create. |
folders[].name | string | Yes | The folder's name, unique on the brand ignoring case. |
folders[].order | integer | No | The folder's place in the list, lowest first. Omit to put it after the folders already there. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with created, replayed.
Example call
{
"brandId": "brand_1",
"folders": [
{
"name": "Funnel stage",
"order": 0
},
{
"name": "Regions",
"order": 1
}
],
"idempotencyKey": "folders-2026-11-05"
}Rename or reorder tag folders (update_tag_folders)
update_tag_folders. Write. Most clients ask you before they run it. Team owners and admins only.
Renames or reorders up to 100 tag folders in one call. The tags inside a folder stay in it. A system folder can be reordered but not renamed (forbidden); a new name the brand already uses, or one given twice in the call, is a conflict; a folder id the brand does not hold is not_found. Any fault refuses the whole call and nothing changes. Only team owners and admins can change folders, as in the dashboard. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
folders | list of object | Yes | The folders to change, each named by id. |
folders[].folderId | string | Yes | The folder to change, by the id list_tags returns. |
folders[].name | string | No | The folder's new name. Omit to keep its name. |
folders[].order | integer | No | The folder's new place in the list, lowest first. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with replayed, updated.
Example call
{
"brandId": "brand_1",
"folders": [
{
"folderId": "folder_1",
"name": "Products",
"order": 2
}
]
}Delete tag folders (delete_tag_folders)
delete_tag_folders. Delete. Most clients ask you before they run it. Team owners and admins only.
Deletes up to 100 tag folders. The tags inside a folder are not deleted: they stay on their prompts and move to no folder, and the answer names them. This cannot be undone. A system folder cannot be deleted (forbidden), and a folder id the brand does not hold is not_found; either refuses the whole call and nothing is deleted. Only team owners and admins can delete folders, as in the dashboard. To delete the tags too, call delete_tags. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
folderIds | list of string | Yes | The folders to delete, by the ids list_tags returns. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with deleted, replayed.
Example call
{
"brandId": "brand_1",
"folderIds": [
"folder_1"
]
}Saved views
Saved views: a market, tags and a query type saved under a name, applied to the visibility report.
Saved views (list_views)
list_views. Read. Changes nothing.
The brand's saved views: named filters the team set up in the dashboard to focus on one segment of the portfolio, such as one market and a few tags. Each view carries its id, its name and its filters exactly as get_visibility_report takes them (market, tagIds, queryType: spread them into its arguments), with the market and tags also named and how the view matches its tags. A view saved with no query type reads all prompts, and its filters say queryType all. The report matches tags by folder; a view whose several tags match by any or all reads a different set, and reportMatchesView says so. The filters are the view as the dashboard applies it: a tag the brand has since deleted is listed under missingTagIds and left out, a market with no tracked prompts is left out (market.measured false), and a view with nothing left says so in reportNote. Views come by name. Use it to report on the segments the team already uses, or before creating a view so a name is not repeated.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with views, nextCursor, total.
Example call
{
"brandId": "brand_1",
"limit": 100
}Create saved views (create_views)
create_views. Write. Most clients ask you before they run it. Team owners and admins only.
Creates up to 50 saved views on the brand in one call, each a name plus a market, tags and a query type. The team sees them in the dashboard's Views menu on the visibility and prompts pages. A view must narrow something: a market, at least one tag, or a query type other than all. View names are unique per brand ignoring case: a name in use, or one repeated in the call, refuses the whole call with conflict and nothing is created. A tag id the brand does not hold is not_found; a market Heading does not measure is invalid_input. Only team owners and admins can create views, as in the dashboard. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
views | list of object | Yes | The views to create. |
views[].market | string | No | The market to narrow to, as a market tag such as en-GB. Omit for every market. |
views[].name | string | Yes | The view's name, unique on the brand ignoring case. |
views[].queryType | one of all, branded, unbranded | No | branded, unbranded or all. Defaults to all. |
views[].tagIds | list of string | No | Tags to narrow to, by the ids list_tags returns; at most 30. |
views[].tagMode | one of facets, any, all | No | How the tags match: facets (the default) matches any tag inside one folder and needs every folder; any matches prompts carrying at least one of the tags; all needs every tag. |
Returns an object with created, replayed.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "views-2026-11-05",
"views": [
{
"name": "Branded",
"queryType": "branded"
},
{
"name": "Payroll or invoicing",
"tagIds": [
"tag_1",
"tag_2"
],
"tagMode": "any"
}
]
}Change saved views (update_views)
update_views. Write. Most clients ask you before they run it. Team owners and admins only.
Renames saved views or changes their filters, for up to 50 views in one call. Only the fields you send change; tagIds replaces the view's whole tag list. Pass market null to drop the market filter. The view must still narrow something afterwards. A new name in use, or given twice in the call, is a conflict; a view or tag id the brand does not hold is not_found. Any fault refuses the whole call and nothing changes. Sending a view's current values changes nothing and is safe to repeat. Only team owners and admins can change views, as in the dashboard. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
views | list of object | Yes | The views to change, each named by id. |
views[].market | string or null | No | The market to narrow to, as a market tag; null for every market. Omit to keep it. |
views[].name | string | No | The view's new name. Omit to keep it. |
views[].queryType | one of all, branded, unbranded | No | branded, unbranded or all. Omit to keep it. |
views[].tagIds | list of string | No | The view's new tag list, replacing the old one; empty for no tag filter. Omit to keep it. |
views[].tagMode | one of facets, any, all | No | How the tags match: facets (the default) matches any tag inside one folder and needs every folder; any matches prompts carrying at least one of the tags; all needs every tag. Omit to keep it. |
views[].viewId | string | Yes | The view to change, by the id list_views returns. |
Returns an object with replayed, updated.
Example call
{
"brandId": "brand_1",
"views": [
{
"market": null,
"queryType": "unbranded",
"tagIds": [],
"viewId": "view_2"
}
]
}Delete saved views (delete_views)
delete_views. Delete. Most clients ask you before they run it. Team owners and admins only.
Deletes up to 50 saved views. A view is only a saved filter: deleting it changes no prompt, tag or figure, and the answer names each view with the filters it held. This cannot be undone. A view id the brand does not hold is not_found and refuses the whole call. Only team owners and admins can delete views, as in the dashboard. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
viewIds | list of string | Yes | The views to delete, by the ids list_views returns. |
Returns an object with deleted, replayed.
Example call
{
"brandId": "brand_1",
"viewIds": [
"view_1"
]
}Competitors
The competitors the brand is compared with, and the candidates waiting on a decision.
Tracked competitors (list_competitors)
list_competitors. Read. Changes nothing.
The brand's tracked competitors and the competitor candidates waiting on a decision. Each competitor carries its id, its name, the tracked name answers are matched on, its aliases, its own domains, when it started being tracked, and whether it is tracked for this brand or for every brand in the team. Every comparison figure (Mention share, the leaderboard, rank among tracked brands) is measured over this set, so it says what those figures mean. Each candidate is a business the competitor passes found in the brand's answers or market research: its id, name, domain, the check's verdict, confidence and evidence, where it came from, and how many answers named or cited it. Candidates the team rejected or the check judged not a competitor are left out unless includeNotCompetitors is true. This tool carries no visibility figures: for each competitor's share of answers and rank, call get_visibility_report with breakdown brand; for the answers that name it, list_answers with its id in competitorIds. Pages over the tracked competitors with limit and cursor; the candidates come whole on every page.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
includeNotCompetitors | boolean | No | When true, also list candidates the team rejected or the check judged not a direct competitor, so one can be accepted anyway. Defaults to false. |
search | string | No | Only competitors and candidates whose name, tracked name, alias or domain contains this, ignoring case. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with candidateCounts, candidates, competitors, nextCursor, total.
Example call
{
"brandId": "brand_1",
"includeNotCompetitors": true,
"limit": 100,
"search": "acme"
}Add tracked competitors (create_competitors)
create_competitors. Write. Most clients ask you before they run it. Team owners and admins only.
Starts tracking up to 50 competitors for the brand in one call, as the dashboard's Add competitor does. Give each one's name, and optionally its own domain and the other names AI answers use for it. From then on Heading finds each one in the brand's answers: every comparison figure (Mention share, the leaderboard, rank among tracked brands) is measured over the tracked set, so adding competitors changes what those figures measure, and each addition is recorded as a measurement change that list_changes reports. Answers collected from then on are matched against each new competitor, and once the brand's first collection day has landed, that day's answers are matched at once as well. A name the brand already tracks, ignoring case, is listed under alreadyTracked and not changed. A candidate with the same name or domain is marked accepted. Only team owners and admins may add competitors. Send an idempotencyKey to make a retry safe. Call list_competitors first to see what is tracked and which candidates are waiting.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
competitors | list of object | Yes | The competitors to start tracking. |
competitors[].aliases | list of string | No | Other names AI answers use for the competitor, such as a short form or an old name, at most 10. Each is matched word for word, ignoring case. No patterns. |
competitors[].domain | string | No | A domain of the competitor's own, bare, such as example.com: no scheme, no path. An answer citing it counts as citing the competitor. |
competitors[].name | string | Yes | The competitor's name, as it writes it. Heading matches answers on it, word for word, ignoring case. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with alreadyTracked, created, notAdded, replayed.
Example call
{
"brandId": "brand_1",
"competitors": [
{
"aliases": [
"Acme Ltd"
],
"domain": "acme.com",
"name": "Acme"
},
{
"domain": "globex.co.uk",
"name": "Globex"
}
],
"idempotencyKey": "competitors-2026-11-05"
}Change tracked competitors (update_competitors)
update_competitors. Write. Most clients ask you before they run it. Team owners and admins only.
Changes up to 50 tracked competitors in one call, as the dashboard's competitor edit does: the name shown, the tracked name answers are matched on, the aliases and the domains. A field left out is not changed; aliases and domains replace the whole list. A changed tracked name, alias or domain changes which answers count as naming or citing the competitor from the next collected answer; answers already collected keep the match they were given. A tracked name another competitor of the brand already uses, ignoring case, is refused. Every competitor is checked before anything is written: an id the brand does not track, or one tracked for every brand in the team, refuses the whole call. Only team owners and admins may change competitors. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
competitors | list of object | Yes | The competitors to change, each with the fields to change. |
competitors[].aliases | list of string | No | The whole alias list, replacing the current one, at most 10. Send [] to clear it. Omit to keep it. |
competitors[].competitorId | string | Yes | The competitor to change, by the id list_competitors returns. |
competitors[].domains | list of string | No | The whole domain list, replacing the current one, bare, such as example.com. Send [] to clear it. Omit to keep it. |
competitors[].name | string | No | The name the dashboard shows. Omit to keep it. |
competitors[].trackedName | string | No | The name answers are matched on, word for word, ignoring case. Omit to keep it. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with replayed, updated.
Example call
{
"brandId": "brand_1",
"competitors": [
{
"competitorId": "comp_1",
"domains": [
"acme.com",
"acme.co.uk"
],
"name": "Acme",
"trackedName": "Acme Inc"
}
],
"idempotencyKey": "rename-acme-1"
}Stop tracking competitors (delete_competitors)
delete_competitors. Delete. Most clients ask you before they run it. Team owners and admins only.
Stops tracking up to 50 competitors of the brand in one call, as the dashboard's competitor removal does. This deletes each competitor and every record of it in the brand's collected answers (its mentions, ranks and daily figures), so its history cannot be read again, even if it is added back. Every comparison figure is measured over the tracked set, so the removal is recorded as a measurement change that list_changes reports, and its candidate is marked rejected so the competitor passes do not propose it again. Every id is checked before anything is removed: an id the brand does not track, or a competitor tracked for every brand in the team, refuses the whole call. Only team owners and admins may remove competitors. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
competitorIds | list of string | Yes | The competitors to stop tracking, by the ids list_competitors returns. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with deleted, replayed.
Example call
{
"brandId": "brand_1",
"competitorIds": [
"comp_1",
"comp_2"
],
"idempotencyKey": "cleanup-2026-11-05"
}Decide competitor candidates (update_competitor_candidates)
update_competitor_candidates. Write. Most clients ask you before they run it. Team owners and admins only.
Accepts or rejects up to 50 competitor candidates in one call, as the dashboard's candidate review does. Accepting starts tracking the candidate as a competitor, under the name the check found or the name you give, with its domain and aliases; it can accept a candidate that was rejected or judged not a competitor. Accepting changes what every comparison figure measures and is recorded as a measurement change that list_changes reports. Rejecting records the team's decision, tracks nothing and keeps the competitor passes from proposing it again. A candidate already tracked is listed under alreadyTracked; to stop tracking one, use delete_competitors. A candidate whose name the brand already tracks under another competitor is listed under refused, with the reason. Every id is checked before anything is written: an id the brand does not hold refuses the whole call. Only team owners and admins may decide candidates. Send an idempotencyKey to make a retry safe. Take the ids from list_competitors.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
accept | list of object | No | Candidates to start tracking as competitors. |
accept[].aliases | list of string | No | The aliases to track it with, replacing the check's, at most 10. Omit to keep the check's. |
accept[].candidateId | string | Yes | The candidate to accept, by the id list_competitors returns. |
accept[].name | string | No | The name to track it under. Omit for the name the check found; a candidate with no name needs one. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
reject | list of string | No | Candidates to reject, by id. |
Returns an object with accepted, alreadyTracked, refused, rejected, replayed.
Example call
{
"accept": [
{
"aliases": [
"Acme Ltd"
],
"candidateId": "cand_1",
"name": "Acme"
}
],
"brandId": "brand_1",
"idempotencyKey": "review-2026-11-05",
"reject": [
"cand_2",
"cand_3"
]
}Reports
The measured figures, computed by the same code as the dashboard pages. See Metrics for what each figure means.
Visibility report (get_visibility_report)
get_visibility_report. Read. Changes nothing.
The brand's visibility in AI answers, as the visibility page measures it, over a UTC day range: Mentioned %, Cited %, Mention share, Citation share, average mentions, average citations, responses, average rank among tracked brands and average sentiment. One call answers one question: no breakdown gives a summary; breakdown date gives a series; breakdown brand gives the leaderboard of the brand and its tracked competitors; platform, prompt, tag and market split the figures; up to three combine (such as brand and platform). Filter by tags (OR within a tag folder, AND across folders, as the page does), market, query type, platforms and prompts. Set compare for the previous period of the same length: every row then carries its previous figures and its change. Defaults: the last 28 days and unbranded prompts, which is what the visibility page opens on. A figure Heading did not measure is null with a reason, never zero. A range that opens before daily tracking began is clamped, and notes say so; a brand on no paid plan has one as-of day.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
startDay | string | No | First UTC day of the range (inclusive), YYYY-MM-DD. Defaults to 27 days before endDay. A day before daily tracking began is moved to that day, with a note; a day after the range's last day answers invalid_input. |
endDay | string | No | Last UTC day of the range (inclusive), YYYY-MM-DD. Defaults to, and may not pass, the last day the brand can be measured on: today for a brand on a paid plan, its as-of day when it has never paid, its last collected day when its plan lapsed. |
breakdown | list of one of date, platform, prompt, tag, market, brand | No | Split the figures by date, platform, prompt, tag, market or brand, alone or up to three combined, in nesting order. Omit for the summary alone. brand lists the brand and every tracked competitor (the leaderboard). date cannot combine with compare, and is refused for a brand with one as-of day. A breakdown that would make more than 20,000 rows is refused: narrow the range, add a filter or drop a breakdown. |
interval | one of day, week, month | No | With a date breakdown: day (default), week (Monday to Sunday) or month. |
queryType | one of all, branded, unbranded | No | unbranded (prompts that do not name the brand, the default and what the visibility page opens on), branded, or all. |
tagIds | list of string | No | Only prompts carrying these tags, by id: any of the tags in one folder, and every folder named. A tag id the brand does not hold answers not_found. |
market | string | No | Only prompts asked in this market, as a market tag such as en-GB. Omit for every market. |
platforms | list of string | No | Only answers from these AI platforms, by id (chatgpt, perplexity, ai_overview, ai_mode, gemini, copilot, claude, gpt_search). Omit for every platform. |
promptIds | list of string | No | Only these tracked prompts, by the ids list_prompts returns. A prompt that is not active answers not_found: figures are measured over active prompts. |
compare | boolean | No | true to measure the comparison period too, so every row carries previous and change. Off by default. |
comparePeriod | one of prev_period, prev_month, prev_quarter | No | The comparison period, always as many days as the range: prev_period (the days just before the range, the default and the page's comparison), prev_month or prev_quarter (the same days one or three months back; one that would overlap the range falls back to prev_period, with a note). Implies compare. |
sortBy | one of mentionedPct, citedPct, mentionShare, citationShare, avgMentions, avgCitations, responses, averageRank, avgSentiment, name | No | Sort rows by this metric, highest first (averageRank lowest first), or by name. Defaults to mentionedPct. With a date breakdown, rows are in date order first. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with breakdown, comparison, filters, interval, notes, range, rows, summary, nextCursor, total.
Example call
{
"brandId": "brand_1",
"breakdown": [
"brand",
"platform"
],
"comparePeriod": "prev_month",
"limit": 100,
"market": "en-GB",
"platforms": [
"chatgpt",
"perplexity"
],
"promptIds": [
"prompt_1"
],
"tagIds": [
"tag_1"
]
}Objective measure (get_objective_measure)
get_objective_measure. Read. Changes nothing.
The figure a metric key result is judged on, exactly as the objective page charts it: visibility, the share of one UTC day's tracked answers that mention the brand, for one scope (query type, and optionally a market and tags), on the latest measured day by the page's own rule (for a brand whose plan lapsed, its last collected day). Read it to set a baseline before create_objectives or update_objectives: write its reading's value and day into measureConfig.baseline and its scope into measureConfig.scope, unchanged, so the key result is judged on the same measure over the same prompts. Read it again with a key result's own scope and its baseline day as sinceDay to verify that key result: the reading is then the key result's current figure on the objective page. It is a different figure from get_visibility_report, whose range figures pool many days; never compare one with the other. A scope with no measured day answers a null reading with a reason, never zero.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
queryType | one of all, branded, unbranded | Yes | Which prompts: unbranded (prompts that do not name the brand), branded, or all. Required, so the scope a key result is judged on is always stated. |
market | string | No | Only prompts asked in this market, as a market tag such as en-GB. Omit for every market. |
tagIds | list of string | No | Only prompts carrying these tags, by id: any of the tags in one folder, and every folder named. |
sinceDay | string | No | When verifying, the key result's baseline day (YYYY-MM-DD): only days from it on count, as on the objective page, so no day measured since answers a null reading. Omit when setting a baseline. |
Returns an object with measure, reading, reason, scope.
Example call
{
"brandId": "brand_1",
"market": "en-GB",
"queryType": "all",
"sinceDay": "2026-10-01",
"tagIds": [
"3f0c8a8e-2d4b-4c5e-9a7b-1e2f3a4b5c6d"
]
}Perception report (get_perception_report)
get_perception_report. Read. Changes nothing.
How AI depicts the brand, as the Perception page measures it, over a UTC day range. Perception is measured on dimensions: ten spine qualities every brand shares plus qualities generated for the brand's category, and every tracked brand is judged on the same dimensions, so a dimension a rival holds and the brand does not is visible. The summary is the brand's own: overall sentiment, focused dimensions led and the strongest quality. breakdown dimension lists every dimension with the brand's rate, the best compared competitor's rate and the gap, widest deficit first, as the page ranks them; breakdown brand lists the brand and every tracked competitor with their own sentiment and focus figures (the page's brand switcher); both together give each brand's rate on each dimension. Focused dimensions are flagged, never filtered: every dimension is readable. Retired dimensions are listed with the day they retired and measured over the days before it. A brand with fewer than 10 assessed answers reads null with the reason not_measured, never 0%. Filter by market, query type, platforms, tags and prompts; set compare for the previous period of the same length. Defaults: the last 28 days and unbranded prompts, as the page opens. It answers what AI says about the brand; get_visibility_report answers how often AI names it.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
startDay | string | No | First UTC day of the range (inclusive), YYYY-MM-DD. Defaults to 27 days before endDay. A day before daily tracking began is moved to that day, with a note. |
endDay | string | No | Last UTC day of the range (inclusive), YYYY-MM-DD. Defaults to, and may not pass, the last day the brand can be measured on: today on a paid plan, its as-of day when it has never paid, its last collected day when its plan lapsed. |
breakdown | list of one of dimension, brand | No | dimension, brand, or both in nesting order. Omit for the summary alone. brand lists the brand and up to 49 tracked competitors, in tracked order; a note says when more were left out. |
queryType | one of all, branded, unbranded | No | unbranded (prompts that do not name the brand, the default and what the page opens on), branded, or all. |
tagIds | list of string | No | Only prompts carrying these tags, by id: any of the tags in one folder, and every folder named. A tag id the brand does not hold answers not_found. |
market | string | No | Only prompts asked in this market, as a market tag such as en-GB. Omit for every market. |
platforms | list of string | No | Only answers from these AI platforms, by id (chatgpt, perplexity, ai_overview, ai_mode, gemini, copilot, claude, gpt_search). Omit for every platform. |
promptIds | list of string | No | Only these tracked prompts, by the ids list_prompts returns. A prompt that is not active answers not_found. |
compare | boolean | No | true to measure the comparison period too, so every row carries previous and change. Off by default. |
comparePeriod | one of prev_period, prev_month, prev_quarter | No | prev_period (the days just before the range, the default), prev_month or prev_quarter (the same days one or three months back; one that would overlap the range falls back to prev_period, with a note). Implies compare. |
sortBy | one of gap, rate, sentiment, name | No | gap (the default, the page's order: dimensions widest deficit first, retired last; brands the brand first, then competitors in tracked order), rate (dimensions by rate, highest first), sentiment (brands by sentimentPct, highest first) or name. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with breakdown, comparison, dimensions, filters, notes, range, rows, summary, nextCursor, total.
Example call
{
"brandId": "brand_1",
"breakdown": [
"dimension"
],
"comparePeriod": "prev_month",
"limit": 100,
"market": "en-GB",
"platforms": [
"chatgpt",
"perplexity"
],
"promptIds": [
"prompt_1"
],
"queryType": "all",
"sortBy": "rate",
"tagIds": [
"tag_1"
]
}Sources report (get_sources_report)
get_sources_report. Read. Changes nothing.
The sources AI platforms cite in their answers to the brand's tracked prompts, as the Sources page measures them, over a UTC day range. level domain (the default) lists cited domains with Used % and average citations; level url lists cited pages with Citation % and Retrieval %. Every row says whether the domain is the brand's own, and carries two flags over the whole range: gap (cited in answers that name a tracked competitor and in none that name the brand, over this report's own answers: co-citation in answers, never a claim about what the source's pages say, and not the Sources page's Gaps tab figure) and rising (its citation rate rose against the period just before). Filter by tags (OR within a tag folder, AND across folders), market and query type; at level url also by one platform and page format. Break down by date (domains by month; pages by day, week or month) or, at level url, by each platform the brand tracks. Set compare for the previous period of the same length. Defaults: the last 28 days and unbranded prompts, as the Sources page opens. A cited source is a page an AI answer named as its source: not a backlink, not traffic. A figure Heading did not measure is null with a reason, never zero; read the notes before stating a figure.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
level | one of domain, url | No | domain (the default) for one row per cited domain, url for one row per cited page. |
startDay | string | No | First UTC day of the range (inclusive), YYYY-MM-DD. Defaults to 27 days before endDay. A day before daily tracking began is moved to that day, with a note. |
endDay | string | No | Last UTC day of the range (inclusive), YYYY-MM-DD. Defaults to, and may not pass, the last day the brand can be measured on: today for a brand on a paid plan, its as-of day when it has never paid, its last collected day when its plan lapsed. |
breakdown | list of one of date, platform | No | Split the rows by date, or at level url by each platform the brand tracks, or both, in nesting order. date cannot combine with compare, and is refused for a brand with one as-of day. A breakdown that would read more than 31 periods and platforms is refused: narrow the range or use a longer interval. |
interval | one of day, week, month | No | With a date breakdown: month (the default), week (Monday to Sunday) or day. Domains are read by month only: their figures are kept per month. |
queryType | one of all, branded, unbranded | No | unbranded (prompts that do not name the brand, the default and what the Sources page opens on), branded, or all. |
tagIds | list of string | No | Only prompts carrying these tags, by id: any of the tags in one folder, and every folder named. A tag id the brand does not hold answers not_found. |
market | string | No | Only prompts asked in this market, as a market tag such as en-GB. Omit for every market. |
platform | string | No | Level url only: only answers from this AI platform, by id (chatgpt, perplexity, ai_overview, ai_mode, gemini, copilot, claude, gpt_search). Domain figures cannot be split by platform. |
domainType | one of own, ugc, corporate, editorial | No | Only domains of this type, such as own or ugc. |
format | one of listicle, article, how_to, product, homepage, forum, video, other | No | Level url only: only pages of this format, such as video. |
search | string | No | Only domains (or page addresses at level url) containing this text. |
compare | boolean | No | true to measure the comparison period too, so every row carries previous and change. Off by default. |
comparePeriod | one of prev_period, prev_month, prev_quarter | No | The comparison period, always as many days as the range: prev_period (the days just before the range, the default and the Sources page's comparison), prev_month or prev_quarter (one that would overlap the range falls back to prev_period, with a note). Implies compare. |
sortBy | one of usedPercent, avgCitations, citationRate, retrievalRate, citationCount, retrievalCount, name | No | Sort rows by this metric, highest first, or by name; a metric the level does not carry sorts last. Defaults to usedPercent for domains and citationRate for pages, as the Sources page sorts. With a date breakdown, rows are in date order first. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with breakdown, comparison, counts, filters, interval, level, notes, range, rows, nextCursor, total.
Example call
{
"brandId": "brand_1",
"breakdown": [
"date"
],
"interval": "month"
}Answers
The AI answers Heading collected for the brand's tracked prompts. Every score is computed from these, so this is where a disagreement gets settled.
AI answers (list_answers)
list_answers. Read. Changes nothing.
The AI platform answers collected for the brand's active prompts, newest first, as the visibility page lists them: the evidence every visibility figure is computed from. Each row carries the answer's id, the prompt it answers (id and text), the platform, the UTC day it counts on, whether it names the brand, the brand's rank among tracked brands (null when not named), the brand's sentiment (null when not scored), the tracked competitors it names with their ranks, how many sources it cites, and a short plain-text excerpt. Filter by UTC day range (default the last 28 days, clamped to the day daily tracking began), platforms, prompts, market, tags, query type, the tracked competitors named, or to answers that name the brand. Use it for "what is ChatGPT saying about us", to find the answers behind a score that moved, or to pick answers to read in full with get_answer. Page with limit and cursor; a cursor only works with the filters it came from.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
competitorIds | list of string | No | Only answers that name at least one of these tracked competitors, by id. A competitor id the brand does not track answers not_found. |
endDay | string | No | The last UTC day to read (YYYY-MM-DD), inclusive. Defaults to today. |
market | string | No | Only answers to prompts asked in this market, as a market tag such as en-GB. Omit for every market. |
mentionedOnly | boolean | No | When true, only answers that name the brand. |
platforms | list of one of chatgpt, perplexity, ai_overview, ai_mode, gemini, copilot | No | Only answers from these AI platforms. Omit for every platform. |
promptIds | list of string | No | Only answers to these prompts, by the ids list_prompts returns, whatever their status, so a paused prompt's earlier answers can be read. A prompt the brand has removed is accepted too, for its answers from days before the removal. Omit for every active prompt. A prompt id the brand does not hold answers not_found. |
queryType | one of all, branded, unbranded | No | branded (prompts that name the brand), unbranded (prompts that do not) or all. Defaults to all. |
startDay | string | No | The first UTC day to read (YYYY-MM-DD), inclusive. Defaults to 27 days before endDay. Days before daily tracking began are not read, and the answer says so. |
tagIds | list of string | No | Only answers to prompts carrying these tags, by id: any of the tags in one folder, and every folder named. A tag id the brand does not hold answers not_found. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with answers, range, nextCursor, total.
Example call
{
"brandId": "brand_1",
"competitorIds": [
"competitor_1"
],
"endDay": "2026-11-04",
"limit": 100,
"market": "en-GB",
"mentionedOnly": true,
"platforms": [
"chatgpt",
"perplexity"
],
"promptIds": [
"prompt_1"
],
"queryType": "unbranded",
"startDay": "2026-10-08",
"tagIds": [
"tag_1"
]
}AI answer (get_answer)
get_answer. Read. Changes nothing.
One collected AI answer in full, by the id list_answers returns: the full answer text exactly as the platform gave it (markdown, never shortened), the prompt it answers, the platform, the UTC day and time it was collected and how it was fetched, every source it cites (URL, domain, title where the platform gave one, and whether the domain is one of the brand's sites), and every tracked brand it names (the brand and its tracked competitors) with each one's rank among tracked brands and sentiment. Use it to quote an answer, to see who an answer recommends instead of the brand, or to check which pages it cites. An answer to a prompt this brand does not track answers not_found.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
answerId | string | Yes | The answer to read, by the id list_answers returns. |
Returns an object with answer, brand, citations, mentions, prompt.
Example call
{
"answerId": "answer_1",
"brandId": "brand_1"
}Measurement changes
What changed what the scores measure. When a figure moved, read this first to tell a change in the measurement from a change in performance.
Measurement changes (list_changes)
list_changes. Read. Changes nothing.
What changed what the brand's scores measure, one entry per UTC day, newest day first: prompts added, tracked, reactivated, paused, removed, suggested or rejected (with counts and a sample of each), AI platforms added to or removed from the tracked set, and competitors added to or removed from the tracked set (by name). Each day is netted, so a change undone the same day is not listed, and states the active prompt count at the end of the day, which is the set that day's visibility is measured over. Visibility is measured over the active prompts and the tracked platforms, and every comparison over the tracked competitors, so a score that moves on one of these days may have moved because the measurement changed, not the brand's performance. Read it before explaining any movement in a score; the days line up with the change markers on the visibility chart. A competitor removed and added back on one day nets to nothing here, although the removal dropped its earlier mention history. Covers the last 30 days by default; name startDay and endDay for another range (up to 366 days). The default range ends today, so when paging with cursor across midnight UTC, pass the same endDay on every page. Days with no change are not listed.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
endDay | string | No | The last UTC day (YYYY-MM-DD) to cover, inclusive. Defaults to today; pass it explicitly when paging, so every page covers the same days. |
startDay | string | No | The first UTC day (YYYY-MM-DD) to cover, inclusive. Defaults to 29 days before endDay, so 30 days in all. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with changes, range, nextCursor, total.
Example call
{
"brandId": "brand_1",
"endDay": "2026-10-31",
"limit": 50,
"startDay": "2026-10-01"
}Prompt research
Prompt research: candidate questions from four sources, triaged before anyone tracks them. Two sources spend credits; see Writes and limits.
Prompt candidates (list_prompt_candidates)
list_prompt_candidates. Read. Changes nothing.
The brand's prompt research list for one market: candidate questions that sources offered the brand (the index of AI answers, Search Console, questions generated from the brand profile, an uploaded list) before anyone tracks them. Each candidate carries its id, exact text, status, topic, the judge's relevance, its duplicate group (other wordings of the same question, led by the strongest), its flags, its query type, every source with the figures it gave and the UTC day it gave them, the candidates a converted question was written from, its target page and the team's note. A null figure means that source gave no figure, never zero. Omit view for the Inbox and the Shortlist together, the questions the team has not decided on; tracked lists the candidates already added. The answer also states every market's counts, the topics in use, how many candidates carry each flag, the market's recent pulls (a pending pull lands here when it finishes), and the team's AI & data credits with what each source costs. Order is by demand, the strongest AI search volume first. A candidate is NOT a tracked prompt (list_prompts lists those). Use it before pulling, moving or adding candidates, to find their ids.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
market | string | No | The market as a market tag such as en-GB. Omit for the market the workbench opens on: the brand's primary market, or the first one holding candidates. |
topic | string | No | Only candidates in this topic, matched ignoring case, as the topics list names it. |
view | one of inbox, shortlist, tracked, dismissed, used | No | inbox, shortlist, tracked (added as prompts), dismissed or used (rewritten into another question). Omit for the Inbox and the Shortlist together. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with candidates, credits, listed, market, markets, pulls, nextCursor, total.
Example call
{
"brandId": "brand_1",
"limit": 50,
"market": "en-GB",
"topic": "Pricing",
"view": "shortlist"
}Pull quote (get_pull_quote)
get_pull_quote. Read. Changes nothing.
What a pull_prompt_candidates call with these arguments would cost now, in credits from the team's AI & data credits, and whether the balance lets it start. Changes nothing and spends nothing. An ai_index pull reads the index of AI answers for about 30 credits (state fetch), or costs nothing when anyone searched that topic in the last 30 days (cached) or a read of it is already running (in_flight). A generate run is priced by how many it writes (priced: 2 for 10, 3 for 20, 6 for 40). search_console and your_list are free. Tell the person the cost, then pass credits as acceptQuote to pull_prompt_candidates. A quote is not held: the pull checks the price again and refuses as conflict if it went up.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
count | 10 or 20 or 40 | No | generate only: how many to write. Defaults to 20. |
market | string | No | The market to pull for, as a market tag such as en-GB. Defaults to the brand's primary market. |
source | one of ai_index, search_console, generate, your_list | Yes | The source the pull would read. |
topic | string | No | ai_index only, required there: the topic to look up. |
Returns an object with balance, canSpend, cost, credits, market, source, state.
Example call
{
"brandId": "brand_1",
"source": "search_console"
}Pull prompt candidates (pull_prompt_candidates)
pull_prompt_candidates. Write. Most clients ask you before they run it. Any team member with access to the brand. Can spend credits.
Pulls candidate questions into the brand's prompt research list for one market, from one source: ai_index (questions people ask AI assistants about a topic, from the index of AI answers), search_console (the connected site's own search queries, free), generate (questions written from the brand profile, aimed at chosen angles) or your_list (lines you supply, free). Nothing is tracked: candidates land in the Inbox for the team to review, and a text already listed or tracked is merged, never doubled. The AI index and Generate are paid from the team's AI & data credits: call get_pull_quote first, tell the person the cost (an AI index read uses about 30 credits, nothing when anyone searched that topic in the last 30 days; a Generate run of 20 uses about 3), and pass that number as acceptQuote: it is the most this call may spend. A pull that would now cost more is refused as conflict with the new price, and nothing is spent while the balance is not above zero. An AI index read is charged when the index answers, never if the call to the index fails; a Generate run is charged when its candidates land. Both usually land within minutes: the answer says pending, and list_prompt_candidates shows the candidates when they land.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
acceptQuote | integer | No | Required for ai_index and generate: the credits get_pull_quote answered for this pull, the most this call may spend. Ignored for the free sources. |
angles | list of one of differentiators, buying_path, comparisons, problems | No | generate only, required there: what to write about. differentiators, buying_path (where to buy, price, eligibility), comparisons, problems (what it solves). |
count | 10 or 20 or 40 | No | generate only: how many to write, 10, 20, 40. Defaults to 20. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments pulls once and returns the first answer, for a day. |
lines | list of string | No | your_list only, required there: the questions to add as candidates, one per line. |
listName | string | No | your_list only: the name the pull is shown under. Defaults to Agent list. |
market | string | No | The market to pull for, as a market tag such as en-GB. Defaults to the brand's primary market. Any market Heading supports may be added. |
minImpressions | integer | No | search_console only: the fewest impressions a query needs. Defaults to 50. |
questionsFirst | boolean | No | search_console only: put queries phrased as questions first. Defaults to true. |
range | one of last_28_days, last_3_months, last_6_months, last_12_months | No | search_console only: how far back to read. Defaults to last_3_months. |
source | one of ai_index, search_console, generate, your_list | Yes | Where to pull candidates from. |
style | one of questions, mix, short_queries | No | generate only: questions (natural questions), short_queries (search style) or mix. Defaults to questions. |
topic | string | No | ai_index only, required there: the topic to look up, such as running shoes for flat feet. |
Returns an object with balance, candidates, market, note, pull, replayed, source, spentCredits.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "list-2026-11-05",
"lines": [
"which crm suits a five person team"
],
"listName": "Sales call questions",
"source": "your_list"
}Move prompt candidates (move_prompt_candidates)
move_prompt_candidates. Write. Most clients ask you before they run it. Any team member with access to the brand.
Moves candidates between the brand's Inbox, Shortlist and Dismissed lists, as the workbench's triage buttons do: shortlist (inbox to shortlist), dismiss (inbox or shortlist to dismissed), restore (dismissed to inbox) and each move back. Name the list the candidates are in now as from, by what list_prompt_candidates returned: only candidates still there move, so a teammate's newer move is never overwritten, and every candidate that did not move is answered with the list it is in now (null when the brand holds no candidate with that id). A dismissed candidate is never offered again by a later pull. Nothing here is tracked: add_prompt_candidates adds shortlisted ones to the portfolio. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
candidateIds | list of string | Yes | The candidates to move, by the ids list_prompt_candidates returns. |
from | one of inbox, shortlist, dismissed | Yes | The list the candidates are in now. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments moves once and returns the first answer, for a day. |
to | one of inbox, shortlist, dismissed | Yes | The list to move them to. Must differ from from. |
Returns an object with moved, notMoved, replayed.
Example call
{
"brandId": "brand_1",
"candidateIds": [
"cand_3"
],
"from": "dismissed",
"idempotencyKey": "restore-cand-3",
"to": "inbox"
}Add prompt candidates (add_prompt_candidates)
add_prompt_candidates. Write. Most clients ask you before they run it. Any team member with access to the brand.
Adds shortlisted candidates in one market to the brand's tracked prompts, as the workbench's Add to portfolio does, so each prompt keeps where it came from: its candidate, its sources and figures, its query type, its target page (checked against the brand's sites again) and its topic as a tag. New prompts are active and counted in every visibility figure from their first collection day, so this changes what the figures measure. Only candidates on the Shortlist are added, in the Shortlist's order (strongest demand first) until the prompt quota is full; the rest stay on the Shortlist and come back under didNotFit, never as an error. A text the brand already tracks is linked to that prompt and takes no slot. A candidate a teammate moved off the Shortlist first is left where it is and listed under notOnShortlist. Every addition is recorded in the prompt changelog under the person the call acts as. Move candidates to the Shortlist first with move_prompt_candidates; use create_prompts only for questions that are not candidates.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
candidateIds | list of string | Yes | The shortlisted candidates to add, by the ids list_prompt_candidates returns. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments adds once and returns the first answer, for a day. |
market | string | Yes | The candidates' market, as a market tag such as en-GB. |
Returns an object with added, alreadyTracked, collection, didNotFit, filedAsAdded, market, notOnShortlist, quota, replayed, targetPagesLeftOff.
Example call
{
"brandId": "brand_1",
"candidateIds": [
"cand_1",
"cand_2"
],
"idempotencyKey": "add-shortlist-2026-11-05",
"market": "de-DE"
}Context
The brand's context documents: the facts Heading's own pipelines read about the business, and how well Heading understands it.
Context documents (list_context_docs)
list_context_docs. Read. Changes nothing.
The brand's Context tree: the folders and markdown documents the team keeps about the business, which Heading's own prompt generation, perception and setup read for brand facts. Each row carries the item's id, title, kind (folder or document), the folder it sits in, its position among that folder's items (the order the Context page shows, and the index move_context_docs takes), its depth, its version, when it last changed and a document's length; rows come in tree order, each folder followed by what it holds. Bodies are not included: get_context_doc reads one in full. The answer also carries Confidence, how well Heading understands the business as last assessed from these documents: a score and band, and per dimension (what they sell, audience, positioning, competitors, geography, recognition) the coverage grade with the questions and sections that would deepen it, weakest first. Confidence is read from the last assessment and never computed here, so it is null with a reason when nothing is assessed, and a document written in this session does not move it straight away. Read this before writing context, so you change an existing document rather than add a near-copy.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with confidence, counts, docs, nextCursor, total.
Example call
{
"brandId": "brand_1",
"limit": 100
}Context document (get_context_doc)
get_context_doc. Read. Changes nothing.
One context document in full: its title, its whole markdown body with no cap, its version, when it was created and last changed, and its path (the folders it sits in, top level first). Take the id from list_context_docs. Send the version it answers with update_context_docs, so an edit someone else made since is never overwritten. A folder id answers invalid_input, since a folder has no body; list_context_docs lists what it holds.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
docId | string | Yes | The item's id, as list_context_docs returns it. |
Returns an object with body, createdAt, id, parentId, path, title, updatedAt, version.
Example call
{
"brandId": "brand_1",
"docId": "doc_1"
}Add context documents (create_context_docs)
create_context_docs. Write. Most clients ask you before they run it. Any team member with access to the brand.
Adds up to 50 folders or documents to the brand's Context tree in one call. Give each a title, its kind (document unless you say folder), the folder to put it in (a folder id from list_context_docs, or null for the top level) and, for a document, its markdown body. Each lands last in its folder. The bodies in one call may total at most 2,000,000 characters; send more across several calls. The call is all or nothing: a folder id this brand does not hold, or a body on a folder, refuses the whole call and nothing is added. A document with the same title as an existing one is a second document, not an edit: to change what a document says, use update_context_docs. Heading's own pipelines read these documents for brand facts, and a new document with a body marks Confidence for a fresh assessment. Records the person the call acts as. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
docs | list of object | Yes | The items to add, in the order to add them. |
docs[].body | string | No | A document's body, as markdown. Leave it out for a folder, or for an empty document. |
docs[].kind | one of folder, document | No | document (the default) or folder. |
docs[].parentId | string or null | No | The folder it sits in, by id from list_context_docs; null for the top level of the tree. |
docs[].title | string | Yes | The title, as the Context page shows it. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with created, replayed.
Example call
{
"brandId": "brand_1",
"docs": [
{
"kind": "folder",
"parentId": null,
"title": "Competitors"
},
{
"body": "## Main rivals\n\n- **Acme Payroll**",
"kind": "document",
"parentId": "folder_1",
"title": "Rival notes"
}
],
"idempotencyKey": "context-2026-11-05-1"
}Edit context documents (update_context_docs)
update_context_docs. Write. Most clients ask you before they run it. Any team member with access to the brand.
Changes the title or the body of up to 50 context items in one call. Each entry names the item, the version you read it at (from list_context_docs or get_context_doc) and what to change: a title, a body (documents only), or both. The body you send replaces the whole body, so read the document with get_context_doc first and send it back with your change in place. The bodies in one call may total at most 2,000,000 characters. An item someone changed since you read it refuses the whole call with conflict, naming its current version: read it again and apply your change to that. The call is all or nothing, so a stale or unknown item writes nothing. Each item changed gets a new version. A changed body marks Confidence for a fresh assessment. To move an item use move_context_docs. Records the person the call acts as. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
docs | list of object | Yes | The edits, one entry per item; each item at most once. |
docs[].body | string | No | The new body, as markdown, replacing the whole body. Documents only. |
docs[].docId | string | Yes | The item's id, as list_context_docs returns it. |
docs[].title | string | No | The new title. |
docs[].version | integer | Yes | The version you read the item at. An older version than the item holds now refuses the call. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with replayed, updated.
Example call
{
"brandId": "brand_1",
"docs": [
{
"body": "## Who buys\n\n- **Practice managers** at firms of 5 to 50",
"docId": "doc_1",
"version": 3
}
],
"idempotencyKey": "context-edit-2026-11-05-1"
}Move context documents (move_context_docs)
move_context_docs. Write. Most clients ask you before they run it. Any team member with access to the brand.
Moves up to 50 context items to a folder and a place in it, in one call, as dragging them on the Context page would. Each entry names the item, the folder to move it into (a folder id, or null for the top level) and its index among that folder's items, from 0 (an index past the end puts it last). Moves apply in the order sent, each seeing the tree the moves before it left. A folder cannot go inside itself or anything under it. The call is all or nothing: an unknown item or folder, or a move that would put a folder inside itself, refuses the whole call. A move changes no text, so it needs no version and gives none. Records the person the call acts as. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
moves | list of object | Yes | The moves, in the order to make them; each item at most once. |
moves[].docId | string | Yes | The item's id, as list_context_docs returns it. |
moves[].index | integer | Yes | Its place among the destination folder's items, from 0, in the order list_context_docs shows them. |
moves[].parentId | string or null | Yes | The folder it sits in, by id from list_context_docs; null for the top level of the tree. |
Returns an object with moved, replayed.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "context-move-2026-11-05-1",
"moves": [
{
"docId": "doc_2",
"index": 3,
"parentId": null
}
]
}Delete context documents (delete_context_docs)
delete_context_docs. Delete. Most clients ask you before they run it. Any team member with access to the brand.
Deletes up to 50 context items for good. Deleting a folder deletes everything in it, at any depth; the answer says, for each item, how many documents and folders went with it. There is no undo, and Heading's own pipelines stop reading the deleted documents at once, so list what a folder holds with list_context_docs before deleting it and confirm with the person. The call is all or nothing: an id this brand does not hold refuses the whole call and nothing is deleted. An item that sits inside a folder deleted in the same call is counted once: on its own row when it comes first, otherwise in the folder's counts. Records the person the call acts as. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
docIds | list of string | Yes | The items to delete, by id. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with deleted, replayed.
Example call
{
"brandId": "brand_1",
"docIds": [
"folder_1",
"doc_2"
],
"idempotencyKey": "context-delete-2026-11-05-1"
}Tasks, plans and objectives
The task board, its plans, and the brand's objectives.
Tasks and plans (list_tasks)
list_tasks. Read. Changes nothing.
The brand's task board and its live plans, with the ids the task, plan and comment tools take. Each task row carries its id, title, full description, status (backlog, todo, in_progress, done, canceled), priority, assignee (name and email), due day, plan, labels and when it was created, changed and completed. The board's own scope: every review-accepted task on a live plan or with no plan, as the Tasks page shows it. The answer also states how many tasks the board holds in each status and lists every plan that is not archived, with its status, the objective it serves and its open and total task counts, whatever the filters. Filter tasks by status, plan, objective (tasks on plans serving it), assignee or title; page with limit and cursor. Read it before creating work, so an open task that already covers a fix is updated with update_tasks rather than created again.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
assignee | string | No | Only tasks assigned to this teammate, by email address, or "unassigned" for tasks nobody holds. Omit for everyone. |
objectiveId | string | No | Only tasks on plans serving this objective, by the id list_objectives returns. |
planId | string | No | Only the tasks of this plan, by its id, or "none" for tasks with no plan. Omit for every plan. |
search | string | No | Only tasks whose title contains this, ignoring case. |
status | one of backlog, todo, in_progress, done, canceled | No | Only tasks in this status. Omit for every status. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with plans, statusCounts, tasks, nextCursor, total.
Example call
{
"brandId": "brand_1",
"planId": "none"
}Objectives and key results (list_objectives)
list_objectives. Read. Changes nothing.
The brand's objectives: the directions the team has committed to, each proven by one to four key results. Each objective carries its id, title, description, status (active, achieved, not_achieved, abandoned), target day, when it was set and closed, the live plans serving it, and its key results in order. Each key result carries its id, its exact statement, how it can be proven (metric, web or unverifiable, with the reason), its stored measure config with its scope (market, tags, query type), baseline and target, how many verdicts it holds and its newest verdict. Read it before setting or changing an objective, and before recording a verdict, which takes the key result's id and exact statement. Filter by status; page with limit and cursor.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
status | one of active, achieved, not_achieved, abandoned | No | Only objectives in this status. Omit for every status. |
cursor | string | No | The nextCursor from the previous page. Omit for the first page. Opaque: pass it back unchanged. |
limit | integer | No | Rows per page, 1 to 1000. Defaults to 100. |
Returns an object with objectives, nextCursor, total.
Example call
{
"brandId": "brand_1",
"status": "active"
}Add tasks (create_tasks)
create_tasks. Write. Most clients ask you before they run it. Any team member with access to the brand.
Adds up to 50 tasks to the brand's board in one call, for work a person should pick up: fixing something the data surfaced, earning a citation, updating a page. Each task needs a title; it may carry a description (the why, the evidence, what done looks like; never assignment or dates, which have their own fields), a status (todo unless given), a priority, an assignee ("me" or a teammate's email), a due day (YYYY-MM-DD), and a plan, by id (planId, from list_tasks) or by title (planTitle: a live plan with that title is reused, otherwise a new active plan is created, and sibling tasks with the same title land in one plan). The call is all or nothing: if any task cannot be added (an unknown teammate, a plan that is archived or still in review), nothing is added and the answer names the task by its position. Each new task records a "created" entry in its activity under the person the call acts as, and an assignee is notified as on the board. Send an idempotencyKey to make a retry safe. Call list_tasks first so you do not add a task the board already holds.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
tasks | list of object | Yes | The tasks to add, in the order they should appear. |
tasks[].assignee | string | No | Who the task is for: "me" for the person this call acts as, or a teammate's email address. A name alone cannot be resolved; ask for the email. |
tasks[].description | string | No | The task body: the why, the evidence, and what done looks like. |
tasks[].dueDate | string | No | The due date, as a full UTC calendar day (YYYY-MM-DD). A month or quarter ("August 2026", "Q3") is not a date: agree a specific day with the user first. |
tasks[].planId | string | No | Put the task on this existing live plan, by the id list_tasks returns. |
tasks[].planTitle | string | No | Put the task on the live plan with this title, or on a new active plan with it when none exists. |
tasks[].priority | one of urgent, high, medium, low | No | urgent, high, medium or low. Omit for none. |
tasks[].status | one of backlog, todo, in_progress, done, canceled | No | The task's starting status. Defaults to todo. |
tasks[].title | string | Yes | The task title, imperative and specific. |
Returns an object with created, replayed.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "fixes-2026-11-05",
"tasks": [
{
"assignee": "me",
"description": "Three answers cite a competitor's pricing page.",
"dueDate": "2026-11-20",
"planTitle": "Pricing citations",
"priority": "high",
"title": "Publish a comparison page"
},
{
"planId": "plan_1",
"status": "backlog",
"title": "Brief the PR agency"
}
]
}Change tasks (update_tasks)
update_tasks. Write. Most clients ask you before they run it. Any team member with access to the brand.
Changes up to 100 existing tasks in one call, each by its id from list_tasks, with a patch of only the fields to change: title, description, status, priority (null clears it), assignee ("me", a teammate's email, or null to unassign), due day (null clears it), or plan (planId, null for no plan, or planTitle, which reuses or creates a live plan). Each patch is compared with the stored task and only the fields that differ are written, so a replayed patch changes nothing and says so (changed: false). Every changed field is recorded in the task's activity under the person the call acts as, a task marked done records who completed it, a plan's status follows its tasks, and a new assignee is notified, exactly as on the board. Every task is checked before anything is written: a task this brand does not hold, one on an archived plan or a plan still in review, or a declined task refuses the whole call, naming it by position. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
updates | list of object | Yes | The changes, one entry per task. A task appears once. |
updates[].patch | object | Yes | The fields to change. At least one must be present. |
updates[].taskId | string | Yes | The task to change, by the id list_tasks returns. |
Returns an object with replayed, updated.
Example call
{
"brandId": "brand_1",
"idempotencyKey": "triage-2026-11-05",
"updates": [
{
"patch": {
"assignee": "sam@example.com",
"dueDate": "2026-11-20",
"priority": "high"
},
"taskId": "task_1"
},
{
"patch": {
"assignee": null,
"planId": null
},
"taskId": "task_2"
},
{
"patch": {
"planTitle": "Pricing citations"
},
"taskId": "task_3"
}
]
}Add task comments (create_task_comments)
create_task_comments. Write. Most clients ask you before they run it. Any team member with access to the brand.
Posts up to 50 comments on the brand's tasks in one call, each on a task by its id from list_tasks, written as the person the call acts as. A comment is plain text; to mention a teammate write @Name, where id is the teammate's id as list_tasks gives it for an assignee, and that teammate is notified, as the task's assignee is, exactly as on the board. Use it to record what was found, decided or done on a task. A task this brand does not hold refuses the whole call, naming the comment by position. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
comments | list of object | Yes | The comments to post, in order. |
comments[].body | string | Yes | The comment text. |
comments[].taskId | string | Yes | The task to comment on, by the id list_tasks returns. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
Returns an object with created, notificationsDelivered, replayed.
Example call
{
"brandId": "brand_1",
"comments": [
{
"body": "The page went live today.",
"taskId": "task_1"
}
]
}Add plans (create_plans)
create_plans. Write. Most clients ask you before they run it. Any team member with access to the brand.
Adds up to 20 plans to the brand in one call. A plan groups tasks under one title and may serve one active objective (objectiveId, from list_objectives). A new plan is active at once, with no tasks: add them with create_tasks and its planId. A title another live plan of this brand already carries is not added again: that plan is reported under alreadyExisted, so a retry converges. When the call names an objective for such a plan, a plan serving no objective is linked to it, and one serving another objective refuses the call with conflict (move it with update_plans). An objective this brand does not hold, or one that is closed, refuses the whole call. A plan's status is derived from its tasks.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
plans | list of object | Yes | The plans to add. Each title appears once. |
plans[].objectiveId | string | No | The active objective this plan serves, by its id. |
plans[].title | string | Yes | The plan's title. |
Returns an object with alreadyExisted, created, replayed.
Example call
{
"brandId": "brand_1",
"plans": [
{
"objectiveId": "objective_1",
"title": "Earn review citations"
}
]
}Change plans (update_plans)
update_plans. Write. Most clients ask you before they run it. Any team member with access to the brand.
Changes up to 50 existing plans in one call, each by its id from list_tasks: rename it, or link it to an active objective (objectiveId) or unlink it (null). Each patch is compared with the stored plan and only what differs is written, so a replayed patch changes nothing and says so. A plan's status is derived from its tasks, so it is not a field: complete a plan by moving its tasks to done with update_tasks. An archived plan, a title another live plan already carries, or a closed objective refuses the whole call, naming the plan by position.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
updates | list of object | Yes | The changes, one entry per plan. A plan appears once. |
updates[].patch | object | Yes | The fields to change. At least one must be present. |
updates[].planId | string | Yes | The plan to change, by the id list_tasks returns. |
Returns an object with replayed, updated.
Example call
{
"brandId": "brand_1",
"updates": [
{
"patch": {
"objectiveId": null
},
"planId": "plan_1"
}
]
}Set objectives (create_objectives)
create_objectives. Write. Most clients ask you before they run it. Any team member with access to the brand.
Sets up to 10 objectives for the brand in one call, each a qualitative direction the team commits to, agreed with the user first, with 1 to 4 key results. Each objective is active at once. Each key result is a free-text statement classified honestly by how it can be proven: 'metric' (from this brand's tracked Heading measures; attach a measure config only then, and only for a measure Heading tracks, with the baseline and the scope taken unchanged from get_objective_measure), 'web' (provable by web search or fetch at verification time), or 'unverifiable' (state the reason; an honest unverifiable classification is better than a made-up metric). The answer carries each objective's id and its key result ids. Send an idempotencyKey to make a retry safe.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
objectives | list of object | Yes | The objectives to set. |
objectives[].description | string | No | Narrative context: why this objective, as agreed. |
objectives[].keyResults | list of object | Yes | The key results that will prove it, in order. |
objectives[].targetDate | string | No | A target day (YYYY-MM-DD). Nothing happens on it by itself: verification is always asked for. |
objectives[].title | string | Yes | The objective: the direction, stated plainly. |
Returns an object with created, replayed.
Example call
{
"brandId": "brand_1",
"objectives": [
{
"keyResults": [
{
"statement": "Named in most answers about small-firm CRMs",
"verifiability": "metric"
},
{
"statement": "Listed on the top three review sites",
"verifiability": "web"
}
],
"targetDate": "2027-03-31",
"title": "Become the default CRM for small accounting firms"
}
]
}Change objectives (update_objectives)
update_objectives. Write. Most clients ask you before they run it. Any team member with access to the brand.
Changes up to 10 active objectives in one call, each by its id from list_objectives. A patch can change the title, the description, the target day (null clears it), and the key results as the FULL new set: an entry with an id keeps or edits that key result, an entry without one adds a key result (or keeps the stored one with the same statement), and stored key results missing from the set are removed. The set must hold 1 to 4; a key result that already carries verdicts cannot be removed; a closed objective cannot be changed. Every objective is checked before anything is written, and any refusal refuses the whole call, naming the objective by position.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
updates | list of object | Yes | The changes, one entry per objective. An objective appears once. |
updates[].objectiveId | string | Yes | The objective to change, by the id list_objectives returns. |
updates[].patch | object | Yes | The fields to change. At least one must be present. |
Returns an object with replayed, updated.
Example call
{
"brandId": "brand_1",
"updates": [
{
"objectiveId": "objective_1",
"patch": {
"targetDate": null,
"title": "Lead small-firm CRM answers"
}
}
]
}Record key result verdicts (create_key_result_verdicts)
create_key_result_verdicts. Write. Most clients ask you before they run it. Any team member with access to the brand.
Records up to 20 verdicts on key results in one call, during a verification the user asked for: 'met', 'not_met' or 'inconclusive', each with markdown evidence citing exactly what was checked (the measures read, the pages fetched, links included). 'inconclusive' is an honest outcome when the check cannot decide; never record 'met' on inference alone. Pass each key result's id and its statement exactly as list_objectives gives it: a verdict whose statement no longer matches the stored one, or whose objective is no longer active, refuses the whole call. Verdicts never overwrite: a new check appends a new verdict, and the statement judged is kept with it. The Objectives page shows who recorded each verdict and how.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
idempotencyKey | string | No | Any unique string for this request. A retry with the same key and arguments writes nothing new and returns the first answer, for a day. |
verdicts | list of object | Yes | The verdicts to record. |
verdicts[].evidence | string | Yes | Markdown evidence for the judgment: what was checked and what was found, links included. Cite tool answers, never an inferred figure. |
verdicts[].keyResultId | string | Yes | The key result, by the id list_objectives returns. |
verdicts[].statement | string | Yes | The key result's statement, exactly as list_objectives gives it. |
verdicts[].value | one of met, not_met, inconclusive | Yes | met, not_met or inconclusive. |
Returns an object with created, replayed.
Example call
{
"brandId": "brand_1",
"verdicts": [
{
"evidence": "Mentioned in 64% of answers on 2026-11-04 (list_prompts).",
"keyResultId": "kr_1",
"statement": "Named in most answers about small-firm CRMs",
"value": "met"
}
]
}Agent readiness
How ready the brand's own website is for AI agents. The one tool that measures the site rather than what AI says about it.
Agent readiness (get_agent_readiness)
get_agent_readiness. Read. Changes nothing.
The brand's latest Agent readiness Run: whether the brand's own public website can be found, read and used by the machines that produce AI answers. Answers the 0 to 100 score and its grade (A+ to F), the four weighted Layers (Discovery 20, Access 30, Usability 40, Payments 10) with the points each earned, the fixes ranked by the points a full pass would add, and every Probe with its outcome, the URL it fetched, what it observed and what to change. These are the figures the brand's Agent readiness page shows. Each Probe is one HTTP observation a person can repeat; none states a cause or a forecast, and nothing here joins visibility data. A brand never measured answers run null; a Run that could not reach the site answers no Probes and a null score, never a clean result; a Layer nothing applied to has null points, never zero. Use it when someone asks whether their site is ready for AI agents, or before proposing site-side work: it is the only tool that measures the brand's own site. Filter the Probes by outcome; choose the domain when the brand has more than one.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
domain | string | No | Which of the brand's domains to read, as list_brands reports them. Defaults to the brand's main domain, as the page does. |
outcome | one of pass, fail, warning, error, pending, not_applicable | No | Only Probes with this outcome. fail and warning are the ones to act on. Omit for every Probe. The score, Layers and fixes are always the whole Run. |
Returns an object with domain, domains, fixes, grade, gradeLabel, layers, note, probes, run, score.
Example call
{
"brandId": "brand_1",
"domain": "acme.com",
"outcome": "fail"
}Traffic
The Traffic page: Google Analytics, Search Console, leads and AI crawler activity.
Traffic report (get_traffic_report)
get_traffic_report. Read. Changes nothing.
The brand's Traffic page as data, for one UTC day range: what AI visibility led to, and which AI crawlers fetched the brand's site. Answers Google Analytics figures (AI-referred sessions, leads, revenue, conversion rate, each with its change against the equal-length range before), leads from every connected source and each source's own count, the lead webhook's state and recent leads, Google Search figures from Search Console (clicks, impressions, CTR, position, top queries and pages), a daily series of visibility beside AI sessions and search crawls, and crawl activity from the brand's own server logs: requests by category (Search, Retrieval, Training, never added into one measure), the verified share, what the site answered, each crawler, the most fetched URLs, pages people use that no search crawler fetched, and AI arrivals in the logs beside Analytics sessions. A source that is not connected answers null with a reason, never 0. Log entries and Analytics sessions are different units and are never added together; nor are Search Console clicks and sessions. Crawl activity is part of a paid plan: without one, crawl answers state plan_required and every other part still answers. With no range asked for, each part opens on the page's own default: the last 30 UTC days for Analytics, leads and Google Search, the last 28 for crawl activity. A named range covers every part. Use it when someone asks whether AI visibility is producing anything, or whether AI crawlers can reach the site.
Arguments
| Argument | Type | Required | What it is |
|---|---|---|---|
brandId | string | Yes | The brand to work on, by the id list_brands returns. A brand this connection cannot reach answers brand_not_found. |
endDay | string | No | The last UTC day of the range, YYYY-MM-DD. Give both startDay and endDay, or neither for the last 30 days ending today. |
startDay | string | No | The first UTC day of the range, YYYY-MM-DD. At most 366 days before endDay. |
Returns an object with analytics, crawl, daily, googleSearch, leads, previousRange, range, webhook.
Example call
{
"brandId": "brand_1",
"endDay": "2026-10-31",
"startDay": "2026-10-01"
}