◆ Sapport for developers API reference Error catalog OpenAPI
Sections

#Strategist reports

Status: Available (reading). GET /strategist/reports and GET /strategist/reports/{id} work (scope strategist:read). Reports are built on aggregates; competitor names are hidden, and the analytics snapshot, the model and the run cost are not returned.

The strategist is Sapport's AI marketer. It collects the data of your site and advertising (traffic, search queries, articles, competitors, the CRM funnel in counters) and, once a day and once a week, prepares a report: what happened, what is worrying, and what is worth doing. The reports API hands this report to your system as a structure, not as ready-made text for a messenger.

Access: strategist:read. Paid: no (reading a finished report). PII: no. The report is built from aggregates and counters; it contains no personal data of students or leads.

The public API serves two kinds of strategist reports: daily (daily) and weekly (weekly). Runs of other modules that live in the same reports table (for example, creatives) are not served through the API.

#Base URL

CellURL
RUhttps://formula-cream.pro/api/public/v1
ENhttps://wfacademy.org/api/public/v1
IDhttps://wfacademy.id/api/public/v1

Errors, pagination, and limits: Conventions. Keys and scopes: Authentication. In all examples, the key is deliberately fake.

#Endpoints

Method and pathScopeWhat it does
GET /strategist/reportsstrategist:readA list of reports: headers without the body
GET /strategist/reports/{id}strategist:readOne report in full

The report is created by the strategist itself on a schedule. You cannot order a report with an API request. The list is cursor-based (Pagination).

#Report structure

A report answers three questions: what matters most, what is worrying, and what to do.

BlockFieldWhat is inside
HighlightshighlightsShort facts for the period
ConcernsconcernsProblems and risks: metric drops, gaps in the schedule, "Don't do" items
RecommendationsactionsWhat to do, with a priority, an expected effect, and evidence
EvidenceevidenceCases and data that the recommendations rely on

#Report header (a list item)

FieldTypeDescription
idstring (uuid)The report ID
report_datestring (date, YYYY-MM-DD)The report date
report_typestringThe kind of report: daily or weekly
summary_textstringA short summary
actions_countintegerThe number of recommendations
data_as_ofobjectAs of when the report's data is current: {indicator: date YYYY-MM-DD} (for example, {"gsc": "2026-10-09", "tracker": "2026-10-11"}); the snapshot key is the date of the analytics snapshot the report is built on. An empty object if there is no snapshot
delivered_atstring (ISO 8601) | nullWhen the report was delivered to the Telegram bot; null if it was not delivered

The list returns only headers: the report body is never returned in it, whatever the set of permissions.

#Full report

FieldTypeDescription
idstring (uuid)The report ID
report_datestringThe report date
report_typestringThe kind of report: daily or weekly
summary_textstringA short summary
data_as_ofobjectAs in the header
highlightsarrayHighlights
concernsarrayConcerns
actionsarray of objectRecommendations (below)
evidenceobjectEvidence (below)

References to internal objects (an article, a query, a product) in the text of highlights, concerns, and rationale are replaced with readable labels.

#Recommendation (actions[])

FieldTypeDescription
titlestringWhat to do
prioritystringhigh, medium, or low
rationalestringWhy it is needed
target_refstring | nullWhat it affects: a reference to an object (an article, a query, a product)
expected_effectobject | nullThe expected effect: metric (the metric), delta (the change), horizon_days (the term in days)
check_after_daysinteger | nullAfter how many days to check the result
basisstringWhat the recommendation rests on (the table below)

The fields target_ref, expected_effect, check_after_days, and basis are in the structure of new reports. In reports created before they appeared, these fields are absent or empty (see "Open questions").

#Basis of a recommendation (basis)

ValueMeaning
own_measuredMeasured on your data
observed_patternObserved at competitors
researchRelies on external research
hypothesisAn assumption without confirmation. This is the default value

A recommendation without a single case is marked hypothesis: the strategist calls a guess a guess.

#Evidence (evidence)

Up to three pieces of evidence are attached to each recommendation.

FieldTypeDescription
evidence.actions[]arrayOne element per recommendation (in the order of actions)
evidence.actions[].indexintegerThe recommendation's number
evidence.actions[].statusstringsupported (confirmed by evidence) or unsupported
evidence.actions[].basisstringThe basis (as in the table above)
evidence.actions[].items[]arrayEvidence
evidence.actions[].items[].refstringA reference to the source
evidence.actions[].items[].kindstringown (your data), competitor (a competitor), pulse (the pulse), signal (a signal); the set may grow
evidence.actions[].items[].labelstringThe source's label
evidence.actions[].items[].urlstringThe page address (optional)
evidence.actions[].items[].quotestringA quotation (optional)

Labels, addresses, and quotations are filled in by the server from the data that it itself gave to the strategist; the model sends only a reference. That is why there are no invented addresses in the evidence.

Evidence checking is honestly limited. The server checks that the reference was issued by the server itself and that the quotation was taken from the source. It does not check that the case actually supports the conclusion. The field evidence.validation.relevance_checked is always false.

