Choose the endpoint
All three serve finalised results only, and mentions and sources take own brands only. Mentions and results come newest date first. Without
limit, a page holds 50 rows.
GET …/mentions returns one row per result unit, whether or not the brand was mentioned, so you can compute presence rates from it:
Response
Backfill a brand’s history
The script below walksGET …/mentions for one own brand and one date window, and writes each row as a line of JSON. It handles every failure the walk can meet:
429and503: it waits forRetry-Afterseconds, then repeats the request. It stops instead of sleeping for more than five minutes, which is what an exhausted monthly quota asks for.- A void walk:
400 invalid_cursorwithdetails.reasonset tostale, or409 scan_in_progress. Both mean the brand’s data changed under the walk. The script waits until the brand’s change token stops moving, then starts again from the first page. - Partial output: it writes to a temporary file and renames it when the walk completes, so the output file is never partial.
- The minute allowance: when
X-RateLimit-Remainingreaches0, it sleeps untilX-RateLimit-Reset.
Why a walk can become void
Every page of a walk belongs to one state of the brand’s data. If the brand’s change token moves while you page, the API refuses the next page instead of serving rows from two states. While a scan of the brand runs, the token moves with every result, so expect refusals then. A large first page can be refused the same way, with409 scan_in_progress and a Retry-After.
The remedy is always the same: discard the rows from the abandoned walk and start again from the first page. Reading results while a scan is running has the full rules.
Keep the copy in sync
After the backfill, do not walk every brand on every run. Let the change token tell you which brands to read.1
Store a token and a date for each own brand
Keep the last
changed_at you synced, and the UTC date you synced on.2
List the summaries
Call
GET /orgs/{orgId}/brands/summary with limit=200, the largest page. One call covers up to 200 own brands. Follow next_cursor if has_more is true.3
Skip the brands that did not change
Compare each row’s
changed_at with the stored value, for equality only. Skip the brand when they match. Skip it too when changed_at is null: nothing has been recorded for the brand yet.4
Re-pull a short trailing window for the rest
Walk
GET …/mentions from the day before your last sync to today, with walk_mentions from the backfill script (walkMentions in Node.js). Routine scans only add or replace units on the newest dates, so a short window is enough for them.5
Upsert on the unit's natural key
The key is
prompt_id, platform, country and run_date. A second scan on the same day replaces a unit, so an insert-only load would store it twice.6
Store the token you read in step 2
Store the
changed_at from the summary, not one you read after the pull. If more data arrived while you were pulling, the stored token is already out of date, and the next run pulls the brand again.A trailing window does not see every change. A unit can disappear: deleting a prompt in the dashboard removes its results. An older unit can also change: when a brand alias is added or removed, Surfais recounts the brand’s recent results. If your copy must match exactly, re-run the backfill over a longer window from time to time, and replace that window in your copy rather than upserting into it.
Windows and filters
fromandtoare inclusive UTC dates, writtenYYYY-MM-DD.todefaults to today, andfromto 30 days beforeto.- A window can span at most 366 days on mentions and results, and at most 90 days on sources. A wider one answers
400 validation_errorwithdetails[].codeset towindow_too_wide. For a longer history, run one walk per consecutive window. - Mentions and results accept
platformandcountryfilters. Sources accepts neither. - The first page fixes the window and the filters. Continue with
cursorandlimitalone, as the script does. Naming a different value on a later page answers400 invalid_cursor, withdetails.reasonset towindow_changedorfilter_changed. To change either, start a new walk.
Truncated brand lists on result units
GET …/results lists each tracked brand’s mention in the unit’s brands array:
Response
brands_truncated is true and brands_total says how many there were. Mentioned brands are kept first, then brands by position, so a truncated list is the top of a ranking. It is not a complete list of mentions: a unit that mentions more brands than the cap loses some of them.
Within the array, entries are ordered by brand_id. The cap never affects GET …/mentions, which returns one brand’s mention per unit.
Be a good citizen
- Export when the data is quiet. Start bulk walks once the brand’s change token has settled. Scheduled scans start early in the UTC day, so a daily export is better placed later. See Scan schedule & cadence.
- Use smaller pages while a scan runs. A smaller first page is less likely to meet
409 scan_in_progress. - Watch
X-RateLimit-Remaining. Your limits are set on your key, so read them from the response headers and pause before you reach zero. See Rate limits and quotas. - Do not sleep on a monthly quota.
Retry-Afteron429 quota_exceededcounts to the end of the month. Stop the job and raise it with your team instead. - Ask the summary first. One
GET /orgs/{orgId}/brands/summarypage tells you which brands changed. Read the per-brand endpoints only for those.