Skip to main content

Base URL and authentication

Takes Authorization: Bearer <YOUR_API_KEY> (details). Reading figures needs the stats:read permission. Changing what is measured, under Configuring what gets measured, needs config:write. These endpoints measure a different thing from the rest of the API. Everywhere else, Traceten counts what happened on your site. Here, Traceten asks answer engines your own tracked questions on a schedule and records what they said. Nothing on this page is derived from your visitors, and nothing here appears in your session, visitor or revenue figures. It is a poll of the prompt set you defined, not an observation of what real people asked. How AI visibility is measured covers the sampling method and every formula below; AI answer engines covers what each engine is and what we send it.

Three numbers, never one score

Traceten does not publish a single “visibility score”. Three separate measures answer three different questions, and blending them would hide which one moved. A mention is not a citation. An engine can name you without linking to you, which usually means the model already knows your brand. It can also cite your page without naming you, which means your content answered the question and you got no credit in the text. Those are different problems with different fixes, so the two numbers never merge. Position weighting is a fourth measure, prominence, and it is deliberately kept out of presence. See Prominence.

Confidence intervals, and when a change is real

Every rate carries a Wilson 95% interval as low and high.
Report the band, not just the rate. A 34 percent presence rate measured over 15 runs and one measured over 1,500 are different claims, and only the interval shows which you have. comparison.presence_changed is true only when this window’s interval and the previous window’s interval do not overlap. Two rates that differ while their bands overlap are flat. This is stricter than comparing the rates, and deliberately so: a move from 3 of 10 to 4 of 10 looks like a 33 percent improvement and is indistinguishable from noise.

The denominator is answered_runs, not runs

An engine does not always answer. Google renders an AI Overview for some questions and not others, and a run where nothing rendered is a real observation about the search results page, not a sample of whether you were named in one. So every rate on this page divides by answered runs. Both figures are returned, plus coverage, which is answered_runs / runs. More precisely, a brand’s rates divide by scored_answered_runs: the answered runs in the scans that brand was actually scored in, which is not always the site total. A competitor you added half way through a window has rows only from the day you configured it, and charging it for the days before it existed would halve its presence rate for no reason. For a brand present throughout, scored_answered_runs equals answered_runs. Read them together:
  • Low coverage means the engine is not answering this question at all. Rewriting your page will not change it. A different prompt might.
  • Low presence with high coverage means the engine answers the question and does not mention you.

Empty states that are not zero

Three states look like a 0 percent presence rate and are not. Rendering any of them as 0 tells a customer they are invisible when the truth is that nothing was measured. citation and share_of_voice go null under the same conditions as presence, so all three move together. A brand that was never scored returns null for every rate rather than a 0 percent that would draw a false drop on the day it was created.

has_data means something narrower on the citation endpoints

On /summary, /timeseries and /competitors, has_data: false means no runs landed in the window. On /citations, /citations/by-page and /runs it means the window produced no rows of that kind: no citations, no cited pages of yours, no stored answers. Those are measured zeroes, not absent measurements. A scan can run, answer, and cite nobody.

GET /v1/visibility/summary

Presence, share of voice and citation rate for a window, with the previous window for comparison.
That response says: 34 percent presence over the 10 days ending September 2, somewhere between 25 and 45 percent. The previous 10 days read 31 percent. The bands overlap, so presence_changed is false and this is not a rise.

Runs and prompts are different denominators

engines[] carries both, and they answer different questions. runs and answered_runs count engine calls: each prompt is asked several times so the interval means something, so these are the repetitions. prompts_scanned and prompts_answered count distinct prompts. “Google showed an AI Overview for 8 of your 10 prompts” is the second pair. It is not answered_runs / runs, which in the example above is 41 of 50 and is a statement about repetitions. prompts_scanned is prompts scanned in this window, never the number configured on your site. The two differ whenever a prompt was added mid-window or is paused, and using your configured count as the denominator would label the answer “your 20 prompts” while the numerator covered only what ran. Say “scanned”. An engine that answers fewer prompts than it was asked is not a gap. Google declining to render an AI Overview for a question is information about the question.

