◆ Sapport for developers API reference Error catalog OpenAPI
Sections

#Strategist topics and actions

Status: Available (reading) / Planned (decisions). GET /strategist/topics, GET /strategist/topics/{id}, GET /strategist/actions and GET /strategist/actions/{id} work (scope strategist:read). Approving, rejecting and confirming (POST …) are Planned: calls return 404.

The strategist does not publish or spend money without your confirmation: it prepares a proposal, and you make the decision. In the API there are two kinds of such proposals, and they are two different resources.

Access: strategist:read for reading; strategist:write for approving, rejecting, and confirming. Paid: approving a topic is free; generating an article from a topic is a paid operation with the articles:generate scope (Paid operations). PII: no.

#Base URL

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

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

#Two resources

ResourceWhere it comes fromWhat is decidedPath
Topic (a topic queue item)The content planner, competitor analysis, and also the strategist after you confirm its actionApprove a topic for an article, or reject it/strategist/topics
Action (a pending chat action)The strategist in a conversation: "put a topic in the queue"Confirm: the topic goes into the topic queue. No article is written/strategist/actions

A topic's path from idea to article consists of three separate decisions:

the strategist proposes a topic in chat  →  you confirm the action  →  the topic is in the queue (proposed)
                                                                   │
         planner / competitors ─────────────────────────────────────┘ (also put topics in the queue)
                                                                   ▼
                                           you approve the topic  →  the topic is approved
                                                                   ▼
                      you order generation  →  a draft article (draft)

Approving a topic does not start generation. Generation is a paid operation and is started by a separate request: either POST /articles/generation-jobs with topic_id (Article generation), or the same approval request with the generate: true flag; in both cases the cost is explicit and arrives in usage.

Only actions from your own conversation sessions are visible (Chat with the strategist); the queue's topics are shared across the tenant.

#Endpoints

Available — the four read routes; the four POST routes are Planned.

Method and pathScopeWhat it does
GET /strategist/topicsstrategist:readThe topic queue
GET /strategist/topics/{id}strategist:readOne topic
POST /strategist/topics/{id}/approvestrategist:writeApprove a topic; with generate: true, also order generation (paid)
POST /strategist/topics/{id}/rejectstrategist:writeReject a topic
GET /strategist/actionsstrategist:readPending actions of your sessions
GET /strategist/actions/{id}strategist:readOne action
POST /strategist/actions/{id}/confirmstrategist:writeConfirm an action
POST /strategist/actions/{id}/cancelstrategist:writeReject an action

#Topic object

FieldTypeDescription
idstring (uuid)The ID
titlestring | nullThe proposed article title
rationalestring | nullThe rationale: why the topic is needed now
trend_scorenumber | nullAn importance score, 0-100; the queue is ordered by it
sourcestringWhere the topic is from: planner, competitors, strategist, and other sources
statusstringThe state (the table below)
cluster_idstring (uuid) | nullThe keyword cluster the topic belongs to
generated_article_idstring (uuid) | nullThe article created from the topic
proposed_atstring (ISO 8601)When it was proposed
decided_atstring (ISO 8601) | nullWhen a decision was made
expires_atstring (ISO 8601) | nullWhen an unprocessed topic goes stale

#Topic states (status)

StateMeaning
proposedWaiting for a decision
approvedApproved, the article is not yet ordered
generatingArticle generation is in progress
generatedA draft article is created, generated_article_id is filled in
rejectedRejected by you
duplicateRecognized as a repeat of an existing topic
expiredThe time ran out without a decision

Only a topic in the proposed state can be approved or rejected; in any other state, the request gets 409 invalid_state.

#How the queue works

#GET /strategist/topics

