◆ Sapport for developers API reference Error catalog OpenAPI
Sections

#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

CellURL
RUhttps://formula-cream.pro/api/public/v1
ENhttps://wfacademy.org/api/public/v1
IDhttps://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 pathScopeWhat it does
POST /strategist/chat/sessionsstrategist:chatStart a conversation session
GET /strategist/chat/sessionsstrategist:readA list of sessions
GET /strategist/chat/sessions/{id}strategist:readA session with its messages
POST /strategist/chat/sessions/{id}/turnsstrategist:chatAsk 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:

WhatValue
Question lengthup to 4000 characters
Tool calls per turnno more than 6
Maximum turn time90 seconds
Single-turn cost limitPreserved. Today it equals US$0.25; a turn that reaches the limit stops and returns what it managed to produce
Memorythe last 20 messages of the session are mixed into the next turn
Answerno 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.

ParameterTypeRequiredConstraintsExample
id (path)string (uuid)yesA session of your integrationc2e84a17-…
messagestringyes1-4000 characters, not empty after trimming whitespaceПочему упал трафик из поиска?
wait (query)integerno0-20 seconds20
Idempotency-Key headerstringyesUp to 255 characters0e8a2f61-…

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 fieldTypeDescription
contentstringThe strategist's answer
tool_calls[]arrayWhat the strategist looked at: name (the tool), summary (a short description, for example "looked at traffic for 14 days")
actions[]array of stringIDs of the actions that the strategist prepared in this turn
truncatedstring | nullThe 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.

FieldTypeDescription
idstring (uuid)The ID
titlestring | nullThe title
message_countintegerThe number of messages in the session
total_costobjectThe session's total cost: {amount, currency}, amount is a string
created_at, updated_atstring (ISO 8601)Dates

#GET /strategist/chat/sessions/{id}

The whole session: messages in order, up to 400 messages.

Message fieldTypeDescription
idstring (uuid)The ID
rolestringuser, assistant, tool
contentstring | nullThe text. Always null for tool messages
kindstring | nullaction: a proposal card message
action_idstring (uuid)For proposal cards: the ID of the action; read its current state there
tool_calls[], actions[], truncatedFor assistant messages: as in the table above
created_atstring (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:

ActionWhat happens after confirmation
propose_topic: propose a topicThe 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

EventWhen
strategist.turn.completedA conversation turn finished (successfully or with an error)
operation.completedA 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:

CodeHTTPWhen
invalid_api_key401The key was not accepted
scope_missing403No strategist:chat / strategist:read
not_found404There is no such session, or it belongs to someone else
validation_failed422An empty question or one longer than 4000 characters; wait outside 0-20; an expired cursor
idempotency_conflict409The Idempotency-Key was already used with a different body
invalid_state409A turn is already running in the session: wait for the operation to finish
insufficient_funds402Not enough funds in the wallet
spend_limit_reached402The spending, concurrency, or operation cost limit was hit
upstream_unavailable503No model key, or the model is unavailable
service_unavailable503The limits store is unavailable: the paid turn was not accepted
rate_limited429The rate limit for paid requests was exceeded

#Open questions