Engine labels

Render engines[].label, never the engine value. Traceten measures each vendor’s API, not the consumer product a person uses in a browser, and the two do not always return the same answer. Printing a bare “ChatGPT” over an API measurement would be a claim about a product Traceten did not query.

Prominence

prominence is position weighting: the mean of 1 / rank across mentions the engine ranked. Higher is better, and 1.0 means every ranked mention came first. It is reported separately from presence and is never a component of it. Presence asks whether you were named. Prominence asks how near the top. Multiplying them produces a single number that cannot be acted on. Two fields keep it honest:
  • unranked_mentions counts mentions the weighting could not describe, because the answer was prose rather than a ranked list. A score computed from 22 of 30 mentions does not describe all 30.
  • prominence_window is the window prominence was actually computed over. Position data is kept for prominence_retention_days, which is shorter than the presence trend. Ask for a longer window and this field narrows. Read it, not the window you sent. null means the whole request is older than the retained data, and prominence is then null too.

Asking about a competitor

brand_id selects the subject of the response, not just a row filter. Pass a competitor’s brand id and the brand block describes them. Two consequences:
  • share_of_voice comes back null. Narrowing the rows to one brand leaves its own mentions as the denominator, which would read 100 percent for whoever you asked about.
  • A brand_id that names no brand on your site is a 422, not a silent fall back to your own brand.
/v1/visibility/timeseries accepts the same parameter and behaves identically, including returning share_of_voice: null on every point. While brand_id is set, the site wide block on /summary is folded from the filtered rows: runs, answered_runs, coverage, rep_sets, scored_brands, engines and has_unscored_runs then describe that brand’s rows rather than the whole site. totals_scope says which you are looking at, "site" or "brand". /v1/visibility/runs rejects a brand_id that names no brand on your site, and rejects brand_id together with present=false: a brand is only ever mentioned in an answered run, so that pairing has no results by construction. Sending brand_id= with an empty value selects the rows for scans that ran while no brand was confirmed. Those carry cost and coverage only, so brand comes back null rather than a block of zeroes under someone’s name. /v1/visibility/runs rejects the empty value instead, because those runs have no answers behind them.

GET /v1/visibility/timeseries

Visibility over time, bucketed by scan window.
One point is one scan window, not one day. granularity is always scan_window. A scan does not run all at once. It trickles across its window to spread cost and stay inside provider rate limits, so the individual days inside a window carry whichever runs happened to land on them. Plotting those days produces a sawtooth that looks like violent week to week swings and is an artefact of the schedule. Label each point with its window. That is the part of the scan window your request actually covers, which is not always the whole scan: ask for September 1 to 2 of a scan that ran from August 24 and the point holds two days of runs, so calling it “the 10 days ending September 2” would describe evidence you did not receive. scan_window is what the scan itself covered and partial is true whenever the two differ. A partial point rests on less evidence than a whole one, so the two are not directly comparable. Do not re-bucket these points to days. status and failure_reason describe the scan behind the point. A failed or partial scan produces a point with fewer runs, so a dip in presence that coincides with a failure is a measurement problem, not a visibility problem. A point with scan_id: null holds runs that matched no recorded scan window. Those runs are reported in their own bucket rather than folded into a neighbour, which would move them into a window that did not produce them.

GET /v1/visibility/competitors

Share of voice across every brand you track.
share_of_voice is a share of mentions, not of runs. One answer that names three brands adds to three numerators, so these values relate to each other and not to answered_runs. They sum to 1 across every brand you track, which may include brands beyond the two shown above. There is no brand_id filter on this endpoint. Narrowing to one brand would leave the share of voice denominator holding a single member, and every row would read 100 percent. Each row divides by its own scored_answered_runs, not by the top level answered_runs, so mention_runs / answered_runs will not reproduce presence.rate for a brand you configured part way through the window. Use the value on the row. A brand with archived: true is no longer scanned. It keeps its name so an older series stays labelled.

