curl --request GET \
--url https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/sources \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/sources"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/sources', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"data": [
{
"domain": "<string>",
"citation_count": 0,
"cited_rate": 123,
"category": "<string>",
"influenceability": "<string>",
"opportunity_score": 0
}
],
"pagination": {
"next_cursor": "<string>",
"has_more": true
}
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}List cited sources
Cited domains over a window.
Citations aggregated over the own brand’s finalised run units in the window (unit grain), enriched with the brand’s latest opportunity score and classification for the domain: category and influenceability are RELATIVE TO THIS OWN BRAND, read from its latest source action for the domain, and null for a domain the brand has no action for — never the global dictionary (a sibling own brand’s domain is owned there but other here, with influenceability competitor-owned). Ordered by citation_count desc, then domain asc in code-point order (plain string comparison of the lower-cased domain, not locale collation) — the cursor continues from exactly that tuple. The aggregate is computed per request over every unit in the window, so the span is capped at 90 days (400 validation_error, details[].code = "window_too_wide" beyond it). Pages are consistent within one scan generation only: the cursor is anchored to the brand’s change token (last_scan_changed_at), and once a scan finalises between pages the next page is 400 invalid_cursor with details.reason = "stale" — restart from the first page. The token is checked again after the aggregate is computed: if it moved, the aggregate is recomputed once, and if it moved again the answer is 409 scan_in_progress with Retry-After (5s) — units are finalising faster than they can be summed, so expect it while a scan runs. It is checked once more after the per-domain enrichment (the brand’s latest source actions and the classification): replacing a brand’s source actions moves the token too, so a move there is 409 on a first page and 400 invalid_cursor (stale) under a cursor — every field of a page belongs to one generation. Captured forward-only from 2026-08-19. Own brands only.
curl --request GET \
--url https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/sources \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/sources"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/sources', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"data": [
{
"domain": "<string>",
"citation_count": 0,
"cited_rate": 123,
"category": "<string>",
"influenceability": "<string>",
"opportunity_score": 0
}
],
"pagination": {
"next_cursor": "<string>",
"has_more": true
}
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}Authorizations
A Surfais API key — an organisation key or a partner key. Issued by Surfais; shown once. Rotate by creating a new key, then revoking the old one.
Path Parameters
Organisation id. Org keys: the key's own org. Partner keys: any org with an active link. Anything else is 404.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$Brand id within the org (own brand or competitor row); 404 when not in the org. Writes that add tracking need an own (400 validation_error with details[].code = "own_brand_only"), non-archived (409 brand_archived) brand.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$Query Parameters
Page size (1–200). Default 50.
1 <= x <= 200Opaque cursor from a previous page's pagination.next_cursor. It pins the walk — its sort anchor, the resolved date window and the FILTERS the first page ran under — so continue with the cursor alone: omitted filters are restored from it. Naming a different value for one of them is 400 invalid_cursor (details.reason = "filter_changed", details.parameter the one that differs); start again from the first page to change a filter.
2048Window start (inclusive). Default: to minus 30 days.
^\d{4}-\d{2}-\d{2}$Window end (inclusive). Default: today (UTC). Max span 90 days; from after to is 400 validation_error. On a paginated endpoint the resolved window is pinned by the first page and carried in the cursor — continue with the cursor alone; naming a different from/to on a continuation is 400 invalid_cursor (details.reason = "window_changed").
^\d{4}-\d{2}-\d{2}$