◆ Sapport for developers API reference Error catalog OpenAPI
Sections

#Quickstart in 5 Minutes

Status: server API (reading, wallet, webhooks) — Available; sandbox and test keys — Planned; widget and loader — Available. Steps 1–4 relate to the server API and describe the target behavior. Step 5 (the widget) works today.

Goal of this page: issue a key, choose your cell's address, verify access with a GET /me call, make a first useful request, and embed the chat with a single line.

#What you need

#Step 1. Issue a key

  1. Sign in to your cell's dashboard.
  2. Open Settings → API.
  3. Create an integration (a service account) and choose scopes. analytics:read is enough for a first look.
  4. Set the key name, the mode (live or test), and, if needed, a list of allowed IPs.
  5. Copy the key. It is shown only once. A lost key cannot be recovered; you can only issue a new one.

A key looks like this (the example is deliberately fake):

sap_ru_live_EXAMPLE0KEYID_EXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEX

Put the key in an environment variable or in your server's secret store:

export SAPPORT_API_KEY='sap_ru_live_EXAMPLE0KEYID_…'

For details, see Authentication.

Test keys (sap_<cell>_test_…) work on the cell's sandbox. The sandbox is Planned.

#Step 2. Choose your cell's address

A key works only in the cell where it was issued.

CellBase address
RUhttps://formula-cream.pro/api/public/v1
ENhttps://wfacademy.org/api/public/v1
IDhttps://wfacademy.id/api/public/v1
export SAPPORT_BASE_URL='https://formula-cream.pro/api/public/v1'

An RU key used at the EN address returns 401 wrong_cell with a correct_base_url field containing the right address. See Cells and data.

#Step 3. Verify the key with GET /me

GET /me requires no scopes and costs nothing. It returns the tenant, the cell, the integration, key details (scopes, expiry, IP list), limits, the wallet state, and the CRM mode.

#curl

curl -sS "$SAPPORT_BASE_URL/me" \
  -H "Authorization: Bearer $SAPPORT_API_KEY"

#JavaScript (Node.js 18+, fetch)

const res = await fetch(`${process.env.SAPPORT_BASE_URL}/me`, {
  headers: { Authorization: `Bearer ${process.env.SAPPORT_API_KEY}` },
});

if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail} (request ${problem.request_id})`);
}

console.log(await res.json());

#Python (httpx)

import os
import httpx

resp = httpx.get(
    f"{os.environ['SAPPORT_BASE_URL']}/me",
    headers={"Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}"},
    timeout=10,
)
if resp.status_code != 200:
    problem = resp.json()
    raise RuntimeError(f"{problem['code']}: {problem['detail']} (request {problem['request_id']})")

print(resp.json())

#PHP (cURL)

<?php
$ch = curl_init(getenv('SAPPORT_BASE_URL') . '/me');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('SAPPORT_API_KEY')],
    CURLOPT_TIMEOUT        => 10,
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
    $problem = json_decode($body, true);
    throw new RuntimeException($problem['code'] . ': ' . $problem['detail']);
}
print_r(json_decode($body, true));

#Example response

The values are illustrative; the structure is fixed.

{
  "tenant": { "id": "3f0c9a52-0000-4000-8000-000000000001", "name": "Пример-студия" },
  "cell": "ru",
  "base_url": "https://formula-cream.pro/api/public/v1",
  "integration": { "id": "int_EXAMPLE01", "name": "Моя CRM" },
  "key": {
    "key_id": "EXAMPLE0KEYID",
    "mode": "live",
    "scopes": ["analytics:read"],
    "expires_at": "2027-10-11T00:00:00Z",
    "ip_allowlist": []
  },
  "limits": {
    "read_per_min": 600,
    "write_per_min": 120,
    "paid_per_min": 30,
    "paid_concurrency": 5
  },
  "wallet": {
    "balance": { "amount": "1500.00", "currency": "RUB" },
    "state": "ok",
    "exchange": "active",
    "topup_url": "https://formula-cream.pro/ru/promo/economics#wallet",
    "daily_spend": { "used": { "amount": "0.00", "currency": "RUB" }, "limit": { "amount": "500.00", "currency": "RUB" } }
  },
  "crm_mode": "internal",
  "channel_id": "00000000-0000-4000-8000-000000000000"
}

The wallet.exchange field shows whether exchange is running: with suspended (a negative balance), the other routes respond with 402 wallet_suspended. See Wallet and exchange suspension.

The channel_id field is the identifier of the api channel created together with the integration; you need it if you run your own channel through the AI seller.

If you received a problem+json response instead, look up the code in the error catalog. The most common are invalid_api_key (a typo, or the key was revoked or expired), wrong_cell (the wrong address), or ip_not_allowed (a request from an address outside the list).

#Step 4. First useful call: traffic sources

Scope analytics:read, not paid, no personal data.

curl -sS "$SAPPORT_BASE_URL/analytics/traffic-sources?from=2026-10-01&to=2026-10-07" \
  -H "Authorization: Bearer $SAPPORT_API_KEY"

Example response (illustrative):

{
  "data": [
    { "source": "google", "kind": "search", "sessions": 1280, "visitors": 1104, "leads": 14 },
    { "source": "chatgpt", "kind": "ai_assistant", "sessions": 212, "visitors": 190, "leads": 5 },
    { "source": "social", "kind": "social", "sessions": 96, "visitors": 88, "leads": 1 }
  ],
  "data_as_of": { "tracker": "2026-10-07" }
}

Note data_as_of: data from external sources (for example, the search console) arrives with a delay, and the platform reports honestly the point in time to which each source is current. Daily series are added with the include=series parameter. For parameters and metrics, see Reading analytics.

Other requests with the same key are in the README.

#Step 5. Connect the chat widget with one line

This already works. The widget does not need a secret key; it uses the public channel identifier.

  1. In the dashboard, create a "Web chat" channel and copy its identifier (a UUID).
  2. Paste this before </body> on a page of your site:
<script src="https://formula-cream.pro/widget.js" data-channel="00000000-0000-4000-8000-000000000000"></script>

Replace the domain with your cell's domain (wfacademy.org for EN, wfacademy.id for ID) and the UUID with your own.

Optional parameters in the tag: data-position (bottom-right or bottom-left), data-theme (light by default), data-bubble-text, data-delay (seconds).

For the chat window to open, your site's domain must be listed in the tenant settings, or the site must be linked through a CMS module: the platform allows embedding only on such domains. Without this, the button is visible but the window is blocked by the browser.

For details, see Embedding the widget. If you also want analytics collection with one line, use the loader: Loader.

#What next

I want toPage
Understand keys, scopes, and rotationAuthentication
Know the error format, pagination, retriesConventions
Receive events in real timeWebhooks
Run my own channel through the AI sellerOwn channel
Connect an external CRMCRM modes