Getting Started
Find out whether ChatGPT names your brand for the questions your buyers ask, whether it cites your pages when it does, and how that holds up across repeated answers.
1. Get your API key
Create an account and generate a FetchLayer API key from your dashboard. There is no OpenAI account or OpenAI API key involved.
Create free account →2. Check one answer
Ask the question a buyer would ask, and name the brands to look for. Each one comes back with whether the answer mentioned it, how often, the sentence around each mention, and — separately — whether the answer cited one of its pages.
curl -X POST https://api.fetchlayer.dev/chatgpt/mentions \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"prompt":"best reddit api alternatives","targets":[{"name":"FetchLayer","domains":["fetchlayer.dev"]},"Apify"]}'Mentioned is not cited
mentioned and citedAsSource are reported separately for exactly that reason. Watch retrieved too: it means ChatGPT read one of your pages and then cited someone else. The response also carries otherBrandsMentioned — the brands the answer itself put forward, with the rank it gave them. That is your competitor set for that prompt, as ChatGPT sees it, and you did not have to guess it in advance.
3. Track it over several runs
One answer is a snapshot: ask again in ten minutes and the wording, the order and sometimes the whole list will differ. /ai-rank asks the same prompt several times and reports the aggregate, which is the number worth putting on a dashboard.
curl -X POST https://api.fetchlayer.dev/chatgpt/ai-rank \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"prompt":"best reddit api alternatives","target":{"name":"FetchLayer","domains":["fetchlayer.dev"]},"competitors":["Apify"],"runs":5}'Read the aggregate, not a run
rank, citationRank and the method the answer used to lay its options out. aggregate turns them into mentionRate, averageRank, bestRank, worstRank, citationRate and averageCitationRank, with the same figures per competitor. Keep the prompt, country and webSearch fixed when comparing week to week. The runs are generated alongside each other, so more runs cost more credits but little extra time. Billing is one credit per completed answer, and a run that could not be completed is noted and not billed.
4. Find the prompts worth tracking
Rather than guessing which prompts matter, start from a topic or your brand name. /prompt-discovery returns the prompts it asked, the web searches ChatGPT actually ran while answering them, and prompts it suggests people use.
curl -X POST https://api.fetchlayer.dev/chatgpt/prompt-discovery \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"seed":"reddit api alternatives","maxPrompts":3}'The field names are exact
seed and the count is maxPrompts. brand, domain, topic or promptCount return a 400 naming the field. Unrecognised fields are never quietly ignored on any route. Both lists hold objects, not strings: read text for the wording. observedSearchQueries carry source: "observed_search_query" — searches ChatGPT ran, so the queries your pages have to rank for to be readable by it. suggestedPrompts carry source: "generated_prompt" — suggestions, not observations. Feed the ones that matter back into /ai-rank.
5. Compare a whole market
/visibility-report runs several prompts against several brands in one request and returns share of voice per brand plus a per-prompt breakdown. Extra brands are free — they are read out of the same answers — so the size is bounded by answers: prompts × runs must be at most 10.
curl -X POST https://api.fetchlayer.dev/chatgpt/visibility-report \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"prompts":["best reddit api alternatives","how to scrape reddit"],"targets":["FetchLayer","Apify"],"runs":2}'Tip
Errors and retries
A 400 names the field to fix; unrecognised fields are rejected rather than ignored. A 503 means no finished answer could be obtained within the time allowed, and is always worth retrying — a partial or cut-off answer is never returned as a success. An answer that says "I don't know" is a normal 200, and so is a prompt where none of your brands appear. Read notes on every successful response too: a country that could not be served, runs that could not be completed and details the answer did not show all announce themselves there.
Error responses
Every error, on every route, is JSON in one shape: error (what went wrong, in words), status, code, retryable and requestId, which also comes as the X-Request-Id header. Branch on code and retryable rather than on the text; on a 5xx the text is generic. Quote requestId when reporting a problem.
{
"error": "Missing required parameter: url",
"status": 400,
"missing": ["url"],
"code": "invalid_input",
"retryable": false,
"requestId": "922874e3-c0d0-40d8-8866-d198af132420"
}| code | Status | Retryable | Meaning |
|---|---|---|---|
| invalid_input | 400 | no | The request needs fixing; error names the field. A missing required parameter also lists it in missing. |
| unauthorized | 401 | no | The API key is missing or not valid. |
| insufficient_credits | 402 | no | Nothing left to spend: no credits, a used-up free trial, or a monthly allowance and credit balance both spent. error says which. Waiting does not help; topping up does. |
| forbidden | 403 | no | This key or account is not allowed this request. |
| not_found | 404 | no | What the request names does not exist or is not public. |
| expired | 410 | no | What the request refers to is no longer available; ask for it afresh. |
| quota_exceeded | 429 | no | A request-rate limit was reached; error says which. Slow down before sending more. |
| cancelled | 499 | no | The caller closed or cancelled the request before it finished. The work was stopped and nothing was charged. |
| time_limit | 503 | yes | Your timeoutMs ran out before there was anything to return; ask again, or allow more time. |
| temporarily_unavailable | 503 | yes | Not available right now; ask again shortly. |
| posts_unavailable | 403 | no | TikTok user videos: TikTok is not showing this account’s posts publicly right now. Asking again at once returns the same answer; nothing is charged. |
| store_unreachable | 503 | yes | Store routes (Shopify, WooCommerce): the store itself can’t be reached right now — it may be offline. Ask again later. Where a store route doesn’t say so yet, an offline store is temporarily_unavailable. |
| upstream_error | 502 | yes | The source answered in a way that could not be used; ask again later. |
| internal_error | 500 | yes | Something failed on our side; ask again later, and quote requestId if it persists. |
| tool_error | — | no | MCP only: a tool call failed in a way no other code describes; error says what. |
More codes may be added; treat one you do not recognise by its retryable flag. Over MCP, a tool error carries the same fields in structuredContent, and as a JSON text block beside the message.