ParameterTypeRequiredConstraintsExample
statusstringnoOne of the topic statesproposed
limitintegerno1-100, 25 by default25
cursorstringnoThe next_cursor value; valid for 24 hours
include_deletedbooleannonot supported yet: true gives 422 not_supported (deleted topics are not stored)false
{
  "data": [
    {
      "id": "7a2c9e41-3b58-4d06-9f17-c8e5b1a02d63",
      "title": "Как выбрать увлажняющий крем для сухой кожи",
      "rationale": "Запрос растёт: 250 показов в сутки, статьи по теме на сайте нет.",
      "trend_score": 78.5,
      "source": "planner",
      "status": "proposed",
      "cluster_id": null,
      "generated_article_id": null,
      "proposed_at": "2026-10-10T07:00:00Z",
      "decided_at": null,
      "expires_at": "2026-10-24T07:00:00Z"
    }
  ],
  "next_cursor": null
}
curl "https://formula-cream.pro/api/public/v1/strategist/topics?status=proposed" \
  -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/topics?status=proposed",
  { headers: { Authorization: `Bearer ${process.env.SAPPORT_API_KEY}` } },
);
const { data } = await res.json();
r = httpx.get(
    "https://formula-cream.pro/api/public/v1/strategist/topics",
    params={"status": "proposed"},
    headers={"Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}"},
)
topics = r.json()["data"]
$ch = curl_init("https://formula-cream.pro/api/public/v1/strategist/topics?status=proposed");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SAPPORT_API_KEY")],
]);
$topics = json_decode(curl_exec($ch), true)["data"];

#GET /strategist/topics/{id}

ParameterTypeRequiredConstraintsExample
id (path)string (uuid)yesA topic of your tenant7a2c9e41-…

The 200 response is a topic object. Someone else's or a nonexistent topic returns 404 not_found.

#POST /strategist/topics/{id}/approve

Approves a topic: it moves to approved. No article is written and no money is charged.

ParameterTypeRequiredConstraintsExample
id (path)string (uuid)yesA topic in the proposed state7a2c9e41-…
generatebooleannofalse by default. true orders article generation right away (paid); requires the articles:generate scope and a completed editorial profilefalse
max_costobjectnoOnly together with generate: true: a cost ceiling {"amount":"50.00","currency":"RUB"}; no higher than the tenant's single-operation cost limit
Idempotency-Key headerstringrecommended; required with generate: trueUp to 255 characters5c1f8d2e-…

Without generate, the response is 200 OK:

{
  "id": "7a2c9e41-3b58-4d06-9f17-c8e5b1a02d63",
  "status": "approved",
  "decided_at": "2026-10-11T10:30:00Z"
}

With generate: true, the response is 202 Accepted, an operation envelope of type article.generation (Async operations); the topic moves to generating:

{
  "operation": {
    "id": "op_EXAMPLE02",
    "type": "article.generation",
    "status": "queued",
    "created_at": "2026-10-11T10:30:00Z",
    "resource": { "type": "generation_job", "id": "e4c1a7b2-5d3f-4a90-b8c6-1f2e3d4c5b6a" }
  }
}

The result is the same as for any generation: ?wait<=20 on this same request, GET /operations/{id}, GET /articles/generation-jobs/{id}, or the operation.completed and article.ready events (Article generation). The cost arrives in operation.usage. If generation could not be started (no profile, insufficient funds, a spending limit), the topic stays in the approved state, and the error is named by a code from the catalog.

A decision is made once: if two requests arrive at the same time, one executes and the other gets 409 invalid_state; paid generation is not started twice.

curl -X POST "https://formula-cream.pro/api/public/v1/strategist/topics/$TOPIC_ID/approve" \
  -H "Authorization: Bearer $SAPPORT_API_KEY" \
  -H "Idempotency-Key: 5c1f8d2e-9a47-4b30-8e61-d3a5c7f20b94" \
  -H "Content-Type: application/json" \
  -d '{"generate":true,"max_cost":{"amount":"50.00","currency":"RUB"}}'
