#Strategist reports
Status: Available (reading).
GET /strategist/reportsandGET /strategist/reports/{id}work (scopestrategist: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
| Cell | URL |
|---|---|
| RU | https://formula-cream.pro/api/public/v1 |
| EN | https://wfacademy.org/api/public/v1 |
| ID | https://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 path | Scope | What it does |
|---|---|---|
GET /strategist/reports | strategist:read | A list of reports: headers without the body |
GET /strategist/reports/{id} | strategist:read | One 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.
| Block | Field | What is inside |
|---|---|---|
| Highlights | highlights | Short facts for the period |
| Concerns | concerns | Problems and risks: metric drops, gaps in the schedule, "Don't do" items |
| Recommendations | actions | What to do, with a priority, an expected effect, and evidence |
| Evidence | evidence | Cases and data that the recommendations rely on |
#Report header (a list item)
| Field | Type | Description |
|---|---|---|
id | string (uuid) | The report ID |
report_date | string (date, YYYY-MM-DD) | The report date |
report_type | string | The kind of report: daily or weekly |
summary_text | string | A short summary |
actions_count | integer | The number of recommendations |
data_as_of | object | As 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_at | string (ISO 8601) | null | When 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
| Field | Type | Description |
|---|---|---|
id | string (uuid) | The report ID |
report_date | string | The report date |
report_type | string | The kind of report: daily or weekly |
summary_text | string | A short summary |
data_as_of | object | As in the header |
highlights | array | Highlights |
concerns | array | Concerns |
actions | array of object | Recommendations (below) |
evidence | object | Evidence (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[])
| Field | Type | Description |
|---|---|---|
title | string | What to do |
priority | string | high, medium, or low |
rationale | string | Why it is needed |
target_ref | string | null | What it affects: a reference to an object (an article, a query, a product) |
expected_effect | object | null | The expected effect: metric (the metric), delta (the change), horizon_days (the term in days) |
check_after_days | integer | null | After how many days to check the result |
basis | string | What 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)
| Value | Meaning |
|---|---|
own_measured | Measured on your data |
observed_pattern | Observed at competitors |
research | Relies on external research |
hypothesis | An 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.
| Field | Type | Description |
|---|---|---|
evidence.actions[] | array | One element per recommendation (in the order of actions) |
evidence.actions[].index | integer | The recommendation's number |
evidence.actions[].status | string | supported (confirmed by evidence) or unsupported |
evidence.actions[].basis | string | The basis (as in the table above) |
evidence.actions[].items[] | array | Evidence |
evidence.actions[].items[].ref | string | A reference to the source |
evidence.actions[].items[].kind | string | own (your data), competitor (a competitor), pulse (the pulse), signal (a signal); the set may grow |
evidence.actions[].items[].label | string | The source's label |
evidence.actions[].items[].url | string | The page address (optional) |
evidence.actions[].items[].quote | string | A 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_checkedis alwaysfalse.
#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
| Parameter | Type | Required | Constraints | Example |
|---|---|---|---|---|
limit | integer | no | 1-100, 25 by default | 10 |
cursor | string | no | The next_cursor value; valid for 24 hours | |
report_type | string | no | daily or weekly | daily |
{
"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}
| Parameter | Type | Required | Constraints | Example |
|---|---|---|---|---|
id | string (uuid) | yes | A report of your tenant | 3d8f1b52-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
| Event | When |
|---|---|
strategist.report.ready | A 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:
| Code | HTTP | When |
|---|---|---|
invalid_api_key | 401 | The key was not accepted |
scope_missing | 403 | No strategist:read scope |
not_found | 404 | There is no such report, or it belongs to someone else |
validation_failed | 422 | An invalid report_type, limit outside 1-100, or an expired cursor (errors[].field = /cursor) |
rate_limited | 429 | The rate limit was exceeded |
#Open questions
- Old reports (technical). Reports created before the structured recommendation fields (
target_ref,expected_effect,check_after_days,basis) appeared store the rationale only as text. How to serve them publicly (empty fields or conversion to the new structure) is undecided. - Recommendation ID (technical). In the dashboard's structure, a recommendation may have its own
id, but not in every report. Whether a stableidis needed in the public API is undecided. - Source of
data_as_of(technical). The field is added to the report DTO; which sources it is assembled from (the search console, the tracker, CRM counters) will be settled at release. - List window (technical). The dashboard's list returns headers for the last
daysdays (1-365, 90 by default), at most 400 rows, and is not paginated; in the public API it is replaced by a cursor-based one.