Skip to main content
This guide copies results out of Surfais in two stages: a one-off backfill of a brand’s history, then an incremental sync that re-reads only the brands that changed. Read Data model first if result units, mentions and the change token are new to you.

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 walks GET …/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:
  • 429 and 503: it waits for Retry-After seconds, 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_cursor with details.reason set to stale, or 409 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-Remaining reaches 0, it sleeps until X-RateLimit-Reset.
Run it with the organisation id, the brand id and an inclusive date window:

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, with 409 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.
The token tells you that something changed, not what. It also moves for changes that leave results alone, such as an edited brand name, so some pulls find nothing new.
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

  • from and to are inclusive UTC dates, written YYYY-MM-DD. to defaults to today, and from to 30 days before to.
  • 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_error with details[].code set to window_too_wide. For a longer history, run one walk per consecutive window.
  • Mentions and results accept platform and country filters. Sources accepts neither.
  • The first page fixes the window and the filters. Continue with cursor and limit alone, as the script does. Naming a different value on a later page answers 400 invalid_cursor, with details.reason set to window_changed or filter_changed. To change either, start a new walk.
Pagination and date windows covers cursors in full.

Truncated brand lists on result units

GET …/results lists each tracked brand’s mention in the unit’s brands array:
Response
A unit lists at most 250 brands. When it held more, 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-After on 429 quota_exceeded counts 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/summary page tells you which brands changed. Read the per-brand endpoints only for those.