The hierarchy
Competitors are linked per own brand.
GET /orgs/{orgId}/brands/{brandId} lists an own brand’s competitors. One competitor can be linked to several own brands, and an own brand can itself be tracked as another own brand’s competitor.
Each own brand also has aggregates computed from its result units:
The summary has one row per own brand. There is no organisation-level score: combine the rows yourself if you need one.
The raw text of AI answers is not served. See what v1 does not include.
Prompt fields
GET /orgs/{orgId}/prompts returns active and inactive prompts. Filter with brand_id, active, tag and country. To change what a brand tracks, see Sync prompts.
Identifiers and your own references
Organisation, brand and prompt ids are UUIDs. A path id that is not a UUID answers the same404 not_found as an id that is not in the organisation.
Dates are UTC calendar dates, written YYYY-MM-DD. Timestamps are RFC 3339.
external_ref lets you store your own identifier and get it back, so you do not need a mapping table.
With an organisation key, the organisation’s
external_ref is always null.
A brand’s external_ref is unique within the organisation, compared without regard to case. Provisioning a second brand with the same value answers 409 external_ref_exists. See Provision a property.
Only finalised results are served
Results, mentions and sources include a result unit only once it is final. After an answer is parsed, a later check can still withdraw a mention, so a unit that is still being processed is never served.- Preview runs are excluded. Runs made for the preview during onboarding never appear.
- One unit per prompt, platform, country and scan date. A second scan on the same day replaces the earlier unit. It does not add a row.
countryis always upper-case on a unit, whatever case it was stored in.- Units arrive one by one. A scan finalises its units as it goes, so a read made during a scan sees part of that day’s units. See reading results while a scan is running.
- A unit can disappear. Deleting a prompt in the dashboard removes its results.
Mention fields
Freshness: two different fields
Brand detail and the summary each carry a scan time and a change token. They answer different questions.
The change token moves when:
- a result unit is finalised, withdrawn or deleted, or a scan is stopped early
- the brand’s scores are published
- the brand’s cited-source ratings are refreshed
- the brand itself is edited: its name, domain, geography,
external_refor archived state - a competitor is linked or unlinked, or a linked competitor’s name or domain changes
- a prompt is moved to or from the brand
- a result’s mentions or citations are corrected, for example after a brand alias changes
- the organisation’s scheduling inputs change, such as its plan or scan frequency
next_scheduled_scan on brand detail is the next UTC date the scan schedule will queue a scan for the brand. It is null when nothing is scheduled: a plan with no schedule, an organisation set to on-demand scanning, a competitor, an archived brand, or an organisation that has not completed its first scan.
Scores
GET /orgs/{orgId}/brands/{brandId}/scores serves three different series. Points are in ascending date order.
The AIS Score cannot be split by country, so there is no per-country AIS Score. The per-market series is a separate index: do not expect its values to sum or average to the all-market point.
?country= must be a market Surfais scans. Any other two-letter code answers 400 validation_error with details[].code set to unsupported_country. A supported market with no data returns an empty list.
Every point on the first two series also carries composite_score, a diagnostic composite. It is not the headline either. score can be null, for example for a competitor that several own brands track.
score_version and panel_version
Each score point records two version tags.
score_versionnames the scoring model that produced the point. It is stored with the point when it is scored. It is never the model in use today. It isnullon older points that were scored before versions were recorded.panel_versionnames the measurement panel: the set of AI models that were queried. It changes independently ofscore_version, because the panel can be updated without changing the formula. It isnullon older points too.
GET /orgs/{orgId}/brands/summary reports both tags on latest and previous, and withholds the comparison when they disagree: delta is null and delta_reason says why.
The scoring model is tested first, so a pair that differs on both reports
score_version_mismatch. delta_reason is null when delta is served. It is also null when latest or previous is missing, or when either score is null, because there is nothing to subtract.
GET /orgs/{orgId}/brands/{brandId}/share-of-voice follows the same rule. Its response names one score_version and panel_version pair, and a point from a known different pair is left out of the ais_score means only.
If you alert on score movement yourself, reset your baseline whenever either tag changes.
Share of voice
GET /orgs/{orgId}/brands/{brandId}/share-of-voice returns the own brand and each of its competitors over a date window. The own brand comes first (is_self: true), then the competitors by sov, highest first.
Every entry is measured on the requested own brand’s scans: the answers to its prompts. A competitor that two own brands track can therefore show different numbers in each brand’s share of voice.
ais_score is null when no such point exists. It is also null for a competitor that several own brands track, and for an own brand that appears here as a competitor. The entry’s mentions, visibility and days are still reported.
sov on GET /orgs/{orgId}/brands/summary follows the same rule on the brand’s latest scored date: the brand’s mentions divided by its own plus its competitors’ mentions.
The API’s
sov is measured across the tracked field: the own brand and the competitors it tracks. The Share of Voice signal inside the AIS Score also counts brands you do not track, so the two numbers can differ.500 field_too_large instead of a share computed against part of the field. Do not retry it. Track fewer competitors for that brand, or read each brand’s own series through GET /orgs/{orgId}/brands/{brandId}/scores.
What a competitor id can do
The refusal is a
400 validation_error whose details[].code is own_brand_only. See Errors.
A competitor has one score series, not one per own brand that tracks it. When several own brands track the same competitor, each point holds the numbers from whichever own brand’s scan scored it last. The same caveat covers an own brand that another own brand tracks as a competitor. For a competitor’s numbers in one own brand’s field, use share of voice.
Citations and sources
Citations are captured from 19 August 2026 onward. Earlier result units return an emptycitations list, and GET /orgs/{orgId}/brands/{brandId}/sources counts nothing before that date.
Each citation on a result unit has a url, a domain and cited: true when the answer attributed the source, false when the source was retrieved but not attributed, and null when that is unknown. Citations are listed in the order they were retrieved.
GET …/sources aggregates citations by domain over a window:
Treat
domain as an opaque key, not as a hostname to resolve. It is stored as the answer cited it, so it can hold values that are not valid DNS names.Countries and tags
Countries are market tokens.countries on a prompt, and country on a result unit, always come back upper-case. They are ISO 3166-1 alpha-2 codes for every prompt written through this API or the dashboard’s country picker. Prompts created another way, such as during onboarding or by CSV import, can hold other values, such as OTHER. Treat a token as opaque and pass it back as you received it.
?country= on prompts, results and mentions is case-insensitive and accepts any token the API returns. A token that nothing holds returns an empty page, not an error. The one strict case is ?country= on the score series, which must be a market Surfais scans. See Countries & markets.
Tags are free text, and case is part of the tag. Launch and launch are two different tags. ?tag= matches exactly: it is case-sensitive and is not trimmed. A tag that nothing holds returns an empty page.
Archived brands
A plan downgrade archives the own brands that the new plan no longer covers, and deactivates their prompts. An archived brand hassunset_at set.
- It is left out of
GET /orgs/{orgId}/brandsandGET /orgs/{orgId}/brands/summary. - It stays readable by id, and so does its history.
- A write that adds tracking answers
409 brand_archived, withdetails.brand_id. That covers creating or syncing prompts, editing or reactivating one of its prompts, linking a competitor and requesting a scan. - Reductions still work: deactivating one of its prompts, and unlinking a competitor.
Usage and caps
GET /orgs/{orgId} returns usage: what the organisation holds, against its plan’s caps.
usage
A
cap of null means the cap is not enforced for this organisation. The competitor cap is the exception: it applies to every organisation, so it is reported even where the other three read null.
The numbers above are examples. Read your own from the response, and see Plans & limits for what each plan includes.
A write that would exceed a cap answers 422 with a code that names the cap. See Errors.