Отправка событий с бэкенда
Прямой HTTP-контракт ActionPulse — заголовок авторизации, формат батча, ответ 202, коды отказов, повторы и лимиты. Рабочие примеры на curl, Node.js, Go и Python.
На этой странице
Прямой HTTP-запрос — рабочий и полностью доступный способ отправлять события с бэкенда. Отдельные библиотеки для Node.js, Go и Python в репозитории продукта существуют, но во внешние реестры пакетов не опубликованы, поэтому примеры ниже используют обычный HTTP. Клиент на любом языке — это один POST.
Запрос
POST https://YOUR_ACTIONPULSE_HOST/v1/track
Authorization: Bearer $ACTIONPULSE_SERVER_KEY
Content-Type: application/json
Тело — объект с массивом batch, даже если событие одно:
{
"batch": [
{
"event_id": "8b0d3f2e-1c4a-4f0e-9b1d-2a3c4d5e6f70",
"schema_version": 1,
"event": "order_created",
"user_id": "YOUR_USER_ID",
"time": "2026-07-27T12:00:00Z",
"props": { "plan": "pro", "total": 990 }
}
]
}
Поля события
| Поле | Обязательно | Что важно знать |
|---|---|---|
event |
да | ^[a-z0-9_.]{1,128}$ — строчные латинские буквы, цифры, _ и . |
user_id / anon_id |
нужен хотя бы один | до 256 символов, без нулевого байта |
event_id |
нет, но нужен | UUID; без него повторная отправка создаст второе событие |
time |
нет | RFC 3339; вне окна ±48 ч заменяется временем приёма |
schema_version |
нет | целое ≥ 0; 0 означает текущую версию из реестра |
props |
нет | до 64 ключей, ключ до 64 символов, значение до 8 КБ |
session_id |
нет | обрезается до 256 символов |
url, referrer |
нет | обрезаются до 2048 символов |
Ответ
Успех — 202 Accepted:
{ "accepted": 1, "n": 1, "rejected": [] }
n — сколько элементов вы прислали, accepted — сколько принято, rejected —
список отказов с индексом элемента в исходном батче:
{
"accepted": 1,
"n": 2,
"rejected": [{ "i": 1, "code": "invalid_event" }]
}
Основные коды отказа отдельных элементов:
| Код | Причина |
|---|---|
invalid_event |
имя не подходит под ^[a-z0-9_.]{1,128}$ |
missing_actor |
нет ни user_id, ни anon_id |
invalid_actor |
идентификатор слишком длинный или содержит нулевой байт |
invalid_event_id |
event_id не разбирается как UUID |
too_many_props |
больше 64 свойств |
prop_key_too_long |
имя свойства длиннее 64 символов |
prop_value_too_large |
значение свойства больше 8 КБ |
credential_event_forbidden |
ключ не имеет права отправлять это имя события |
schema_violation |
событие нарушает контракт из реестра в строгом режиме |
Примеры
curl
test -n "$ACTIONPULSE_SERVER_KEY" || { echo "ACTIONPULSE_SERVER_KEY is required" >&2; exit 1; }
curl -X POST https://YOUR_ACTIONPULSE_HOST/v1/track \
-H "Authorization: Bearer $ACTIONPULSE_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{"batch":[{
"event_id":"8b0d3f2e-1c4a-4f0e-9b1d-2a3c4d5e6f70",
"schema_version":1,
"event":"order_created",
"user_id":"YOUR_USER_ID",
"time":"2026-07-27T12:00:00Z",
"props":{"plan":"pro","total":990}
}]}'Node.js
import { randomUUID } from 'node:crypto';
const serverKey = process.env.ACTIONPULSE_SERVER_KEY;
if (!serverKey) throw new Error('ACTIONPULSE_SERVER_KEY is required');
const response = await fetch('https://YOUR_ACTIONPULSE_HOST/v1/track', {
method: 'POST',
headers: {
Authorization: `Bearer ${serverKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
batch: [
{
event_id: randomUUID(),
schema_version: 1,
event: 'order_created',
user_id: 'YOUR_USER_ID',
time: new Date().toISOString(),
props: { plan: 'pro', total: 990 },
},
],
}),
});
if (response.status !== 202) {
throw new Error(`track failed: ${response.status}`);
}
const result = await response.json();
if (result.rejected.length > 0) {
console.error('events rejected', result.rejected);
}Go
serverKey := os.Getenv("ACTIONPULSE_SERVER_KEY")
if serverKey == "" {
log.Fatal("ACTIONPULSE_SERVER_KEY is required")
}
body, err := json.Marshal(map[string]any{"batch": []map[string]any{{
"event_id": uuid.NewString(),
"schema_version": 1,
"event": "order_created",
"user_id": "YOUR_USER_ID",
"time": time.Now().UTC().Format(time.RFC3339Nano),
"props": map[string]any{"plan": "pro", "total": 990},
}}})
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://YOUR_ACTIONPULSE_HOST/v1/track", bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+serverKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("track failed: %s", resp.Status)
}Python
import datetime, os, uuid, requests
server_key = os.environ["ACTIONPULSE_SERVER_KEY"]
response = requests.post(
"https://YOUR_ACTIONPULSE_HOST/v1/track",
json={"batch": [{
"event_id": str(uuid.uuid4()),
"schema_version": 1,
"event": "order_created",
"user_id": "YOUR_USER_ID",
"time": datetime.datetime.now(datetime.UTC).isoformat(),
"props": {"plan": "pro", "total": 990},
}]},
headers={"Authorization": f"Bearer {server_key}"},
timeout=5,
)
response.raise_for_status()
for item in response.json()["rejected"]:
print("rejected", item)Батчи и лимиты
| Ограничение | Значение |
|---|---|
| Элементов в одном запросе | до 500 |
| Размер тела запроса | до 256 КБ |
Превышение количества элементов — 422, превышение размера тела — 413.
Пустой массив batch тоже 422.
Повторы
Повторяйте запрос с тем же телом при ошибке транспорта, 429 и 5xx,
используя экспоненциальную задержку. При 429 учитывайте заголовок
Retry-After — он приходит в секундах.
Ответы 401, 402, 403, 413 и 422 окончательные: повтор их не исправит.
Ошибки уровня запроса
| Код | Что означает |
|---|---|
401 |
ключ отсутствует, недействителен или истёк |
402 |
нет действующей подписки либо закончился пробный период |
403 |
ключ не имеет права на этот запрос |
413 |
тело больше 256 КБ |
422 |
некорректный JSON, пустой батч или превышение 500 элементов |
429 |
превышен лимит запросов, см. Retry-After |
Все ошибки приходят в одном формате:
{ "error": { "code": "validation_failed", "message": "batch is empty", "details": {} } }
Какие события отправлять с сервера
Всё, чему нужно доверять: подтверждённые оплаты, созданные и оплаченные заказы, возвраты, регистрации, старт и конверсия подписки, выдача доступа.
Часть имён вообще принимается только с серверным ключом — registered,
subscription_started, trial_started, trial_activated, trial_converted,
trial_expired, payment_succeeded, payment_attached, order_created,
first_order_created, order_paid, invoice_paid, refund. Попытка
отправить такое имя браузерным токеном вернёт credential_event_forbidden.
Обратное тоже верно: имена из пространства pp.* серверным ключом отправить
нельзя — их формирует браузерный сбор.