GET /v1/visibility/by-prompt

One row per prompt you track: presence with its interval, the brands that beat you on that question, and the pages the engines cited for it.
Use this rather than calling /summary?prompt_id= and /competitors?prompt_id= once per prompt. That is two requests per prompt against your per-minute read budget, and the cited-pages half cannot be asked for a single prompt any other way: /citations takes no prompt_id. Prompts come back most-scanned first.

The interval matters more here than anywhere else

A per-prompt sample is a handful of repetitions, not the whole site’s runs, so the bands are wide. Two prompts are only really different when their intervals do not overlap. Ranking prompts by presence.rate alone ranks noise.

runs: 0 is not zero presence

A prompt you configured but that was not scanned in this window comes back with runs: 0 and presence: null. It has not been measured. Rendering it as 0 percent says the engines were asked and did not name you, which is a different and false statement.

top_citations covers evidence_window, not window

The cited-pages list is built from citation detail rows, which are kept for a shorter period than the rates. evidence_window is the window that list was actually read over. It is narrower than window on a long request, and null when the whole request is older than the horizon, while presence still answers for the full window. evidence_retention_days reports how long that detail is kept. Read it from the response rather than assuming a figure. An empty top_citations inside a narrowed evidence_window means unmeasured, not uncited.

top_competitors is the top five, not the list

Up to five brands per prompt, ordered by share of voice, with your own marked is_self. It answers “who beat us on this question”. For the complete brand list use /v1/visibility/competitors.

Optional parameters

brand_id behaves as it does on /summary: it selects the subject of each presence and narrows the rows. totals_scope then reads brand, and top_competitors collapses to that one brand with a null share of voice. Omit it whenever the question is comparative. top_citations is the one field brand_id does not narrow. Restricting it to the subject brand’s own domains would turn “the pages cited instead of ours” into “our pages that were cited”, which is the opposite question.

GET /v1/visibility/citations

Which URLs the engines cited, yours and everyone else’s.

Optional parameters

citations is a total and adds up across days. peak_prompts_in_a_day and peak_engines_in_a_day are the busiest single day, not window totals. The underlying counts are per day distinct counts, and distinct counts do not add: the same prompt citing the same URL on Monday and again on Tuesday is one prompt, not two. A window wide distinct count is not derivable from this endpoint, so Traceten returns the honest per day figure under a name that says what it is. brand_id: null means the cited domain matches no brand you configured. It is neither yours nor a tracked competitor.

GET /v1/visibility/citations/by-page

Your own cited pages, joined to what happened on them.
Both joins are deterministic. They read observations Traceten already stored. Nothing on this endpoint is modelled or estimated.

The crawl side

answer_fetch_crawls counts an AI company fetching this page to answer a question someone asked right then. It is the closest observable proxy for a real user prompt, and Traceten records it already through AI crawler tracking. answer_fetch_crawls is keyed on the URL path alone, because the crawl rollup stores no hostname. If you serve several hostnames, two cited URLs that share a path (example.com/pricing and blog.example.com/pricing, and / most of all) carry the same count, and adding them together overstates. Every row carries answer_fetch_crawls_host_blind: true as the reminder. This count is lower than the same page’s total on /v1/ai-crawls/by-page, and that is correct. This endpoint counts genuine crawls only: not spoofed, and actually graded. The crawl endpoints report every crawl, including impersonators and rows that could not be checked. A spoofed crawl carries the impersonated company’s name, so counting it here would credit a real assistant with an impersonator’s fetch.

The traffic side: two axes

