Writes and limits
How changes through the Heading MCP work, the rate limit, credits, and what an error looks like
Writes
The Heading MCP can change what Heading tracks and the work around it: prompts, tags and saved views, competitors, brand settings, context documents, tasks, plans and objectives, and the prompt research list. The tools reference marks every tool that makes a change.
A change happens as soon as the call arrives. Every tool that changes something says so in its listing (the MCP annotation readOnlyHint: false, and destructiveHint on a delete or a whole replacement), and most clients ask you before they run a write. That is the approval: Heading does not hold the change for a second one. A read carries readOnlyHint: true, so a client can run it without asking. Every tool carries openWorldHint: false: it works on your Heading data.
A change is made as a person, under the dashboard's rules. A sign-in connection changes things as you; a Write key as the team member who created it. The person's membership and role are read again on every call, so a team member can change only the brands they have been given, and a change only owners and admins may make in the dashboard is refused to everyone else. See Who a change is made as.
What can write. A Write key, or a sign-in connection approved with mcp:write. A Read key, or a sign-in with mcp:read alone, can call every tool, and a change answers forbidden with the reason and the fix. Nothing is changed.
It is recorded like the same change in the dashboard. A prompt change appears in the prompt changelog beside the visibility it moves, a task change in the task's activity, a tag or view change in the brand's audit trail. Each record names the person, and the key when one was used, and says the change came through the Heading MCP.
A retry is safe with an idempotencyKey. Send any unique string as idempotencyKey. A call repeated with the same key and the same arguments, from the same credential, writes nothing new and returns the first answer with replayed: true, for a day. The same key with different arguments answers conflict.
In Heading's own agent, a repeated create writes once. When the agent in Heading calls a create_ tool twice in one conversation with the same arguments and no idempotencyKey, the second call writes nothing and returns the first answer with replayed: true. Any other change runs each time the agent asks for it, so changing something and changing it back in one conversation both happen; only a replay of the very same call is not made twice. This applies only inside Heading: an MCP client gets the same safety by sending an idempotencyKey.
Writes take lists. Every write takes a list of items, so one call can add 50 prompts or close 20 tasks. Each tool's limit is in its arguments.
How a list is applied
- Adding prompts is all or nothing against the quota. When the new prompts in a
create_promptscall do not all fit the brand's prompt quota, none is added, and the answer isquota_exceededwith how many you asked for and how many slots are free. A prompt the brand already tracks in that market is listed underalreadyTrackedand takes no slot. A prompt the brand removed earlier comes back with its history. - Changing or removing prompts answers prompt by prompt. In
update_promptsanddelete_prompts, each prompt succeeds or fails on its own. The one exception is the quota: if the active prompts after anupdate_promptscall would pass the brand's limit, nothing in the call is changed and the answer isquota_exceeded. - A removal says what it removed.
delete_promptslists each prompt with its text, market and whether its history was kept. A prompt that was ever measured keeps its history in every past figure. - Everything else is all or nothing. Tags, folders, saved views, competitors, context documents, tasks, plans, objectives and verdicts: every item is checked before anything is written, and when one cannot be made, nothing is written and the answer names the item.
- An edit to a context document needs the version you read. When someone changed it since, the call answers
conflictwith the version it is at now, so you read it again rather than overwrite their change. - Measurement changes are logged. Adding or removing a competitor, changing the tracked platforms and changing the prompt set change what comparisons measure, so
list_changeslists each on its UTC day.
Credits
Two sources of pull_prompt_candidates are paid from your team's AI & data credits, the same balance agent runs draw on: ai_index (a read of the index of AI answers, about 30 credits, and nothing when anyone read that topic in the last 30 days) and generate (about 3 credits for 20 questions). search_console and your_list are free.
The price comes first, and asking for it is free. get_pull_quote takes the same arguments as the pull and answers the credits, the state and whether your balance lets it start. A paid pull then needs acceptQuote, the credits you agreed to, and spends at most that: if the price went up in between, the pull answers conflict with the new price and spends nothing. A failed call is never charged. While the balance is not above zero a paid pull answers credits_required.
Rate limit
100 requests per minute per credential. An API key has its own 100. A sign-in connection counts against the person who signed in, so every client that person connects shares one 100. Every HTTP request counts: each tool call, listing the tools, the connection handshake and notifications.
The count is shared by every Heading server, so the limit you read is the limit you get. A minute is a UTC calendar minute: the count starts again at the top of each minute.
Every counted response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (the Unix time, in milliseconds, when the count starts again). Over the limit you get an HTTP 429 carrying a JSON-RPC error with code: -32002, the whole seconds to wait in error.data.retryAfter and again in a top-level retryAfter, and a Retry-After header with the same figure. It is not a tool answer, so it does not use the tool error codes below. A refused request does not count against the next minute.
Pages
A list returns up to 1,000 rows a page, 100 when you name no limit. Each answer carries total, the rows across every page, and nextCursor: pass it back as cursor, unchanged, for the next page. It is null on the last page. A cursor only works with the arguments it came from. Every row carries its id and its name or text, so you never need a second call to say what an id is.
Errors
When a tool refuses a call, it answers with what went wrong and how to fix it, as data rather than a bare failure:
{
"error": {
"code": "invalid_input",
"field": "limit",
"message": "Invalid arguments for list_prompts: limit: Too big: expected number to be <=1000.",
"fix": "Correct the named argument and call again; the tool's input schema lists every argument it takes."
}
}The code is one of a fixed set, so an agent can act on it:
| Code | What it means |
|---|---|
invalid_input | An argument is wrong; field names it. |
brand_not_found | The brand does not exist, or this connection cannot reach it. The two answer the same way. |
not_found | A named row (a prompt, tag, task, answer) does not exist on this brand. |
forbidden | This credential or person may not do this: a Read key, a sign-in without mcp:write, or a change only owners and admins can make. |
approval_required | The change waits for a person to approve it (in Heading's own agent). |
approval_edited | A person approved an edited version of the call; only that version ran. |
not_granted | An unattended agent run is not allowed this tool. |
plan_required | The brand's plan does not cover this. |
quota_exceeded | The change would pass a limit, such as the prompt quota or the platform allowance. |
credits_required | The team's credits cannot pay for this. |
conflict | Something changed under the call (a document's version, a price, an idempotency key with new arguments). Read again. |
rate_limited | Heading's own daily cap on reads of the index of AI answers, across every team, is reached. Try again tomorrow. (The per-minute rate limit is answered differently; see Rate limit.) |
unavailable | Something Heading depends on is not available right now. Try again later. |
internal | Heading failed. Try again, and contact support if it keeps happening. |