#Chat with the strategist
Status: Planned. There is no public chat API: a conversation with the strategist is available in the Sapport dashboard (the "Promotion" section, the "Strategist" tab) and in the staff Telegram bot. This page describes the intent from the design; the fields and codes are preliminary and may change at release. No dates are promised.
Chat with the strategist is a conversation with the AI marketer. You ask a question ("why did search traffic drop", "which competitor topics have we not covered"), and the strategist looks at your data with read tools and answers. If it decides that an action is needed, it proposes it but does not carry it out: the decision stays with a human.
Access: strategist:chat for the conversation; strategist:read for reading sessions. Paid: yes, each turn is charged to the tenant's wallet (Paid operations). PII: the strategist works with aggregates and counters (including the CRM funnel in counters); it does not receive personal data of students or leads.
An integration sees only its own sessions. Sessions started by people in the dashboard or in the staff Telegram bot, and sessions of other integrations, are not available 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, idempotency, async operations: Conventions. Keys and scopes: Authentication. In all examples, the key is deliberately fake.
#Endpoints
| Method and path | Scope | What it does |
|---|---|---|
POST /strategist/chat/sessions | strategist:chat | Start a conversation session |
GET /strategist/chat/sessions | strategist:read | A list of sessions |
GET /strategist/chat/sessions/{id} | strategist:read | A session with its messages |
POST /strategist/chat/sessions/{id}/turns | strategist:chat | Ask a question (a conversation turn). Paid |
#How a turn works
Publicly, a conversation turn is asynchronous, like all paid API operations (Async operations); internally, the platform may run it synchronously.
POST .../sessions/{id}/turns (Idempotency-Key) → 202 Accepted + operation
│
├── ?wait=N (up to 20 s): wait on the same job
├── GET /operations/{id}
└── strategist.turn.completed, operation.completed events → GET .../sessions/{id}
A turn can take a noticeable amount of time: the strategist calls tools one after another and only then answers. So do not hold the connection; subscribe to the event or poll the operation.
#The strategist's tools
An integration has data read tools (traffic, search queries, articles, competitors in aggregated form, the CRM funnel in counters) and the propose_topic tool ("propose a topic"). Advertising tools (the ads.manage permission) and competitor raw data (the competitors.raw permission) are unavailable in v1.
Turn limits:
| What | Value |
|---|---|
| Question length | up to 4000 characters |
| Tool calls per turn | no more than 6 |
| Maximum turn time | 90 seconds |
| Single-turn cost limit | Preserved. Today it equals US$0.25; a turn that reaches the limit stops and returns what it managed to produce |
| Memory | the last 20 messages of the session are mixed into the next turn |
| Answer | no longer than 4000 tokens |
If a limit is reached, the strategist does not cut the conversation off silently: it returns what it managed to produce, and the response states the reason for stopping (the truncated field, below).
The strategist's model is chosen by the platform and cannot be specified in the request. The platform takes the session history from its own database, not from the request: you cannot send a "history" along with the question. This protects against spoofing ("the user has already confirmed everything").
#POST /strategist/chat/sessions
Creates an empty session owned by your integration. No request body is needed.
{
"id": "c2e84a17-9d03-4f5b-a6c8-3b71e0d92f45",
"title": null,
"message_count": 0,
"created_at": "2026-10-11T10:00:00Z"
}
If no title is set, it is formed from the first 80 characters of the first question.
curl -X POST https://formula-cream.pro/api/public/v1/strategist/chat/sessions \
-H "Authorization: Bearer $SAPPORT_API_KEY" \
-H "Idempotency-Key: 8b3d1f60-2e47-4a95-b7c1-6d0e9a5f3c28"
# SAPPORT_API_KEY=sap_ru_test_EXAMPLE0KEYID_… (an example, not a real key)
#POST /strategist/chat/sessions/{id}/turns
Asks a question in a session. Paid.
| Parameter | Type | Required | Constraints | Example |
|---|---|---|---|---|
id (path) | string (uuid) | yes | A session of your integration | c2e84a17-… |
message | string | yes | 1-4000 characters, not empty after trimming whitespace | Почему упал трафик из поиска? |
wait (query) | integer | no | 0-20 seconds | 20 |
Idempotency-Key header | string | yes | Up to 255 characters | 0e8a2f61-… |
The question is written to the session right away, before the wallet check and before the model runs. If the turn fails (a wallet refusal, a model failure), the message stays in the history.
The response is 202 Accepted, an operation envelope of type strategist.turn. If the turn finished within wait seconds, the envelope already has status: "succeeded" and usage; otherwise it shows the current state:
{
"operation": {
"id": "op_EXAMPLE01",
"type": "strategist.turn",
"status": "running",
"created_at": "2026-10-11T10:00:05Z",
"resource": { "type": "strategist_session", "id": "c2e84a17-9d03-4f5b-a6c8-3b71e0d92f45" }
}
}
The cost of a turn is usage.cost in the envelope of the finished operation, in the currency of the tenant's wallet; usage.model is the model chosen by the platform. The result of the turn is read from the session: GET /strategist/chat/sessions/{id}. The strategist's answer is the last message with the assistant role:
| Message field | Type | Description |
|---|---|---|
content | string | The strategist's answer |
tool_calls[] | array | What the strategist looked at: name (the tool), summary (a short description, for example "looked at traffic for 14 days") |
actions[] | array of string | IDs of the actions that the strategist prepared in this turn |
truncated | string | null | The reason it stopped before a full answer: calls (the call limit was exhausted), time (time ran out), wallet (not enough funds), cost (the turn cost limit was reached); null if the answer is complete |
{
"id": "5e9b2d73-1c48-4a06-b7f5-80d3a6c1e294",
"role": "assistant",
"content": "Трафик из поиска упал на 12% за неделю: основная потеря на двух страницах про гидролаты. За тот же период там не было обновлений.",
"tool_calls": [
{ "name": "traffic_by_source", "summary": "посмотрел трафик по источникам за 14 дней" }
],
"actions": [],
"truncated": null,
"created_at": "2026-10-11T10:00:19Z"
}
A turn that fails (a wallet refusal, a model failure) ends with a failed operation with error; the user's message stays in the history.
curl -X POST "https://formula-cream.pro/api/public/v1/strategist/chat/sessions/$SESSION_ID/turns?wait=20" \
-H "Authorization: Bearer $SAPPORT_API_KEY" \
-H "Idempotency-Key: 0e8a2f61-7c4b-4d19-a3f5-9b1c6d2e8f47" \
-H "Content-Type: application/json" \
-d '{"message":"Почему упал трафик из поиска?"}'
const res = await fetch(
`https://formula-cream.pro/api/public/v1/strategist/chat/sessions/${sessionId}/turns?wait=20`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SAPPORT_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({ message: "Почему упал трафик из поиска?" }),
},
);
if (res.status === 202) {
const { operation } = await res.json();
// wait for the strategist.turn.completed (or operation.completed) event, or poll GET /operations/{id}
}
r = httpx.post(
f"https://formula-cream.pro/api/public/v1/strategist/chat/sessions/{session_id}/turns",
params={"wait": 20},
headers={
"Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"message": "Почему упал трафик из поиска?"},
timeout=30,
)
if r.status_code == 202:
operation = r.json()["operation"]
$ch = curl_init("https://formula-cream.pro/api/public/v1/strategist/chat/sessions/" . $sessionId . "/turns?wait=20");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SAPPORT_API_KEY"),
"Idempotency-Key: " . bin2hex(random_bytes(16)),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["message" => "Почему упал трафик из поиска?"]),
]);
$res = curl_exec($ch);
#GET /strategist/chat/sessions
A list of your integration's sessions, newest first. A cursor-based list: the limit (1-100, 25 by default) and cursor parameters follow the general conventions.
| Field | Type | Description |
|---|---|---|
id | string (uuid) | The ID |
title | string | null | The title |
message_count | integer | The number of messages in the session |
total_cost | object | The session's total cost: {amount, currency}, amount is a string |
created_at, updated_at | string (ISO 8601) | Dates |
#GET /strategist/chat/sessions/{id}
The whole session: messages in order, up to 400 messages.
| Message field | Type | Description |
|---|---|---|
id | string (uuid) | The ID |
role | string | user, assistant, tool |
content | string | null | The text. Always null for tool messages |
kind | string | null | action: a proposal card message |
action_id | string (uuid) | For proposal cards: the ID of the action; read its current state there |
tool_calls[], actions[], truncated | For assistant messages: as in the table above | |
created_at | string (ISO 8601) | The time |
Tool outputs are not returned: they are snapshots of your analytics, and the screen needs only the name of the call ("looked at traffic for 14 days"), which is in tool_calls.
Someone else's or a nonexistent session returns 404 not_found.
curl "https://formula-cream.pro/api/public/v1/strategist/chat/sessions/$SESSION_ID" \
-H "Authorization: Bearer $SAPPORT_API_KEY"
#Proposals prepared in a conversation
The strategist does not execute control actions. It creates a pending action and shows it to a human. It is executed only by a separate request from a human; the strategist cannot confirm its own proposal in any way, neither directly nor with a "second message on your behalf".
In v1, one kind of proposal goes through this mechanism:
| Action | What happens after confirmation |
|---|---|
propose_topic: propose a topic | The topic enters the topic queue in the proposed state; no article is written |
The action does not touch money or publications: after confirmation, the topic must be approved separately, and article generation ordered explicitly.
Actions are confirmed and rejected with the POST /strategist/actions/{id}/confirm and POST /strategist/actions/{id}/cancel requests (the strategist:write scope). The action states (proposed, confirmed, executed, failed, cancelled, expired), the 7-day lifetime, and the rules for repeated decisions are described in the Action object section.
#Events
| Event | When |
|---|---|
strategist.turn.completed | A conversation turn finished (successfully or with an error) |
operation.completed | A strategist.turn operation finished; data holds the operation envelope |
The strategist.turn.completed payload is thin: session_id, the operation ID, and the outcome (succeeded or failed). Fetch the answer text with GET /strategist/chat/sessions/{id}. Signature, retries, 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:chat / strategist:read |
not_found | 404 | There is no such session, or it belongs to someone else |
validation_failed | 422 | An empty question or one longer than 4000 characters; wait outside 0-20; an expired cursor |
idempotency_conflict | 409 | The Idempotency-Key was already used with a different body |
invalid_state | 409 | A turn is already running in the session: wait for the operation to finish |
insufficient_funds | 402 | Not enough funds in the wallet |
spend_limit_reached | 402 | The spending, concurrency, or operation cost limit was hit |
upstream_unavailable | 503 | No model key, or the model is unavailable |
service_unavailable | 503 | The limits store is unavailable: the paid turn was not accepted |
rate_limited | 429 | The rate limit for paid requests was exceeded |
#Open questions
- Parallel turns (technical). In the dashboard, a second turn in the same session before the first one finishes is not provided for; this page describes it as
409 invalid_state. The exact behavior will be settled at release. - Turn cost limit in the wallet currency (technical). In the dashboard, the limit is set in US dollars (0.25) and the turn's
cost_usdis returned as a number. In the API, the cost is returned as a{amount, currency}string in the wallet currency; the way the limit is converted into the cell's currency is not described. - The
truncatedvalues (technical). The valuescalls,time,wallet,costwere introduced by this page from the internal reasons for stopping the loop; they are not named in the design. - The shape of a message with a turn result (technical). The
actions[]andaction_idfields, instead of the dashboard'spending/pending_state, are proposed by this page.