The traffic columns vary along two axes, not one. Mixing them is the mistake this table exists to prevent. Never divide across an axis. ai_sessions counts AI referred sessions only, so landing_revenue_usd_cents / ai_sessions divides all traffic revenue by AI traffic sessions. On a page where AI is 5 percent of visits that overstates revenue per AI session by about 20 times, with every input individually correct and nothing on the response looking wrong. To report revenue per AI session, pair ai_landing_revenue_usd_cents with ai_sessions. Both populations are returned because both answer a real question: what this cited page is worth to the business, and how much of that AI brought in. The landing figures count sessions that started on this page. A visitor who arrived on /guides/attribution, moved to /pricing and bought counts against /guides/attribution. That is what makes it the right number for a cited page: the citation is what brought them in. When landing_revenue.available is false, all four landing fields are absent from every row. The window reaches further back than session level data is kept, so the number is unknown rather than zero. Render a dash. A zero is a measurement, and this is the absence of one. floor_date is the oldest date the join can answer for.

GET /v1/visibility/runs

The raw answers behind every other number on this page.
This endpoint exists so that every number decomposes to the text that produced it. matched_text appears verbatim inside answer_text, so a mention can be highlighted in the answer it came from. model_id and tool_config are recorded per run, so any figure can be reproduced against the same model and the same search settings. Answers are never truncated. Cutting one could cut through a matched span and break the property the endpoint exists for, so pages are small instead. limit accepts 1 to 50 and defaults to 10.

Optional parameters

present: false is a result, not a failure

When an engine renders no answer, Traceten records the run with present: false, an empty answer_text, and an absence_reason. That is a successful measurement of a real outcome. Google declining to render an AI Overview for a question is information about the question. It is not an error and not missing data, and it is what coverage on the summary counts.

Read evidence_window, not the window you sent

Answers are kept for evidence_retention_days. The rates on /summary come from long lived aggregates that go back further. So a request for a year of evidence returns the evidence that still exists, and evidence_window reports the narrower window actually searched. Use it when describing what you got. If the entire request is older than the retention horizon, evidence_window is null and runs is empty, while the summary for the same window still returns a real presence rate.

Confidence is the only signal of a weak match

Traceten stores short or ambiguous brand aliases at low confidence rather than dropping them, so the evidence stays inspectable. A stoplisted alias still reports extraction_method: "alias_exact", because how a match was found and whether it can be trusted are different questions. confidence is the only field that separates them. Mentions below the presence floor appear in this list but were never counted toward the presence rate, so the evidence can legitimately contain more mentions than the rate did. Do not recompute a presence rate by counting rows here. Use /summary.

Configuring what gets measured

Everything above measures a set you choose: the brands Traceten looks for, and the questions it asks. These endpoints manage that set. They need config:write, except the two listings, which need stats:read. The prompt list is the score. Every rate on this page is measured over exactly the questions you track and no others. Add a question you rank badly for and your presence rate falls. Remove it and your presence rate rises, with nothing changed on your site. Neither is wrong, but a visibility figure is only comparable over time while the list is stable, so treat a change here as a change to the measurement. That is also why Traceten will not enrol a suggested prompt for you. If we picked the denominator, we would be setting your grade.

Two rules that surprise people

A prompt’s text and locale can never be edited. A run records the prompt’s id, not its wording, so editing the text in place would change what a year of history claims to have asked. POST /v1/visibility/prompts/{id}/replace archives the old prompt, with its wording and all of its runs intact, and returns a new prompt with a new id. Archiving is the only delete. Brands and prompts are never removed. A brand_id appears in aggregates that are kept indefinitely, and runs reference a prompt_id for as long as they are stored, so a hard delete would leave a chart series that nothing can name.

GET /v1/visibility/brands

The brands matched against every answer. Add include_archived=true to include archived ones.
confirmed: false means the brand is not being measured at all. Traceten can propose a brand (kind: "auto"), and a proposal stays out of every scan until you confirm it. If a brand shows no data, check this first: an unconfirmed brand has no numbers because nothing was looked for. max_brands is 25 per site. It is a matching speed limit, not a plan limit, because every alias of every brand is scanned against every answer. Upgrading does not raise it.

POST /v1/visibility/brands