#A slice without competitor raw data

In v1, competitor raw data (the competitors.raw permission) is unavailable to an integration, so the report is always trimmed: raw_signals (the analytics snapshot), the model, and the run cost are not returned, and competitor domains and model details are hidden in the evidence. The structure of the blocks is preserved. Creative reports are not served at all.

#GET /strategist/reports

ParameterTypeRequiredConstraintsExample
limitintegerno1-100, 25 by default10
cursorstringnoThe next_cursor value; valid for 24 hours
report_typestringnodaily or weeklydaily
{
  "data": [
    {
      "id": "3d8f1b52-6a47-4c9e-b0d3-9e51a2c7f804",
      "report_date": "2026-10-11",
      "report_type": "daily",
      "summary_text": "Трафик из поиска вырос, конверсия в заявку без изменений.",
      "actions_count": 3,
      "data_as_of": { "gsc": "2026-10-09", "tracker": "2026-10-11" },
      "delivered_at": "2026-10-11T06:10:04Z"
    }
  ],
  "next_cursor": null
}
curl "https://formula-cream.pro/api/public/v1/strategist/reports?limit=10" \
  -H "Authorization: Bearer $SAPPORT_API_KEY"
# SAPPORT_API_KEY=sap_ru_test_EXAMPLE0KEYID_… (an example, not a real key)
const res = await fetch(
  "https://formula-cream.pro/api/public/v1/strategist/reports?limit=10",
  { headers: { Authorization: `Bearer ${process.env.SAPPORT_API_KEY}` } },
);
const { data, next_cursor } = await res.json();
r = httpx.get(
    "https://formula-cream.pro/api/public/v1/strategist/reports",
    params={"limit": 10},
    headers={"Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}"},
)
page = r.json()
$ch = curl_init("https://formula-cream.pro/api/public/v1/strategist/reports?limit=10");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SAPPORT_API_KEY")],
]);
$page = json_decode(curl_exec($ch), true);

#GET /strategist/reports/{id}

ParameterTypeRequiredConstraintsExample
idstring (uuid)yesA report of your tenant3d8f1b52-6a47-4c9e-b0d3-9e51a2c7f804

Someone else's or a nonexistent report returns 404 not_found.

{
  "id": "3d8f1b52-6a47-4c9e-b0d3-9e51a2c7f804",
  "report_date": "2026-10-11",
  "report_type": "daily",
  "summary_text": "Трафик из поиска вырос, конверсия в заявку без изменений.",
  "data_as_of": { "gsc": "2026-10-09", "tracker": "2026-10-11" },
  "highlights": [
    "Переходы из поиска выросли на странице про увлажняющие кремы"
  ],
  "concerns": [
    "Нет новых статей уже 12 дней"
  ],
  "actions": [
    {
      "title": "Выпустить статью про выбор увлажняющего крема",
      "priority": "high",
      "rationale": "Запрос растёт, статьи по теме на сайте нет.",
      "target_ref": "keyword:EXAMPLE",
      "expected_effect": { "metric": "переходы из поиска", "delta": "+10%", "horizon_days": 30 },
      "check_after_days": 21,
      "basis": "own_measured"
    }
  ],
  "evidence": {
    "actions": [
      {
        "index": 0,
        "status": "supported",
        "basis": "own_measured",
        "items": [
          { "ref": "own:EXAMPLE", "kind": "own", "label": "Поисковые запросы за 28 дней" }
        ]
      }
    ]
  }
}
curl "https://formula-cream.pro/api/public/v1/strategist/reports/$REPORT_ID" \
  -H "Authorization: Bearer $SAPPORT_API_KEY"
const res = await fetch(
  `https://formula-cream.pro/api/public/v1/strategist/reports/${reportId}`,
  { headers: { Authorization: `Bearer ${process.env.SAPPORT_API_KEY}` } },
);
const report = await res.json();
for (const action of report.actions) {
  console.log(action.priority, action.title);
}
r = httpx.get(
    f"https://formula-cream.pro/api/public/v1/strategist/reports/{report_id}",
    headers={"Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}"},
)
report = r.json()
for action in report["actions"]:
    print(action["priority"], action["title"])
$ch = curl_init("https://formula-cream.pro/api/public/v1/strategist/reports/" . $reportId);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SAPPORT_API_KEY")],
]);
$report = json_decode(curl_exec($ch), true);

#Event

EventWhen
strategist.report.readyA new report is ready

The payload is thin: the event type and report_id. Fetch the report itself with GET /strategist/reports/{id}. Signature, retries, and event verification: Webhooks.

#Errors

The general format and the code catalog: Conventions. For these endpoints:

CodeHTTPWhen
invalid_api_key401The key was not accepted
scope_missing403No strategist:read scope
not_found404There is no such report, or it belongs to someone else
validation_failed422An invalid report_type, limit outside 1-100, or an expired cursor (errors[].field = /cursor)
rate_limited429The rate limit was exceeded

#Open questions