const res = await fetch(
  `https://formula-cream.pro/api/public/v1/strategist/topics/${topicId}/approve?wait=20`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SAPPORT_API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ generate: true }),
  },
);
if (res.status === 202) {
  const { operation } = await res.json();
}
r = httpx.post(
    f"https://formula-cream.pro/api/public/v1/strategist/topics/{topic_id}/approve",
    params={"wait": 20},
    headers={
        "Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"generate": True},
    timeout=30,
)
$ch = curl_init("https://formula-cream.pro/api/public/v1/strategist/topics/" . $topicId . "/approve?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(["generate" => true]),
]);
$res = curl_exec($ch);

#POST /strategist/topics/{id}/reject

Rejects a topic. Free. The decision is remembered (decided_at), and the topic is no longer offered for approval.

ParameterTypeRequiredConstraintsExample
id (path)string (uuid)yesA topic in the proposed state7a2c9e41-…
Idempotency-Key headerstringrecommendedUp to 255 characters9b2e4c70-…

No body is needed. The response is 200 OK:

{
  "id": "7a2c9e41-3b58-4d06-9f17-c8e5b1a02d63",
  "status": "rejected",
  "decided_at": "2026-10-11T10:31:00Z"
}

Only a topic in the proposed state can be rejected. For a topic in any other state (including one already rejected or one with a finished article), the response is 409 invalid_state; the topic's state does not change.

curl -X POST "https://formula-cream.pro/api/public/v1/strategist/topics/$TOPIC_ID/reject" \
  -H "Authorization: Bearer $SAPPORT_API_KEY" \
  -H "Idempotency-Key: 9b2e4c70-1d3a-4f58-a6e2-7c0b9d4e1f35"

#Action object

An action is created by the strategist in a conversation; it is executed only by your request. The strategist cannot confirm its own proposal in any way, neither directly nor with a "second message on your behalf".

FieldTypeDescription
idstring (uuid)The ID
session_idstring (uuid) | nullThe conversation session in which the action was proposed
typestringThe kind of action. In v1, propose_topic (put a topic in the queue)
titlestringWhat is proposed: for propose_topic, the topic title
statusstringThe state (the table below)
resultobject | nullAfter execution: {topic_id}, the topic created in the queue
errorobject | nullOn failed: {code, message}
created_atstring (ISO 8601)When it was proposed
decided_atstring (ISO 8601) | nullWhen a decision was made
expires_atstring (ISO 8601) | nullWhen it expires: 7 days after the proposal

#Action states (status)

StateMeaning
proposedWaiting for a decision
confirmedConfirmed, being executed
executedExecuted, result is filled in
failedExecution failed, the reason is in error
cancelledRejected by you
expiredThe time (7 days) ran out without a decision

Only an action in the proposed state can be confirmed or rejected. Confirming again, or confirming an expired or already rejected action, gives 409 invalid_state: the actual state is named in detail. Permissions are checked at the moment of confirmation, not at the moment of the proposal: permissions may have changed in 7 days.

#GET /strategist/actions

Actions of your integration's sessions, newest first. Actions of staff in the dashboard and of other integrations are not visible. Until the strategist chat is released for integrations, the list is empty.

ParameterTypeRequiredConstraintsExample
statusstringnoOne of the action statesproposed
session_idstring (uuid)noOnly actions of this session
limitintegerno1-100, 25 by default25
cursorstringnoThe next_cursor value; valid for 24 hours
{
  "data": [
    {
      "id": "d41e7a30-8b25-4c69-a0f3-52c9e1b7d806",
      "session_id": "c2e84a17-9d03-4f5b-a6c8-3b71e0d92f45",
      "type": "propose_topic",
      "title": "Как выбрать увлажняющий крем для сухой кожи",
      "status": "proposed",
      "result": null,
      "error": null,
      "created_at": "2026-10-11T10:02:11Z",
      "decided_at": null,
      "expires_at": "2026-10-18T10:02:11Z"
    }
  ],
  "next_cursor": null
}
curl "https://formula-cream.pro/api/public/v1/strategist/actions?status=proposed" \
  -H "Authorization: Bearer $SAPPORT_API_KEY"