Track your own brand or a competitor.
self and competitor brands are confirmed the moment you create them, because a person entered them. auto brands are created unconfirmed. Domains are stored as bare, lowercase hosts. A pasted URL such as https://Acme.com/pricing is reduced to acme.com. Anything that is not a host is rejected with a 422 rather than stored, because a stored value that can never match would show as a citation rate of zero with nothing to debug.

Read the warnings array

A 201 can carry warnings. They are not errors and nothing was rejected.
An alias shorter than three characters, or one that is also an ordinary English word, cannot be distinguished from a coincidence in prose. Traceten keeps it and matches it, but scores it at 0.3, below the 0.5 threshold a mention needs to count toward your presence rate. So the alias is saved and it will not move your numbers. Show the warning to whoever entered it: this is the difference between an alias that works and one that quietly never counts. The second warning, aliases_truncated, means the brand’s name, aliases and domains together exceed the 200 terms the matcher uses for one brand, so the ones past that will never produce a mention. The brand’s own name is always matched first. Warnings describe the brand as it was stored, after blanks and duplicates are dropped, not the list you sent.

PATCH /v1/visibility/brands/{id}

Change a brand’s name, kind, aliases or domains. Omitted fields are left alone.
aliases and domains replace the existing lists rather than merging into them. Read the current values first, or the ones you do not resend are dropped. An archived brand cannot be edited and returns 404. Renaming one would relabel a historical series after the fact, which is the opposite of what archiving protects. The response carries the same warnings array as create.

POST /v1/visibility/brands/{id}/confirm

Admit a proposed (kind: "auto") brand into scanning and scoring.
Idempotent. Confirming an already confirmed brand returns it unchanged and does not move confirmed_at.

DELETE /v1/visibility/brands/{id}

Archive a brand. Returns 204.
The brand stops being matched. Its name is kept so existing charts stay labelled, and its aliases and domains are cleared, because those are matching configuration with no labelling value once the brand is never scanned again. That also frees the name for reuse. Archiving your own self brand leaves scans running and billing with nothing to score. Every presence rate then reports that nothing was configured to look for, which is not the same as zero.

GET /v1/visibility/prompts

The questions Traceten asks, and your allowance.
Add status=active, status=paused or status=archived to narrow the list. Omit it for all three. quota.used counts live prompts, meaning active plus paused. Archived prompts never count. A paused prompt costs nothing to run but still occupies an allowance slot, because you are holding it.

Intent buckets

intent groups a question by what it asks. Never average a rate across buckets: they measure different events, so a blended figure moves when the mix of questions moves and nothing about your visibility has changed. topic is a free form label beside it. That one is yours, with no fixed set.

POST /v1/visibility/prompts

Add a question.
locale is part of a question’s identity, not a display preference, because the results page an engine sees differs by region. Every character is billed on every repetition on every engine, so text is capped at 1000 characters. That is a cost boundary, not a formatting rule.

PATCH /v1/visibility/prompts/{id}

Change a prompt’s topic, intent or status.
This endpoint does not accept text or locale. Sending either returns 422 prompt_text_immutable and points at the replace endpoint. status accepts active and paused only. Archiving is DELETE, and an archived prompt cannot be brought back: re-adding the question creates a new prompt and starts a new series.

POST /v1/visibility/prompts/{id}/replace

The only way to change a question’s wording or locale.
This is not an edit. The old prompt is archived with its wording and every run it produced, and a new prompt is created with a new id. Anything holding the old id, such as a saved filter or a dashboard link, now points at history. The new prompt starts an empty series. Its rate is not comparable to the old one’s, because they are different questions. That is precisely why Traceten refuses to edit in place. origin and status carry over from the archived prompt, and omitted fields keep their previous values. A one for one swap never fails on your allowance, because the archive happens first.

DELETE /v1/visibility/prompts/{id}

Archive a prompt. Returns 204.
This changes the denominator of every rate from the next scan onward. Past windows keep their numbers, so a presence rate measured before and one measured after are not over the same set of questions. Any comparison across that point should say so. Past runs are kept and stay attached to the archived prompt. This cannot be undone.

