Skip to main content
Surfais asks your prompts to five AI platforms and records what each answer says about your brands. The API serves those records and the numbers computed from them. This page maps the resources and states the rules to build on. It is a reference: go to the section you need. For how to walk a list, see Pagination and date windows.

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 same 404 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.
  • country is 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.
Scores work differently. The scores for a scan date are published when the scan has finished, not unit by unit.

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_ref or 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
To ask “has anything changed?”, poll the change token and compare it for equality. Do not poll last_scan_at: it misses every change that is not a new result. GET /orgs/{orgId}/brands/summary returns the token for every own brand in one list.
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.
next_scheduled_scan is computed when you ask, from the schedule and the clock. The change token does not cover it, so do not cache it until the token moves. Read it when you need it. It is a plan, not a promise: a scheduled scan can be delayed or fail.

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_version names 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 is null on older points that were scored before versions were recorded.
  • panel_version names the measurement panel: the set of AI models that were queried. It changes independently of score_version, because the panel can be updated without changing the formula. It is null on older points too.
Treat both as opaque tags and compare them for equality only. Compare two points only when both tags match. A difference between points from two scoring models, or two panels, is not a movement in the brand’s visibility. The API applies this rule itself. 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.
A field wider than 1,000 members, counting the own brand and its competitors, cannot be measured in one snapshot. The endpoint answers 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 empty citations 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 has sunset_at set.
  • It is left out of GET /orgs/{orgId}/brands and GET /orgs/{orgId}/brands/summary.
  • It stays readable by id, and so does its history.
  • A write that adds tracking answers 409 brand_archived, with details.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.
The same write succeeds once the brand is restored. See Errors.

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.