#GET /strategist/actions/{id}

ParameterTypeRequiredConstraintsExample
id (path)string (uuid)yesAn action of your integrationd41e7a30-…

The 200 response is an action object. An action of another integration, of another tenant, or a nonexistent one returns 404 not_found.

#POST /strategist/actions/{id}/confirm

Confirms an action and executes it: for propose_topic, a topic in the proposed state appears in the queue, and result.topic_id points to it. Free; no article is written, and the topic must be approved separately.

ParameterTypeRequiredConstraintsExample
id (path)string (uuid)yesAn action in the proposed stated41e7a30-…
Idempotency-Key headerstringrecommendedUp to 255 characters3f6a9c12-…

No body is needed. The response is 200 OK, an action object:

{
  "id": "d41e7a30-8b25-4c69-a0f3-52c9e1b7d806",
  "session_id": "c2e84a17-9d03-4f5b-a6c8-3b71e0d92f45",
  "type": "propose_topic",
  "title": "Как выбрать увлажняющий крем для сухой кожи",
  "status": "executed",
  "result": { "topic_id": "7a2c9e41-3b58-4d06-9f17-c8e5b1a02d63" },
  "error": null,
  "created_at": "2026-10-11T10:02:11Z",
  "decided_at": "2026-10-11T10:40:02Z",
  "expires_at": "2026-10-18T10:02:11Z"
}
curl -X POST "https://formula-cream.pro/api/public/v1/strategist/actions/$ACTION_ID/confirm" \
  -H "Authorization: Bearer $SAPPORT_API_KEY" \
  -H "Idempotency-Key: 3f6a9c12-7e50-4b84-9d21-a8c4e0b6f593"
const res = await fetch(
  `https://formula-cream.pro/api/public/v1/strategist/actions/${actionId}/confirm`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SAPPORT_API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
    },
  },
);
const action = await res.json();
r = httpx.post(
    f"https://formula-cream.pro/api/public/v1/strategist/actions/{action_id}/confirm",
    headers={
        "Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
)
action = r.json()
$ch = curl_init("https://formula-cream.pro/api/public/v1/strategist/actions/" . $actionId . "/confirm");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("SAPPORT_API_KEY"),
        "Idempotency-Key: " . bin2hex(random_bytes(16)),
    ],
]);
$action = json_decode(curl_exec($ch), true);

#POST /strategist/actions/{id}/cancel

Rejects an action: it moves to cancelled, and the topic does not enter the queue. Free. The parameters and constraints are the same as for confirm; the 200 OK response is an action object with the cancelled status.

curl -X POST "https://formula-cream.pro/api/public/v1/strategist/actions/$ACTION_ID/cancel" \
  -H "Authorization: Bearer $SAPPORT_API_KEY" \
  -H "Idempotency-Key: 6d0b3e85-2a71-4c96-b4f8-1e9a5c7d3028"

#Events

EventWhen
operation.completedGeneration ordered by approving a topic with generate: true finished
article.readyA draft article was created from the topic
article.generation.failedGeneration from the topic failed

There are no separate "new topic" and "action confirmed" events. 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:read / strategist:write, or, with generate: true, no articles:generate
not_found404There is no such topic or action, or it belongs to someone else
invalid_state409The topic is not in proposed (approve, reject); the action is not in proposed (confirm, cancel), including an expired one
editorial_profile_required409generate: true, but the editorial profile is not completed (Articles)
validation_failed422Generation cannot be started: the topic has no keyword; an invalid max_cost
insufficient_funds402Not enough funds for generation
spend_limit_reached402The spending, concurrency, or operation cost limit was hit
upstream_unavailable503No model key, or the model is unavailable
rate_limited429The rate limit was exceeded

#Open questions