POST /v1/visibility/prompts/suggest

Propose questions built from your own site.
Nothing is saved. The candidates come back in the response and no prompt is created. To track one, post it to /v1/visibility/prompts with origin: "suggested". A candidate matching a question already on the site is filtered out, including one you have archived, so a question you removed on purpose is not offered back.

Your site must be receiving events first

If the site has never reported a tracking event, this endpoint returns 409 site_not_verified and fetches nothing. Traceten will only fetch a domain you have shown you control, and a received event is the only evidence of that we have. Install the snippet and load a page, then try again. You can add prompts by hand in the meantime.

How Traceten reads your site

It starts at the domain on your site record and fetches over HTTPS only, identifying itself as Traceten-SiteReader/1.0. It reads your robots.txt first and honours a Disallow that covers a page it would have read, including the * and $ wildcards. It follows sitemap.xml and your homepage navigation, and stops at 8 pages, 12 requests or 15 seconds, whichever comes first. Every request is pinned to your host and its www variant. That includes redirects: a hop that leaves your site is refused rather than followed, so nothing off your domain can be read through this endpoint. It reads the title, meta description, headings and navigation labels, and fills templates with them. No AI model is involved and no third-party provider is called. Nothing is stored, and no page content is logged. Because it makes a real outbound request from Traceten’s infrastructure, this endpoint needs config:write rather than a read permission, even though it saves nothing. One crawl runs per site at a time. A second request while one is in flight returns 409 suggest_in_progress.

Read the candidates before you create them

Each candidate’s topic is a phrase copied verbatim from one of your pages, and its text is that phrase inside a template. Headings and navigation labels are ordinary marketing copy, but they are whatever you put on the page, so a candidate can contain a person’s name if a heading does. That matters because a prompt you create is sent to third-party answer engines on every scan, for as long as you track it: Perplexity and DataForSEO (which runs it as a Google search, so it reaches Google too) on every plan, and Anthropic and OpenAI as well if you have the engine coverage add-on. See AI answer engines for exactly what each one receives. Traceten drops anything containing @, so an email address in a navigation label never becomes a candidate, but it cannot tell a person’s name from a product name. Read the list before accepting it. Treat the results as a starting point to edit. They are templates built from your own words, not evidence that real buyers ask these questions. Nothing here validates that. pages_read: 0 with a failure means the site could not be read. unreachable means no page answered, robots_disallowed_all means your robots.txt blocks us, and no_domain means the site record has no usable domain. Brand name candidates are still returned, because they need no page content.

Configuration errors

A duplicate question is a 409 rather than a silent success, because asking the same thing twice pays twice and then averages two samples of one thing as though they were two.

Privacy

answer_text is prose written by a third party answer engine, returned unmodified. It can name people, because answers about a market name the people in it. The same applies to sources[].title, the cited page’s own title copied verbatim. A cited profile page arrives as its own title, for example “Jane Doe, VP Product at Acme”. Treat titles like answer text, not like URLs.
  • It is stored for evidence_retention_days and then deleted.
  • It carries no visitor identifier and is never joined to one. Nothing in these responses describes a person who visited your site.
  • The long lived aggregates behind /summary and /timeseries hold counts only. They contain no answer text and no query strings.
  • /v1/visibility/runs responses, including answer_text, are cached for 60 seconds in Traceten’s managed Redis cache (Upstash) so a page refresh does not re-read the evidence table. That is a second location answer text briefly occupies, it expires on its own, and it is keyed per site and per query.
Cited URLs have their query strings and fragments removed before storage, so a token or an email address in the cited URL list is dropped before it is written. That protection covers the URL list only. A URL written out inside the prose of an answer is part of answer_text and is stored verbatim, exactly as the engine wrote it, for evidence_retention_days.

Errors

timezone is rejected rather than ignored. These figures are stored at UTC day grain, so a local day boundary cannot be honoured, and answering in UTC under another zone’s label would be wrong.

Next