Skip to main content
API and MCP access is included on all plans. Browse the API Reference for the full endpoint contract.
The Reports API is one endpoint per report entity. You send a small JSON body that says what to group by, what to compute and what to keep. You get back rows plus a column schema, so the same response renders in a spreadsheet, a BI tool, a script or an AI assistant. Every report page and widget in Surfacd can be rebuilt with it. The entity pages show the exact body for each one:

Visibility

Visibility, mentions, position, sentiment and rank, or one row per AI answer. Dashboard, Rankings, Analyse, Prompts and Brand Mentions.

Sources

Cited domains, hostnames, URLs and pages. Sources overview, tables, gaps and page sentiment.

Narratives

Reputation narratives, themes and sentiment. Requires the Reputation feature.

Endpoints

All three use a team API key as a Bearer token. {project} is the project UUID from GET /api/v1/projects.

Your first query

Visibility per brand over the last four weeks, best first:
columns tells you how to format each cell. rows are raw values keyed by column id. A percent column is a decimal: 0.203 is 20.3%.

The request

Rows and dimensions

A row is one dimension group.
  • "dimensions": ["brand"] is one row per brand.
  • "dimensions": ["brand", "service"] is one row per brand per platform.
  • "dimensions": ["brand", "date"] is a timeline, one row per brand per day. week and month bucket the same way. Use one time bucket per query.
  • "dimensions": ["service"] on visibility, without brand, aggregates across the project’s brands: one row per platform.
  • No dimensions at all gives one project-level row, which is what a KPI card needs.
A brand with no mentions in the window still gets a row, zero-filled, so rankings and charts stay complete. Every dimension with a name also carries an id column: brand comes with brand_uuid, prompt with prompt_uuid, topic with topic_uuid, and so on. Prompts without a topic form one group whose topic and topic_uuid are null.

Filters

Every filter field has a role. The role decides where the condition runs and which operators it accepts. You never choose between a where and a having; the field does. Two things follow from this:
  • “ChatGPT only” changes visibility. service in ["chatgpt"] makes visibility = mentions on ChatGPT ÷ answers on ChatGPT, the same as the platform filter in the app.
  • A brand filter does not. brand in ["…"] keeps one row and drops the rest. That brand’s visibility is exactly what it was with no filter. Use it to pick your brand for a KPI, not to change the maths.
in and not_in take values (a list). Comparisons take a numeric value. contains takes a text value and matches the label case-insensitively. The wrong pairing is a 422.

Population filter values

No service filter means the project’s active platforms, the same default as the rest of the API. The content cuts, mentions_brand, cites_domain and (on sources) mention_sentiment, make “answers that name us” or “answers that cite Reddit” the whole population. Every ratio is then a share of those answers. Add the answers measure to see the denominator.

Compare window

compare_start_date and compare_end_date run the same query over a second window. Same filters, other dates. Every measure then gains two columns, right after it:
  • Both work in filters and order_by. “Movers” is order_by: [{ "field": "share_change", "dir": "desc" }]. “Dropped 10 points” is { "field": "visibility_change", "op": "lte", "value": -0.1 }.
  • Rows match on their dimension keys. A date, week or month row matches by position from each window’s start: day 1 pairs with day 1, so a 28-day chart overlays the previous 28 days. A day with no answers leaves a hole and does not shift the days after it. Windows of different lengths pair from the start; the longer tail reads null.
  • A single-row result also fills benchmarks[0] with value, difference, change and favourable for the first measure, so a KPI card can show its trend. avg_position counts down as good.
  • Ratios compare fairly across unequal windows. Counts do not: 186 mentions over 28 days against 71 over 14 is not a trend.
  • Visibility with the answer dimension is a list and takes no compare window.

Paging and rank

  • limit and offset cut the rows. page.total in the response is the row count before the cut, so you can build a pager. It is correct even on a page past the end.
  • The rank measure numbers rows within their group by the entity’s main measure: visibility on visibility, citations on sources. It is assigned before any entity or measure filter, so { "field": "rank", "op": "lte", "value": 3 } on ["brand", "service"] is “the top three brands on each platform” and does not renumber anyone.
  • top keeps the best values of one dimension over the whole window, on every row. "top": { "field": "brand", "by": "visibility", "limit": 6 } on ["brand", "date"] gives the six most visible brands for every day, where limit: 6 would give six rows. The field must be in dimensions and cannot be date, week or month. Measures where lower is better, such as avg_position, rank ascending. It hides rows and never changes a number.
  • contains on an entity field is your search box: { "field": "url", "op": "contains", "value": "reddit" }.
Some entities offer extra per-row lists that would not fit a flat row: the brands named on a cited page, the sources an answer cites, a narrative’s evidence. Ask for them in related; they come back under related in the response, one list per block. Each entry carries row, the index of its row in rows, so filtering or sorting rows on your side never loses the link.

The response

Finding identifiers

Filters take public identifiers only: UUIDs, slugs, codes and names. Two endpoints tell you what to send. The schema endpoint lists what an entity accepts. Its filter_fields carry the role and operators of every field:
The values endpoint lists what a field takes for your project, with a search:
Put value in the filter. label and description are for people: the primary brand is marked, domains carry their citation count, narratives their sentiment and theme. Fields with a list: brand, mentions_brand, page_brand, narrative, carries_narrative, theme, carries_theme, prompt, topic, tag, service, country, domain, cites_domain, page_type, category, brand_on_page, mention_sentiment, page_sentiment, narrative_sentiment.

Errors

Validation problems return 422 with errors keyed by the offending path, so a wrong body tells you where. 403 means the query reads reputation data (the narratives entity, or a carries_narrative / carries_theme filter) and the team does not have the Reputation feature. 404 means the project is not in the key’s team.

Deprecated endpoints

The Reports API replaces these single-report endpoints. They keep working until a removal date is announced. New integrations should use the report query. All paths start with /api/v1. The detail endpoints (one theme, narrative or response by UUID), mentions and visibility-decomposition are not deprecated.

Using it from an AI assistant

The MCP server exposes the same capability as tools: run_report runs a query, get_report_schema and list_report_values describe what it accepts, and get_report_query_guide explains how to write one. An assistant connected to Surfacd can build any of the reports on the entity pages on request.