curl
Все запросы из терминала — событие, сессия, батч, user_id, офлайн-досыл, gzip, повторы, разбор ответа через jq, Read API и удаление данных.
curl удобен, чтобы проверить ключ, посмотреть ответ сервера глазами и отправлять события из shell-скриптов и cron-задач. Клиент для бэкендов и CLI — в рецепте Go, серверные сессии и Telegram-боты — в рецепте Боты и бэкенды.
Примеры рассчитаны на bash с curl, uuidgen и date. Для разбора ответов нужен jq.
Переменные окружения
Write-ключ pk_… показан в дашборде: откройте приложение и перейдите на вкладку «Интеграция». У
каждой платформы приложения свой ключ. Ключ не секретный — он вшивается в приложение (см.
Быстрый старт). Все примеры берут его из переменной AM_PK и передают
context.platform явно, поэтому работают с ключом любой платформы (порядок определения платформы —
Платформа события):
export AM_PK="pk_…"События принимает ingest по адресу /v1/events. В примерах стоит условный домен
https://i.analytics.example.com: замените его на адрес ingest своей установки, он подставлен в
сниппеты на вкладке «Интеграция».
Для Read API нужен другой, секретный ключ — см. Read API.
Общие приёмы во всех примерах:
- время —
date -u +"%Y-%m-%dT%H:%M:%S.000Z": RFC 3339 в UTC с миллисекундами; - идентификаторы —
uuidgen | tr A-Z a-z: uuidgen на macOS печатает UUID в верхнем регистре, а рекомендуемый формат — нижний; "environment": "development"— тестовые события не попадают в production-отчёты (см. Отладка);"ip": "none"— не определять страну и город по адресу машины, с которой запущен curl (см. context.ip).
Одно событие
Минимальный батч — один session_start:
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
curl -sS https://i.analytics.example.com/v1/events \
-H "Content-Type: application/json" \
-H "X-App-Key: $AM_PK" \
--data-binary @- <<JSON
{
"sent_at": "$NOW",
"context": { "platform": "server", "app_version": "1.0.0", "environment": "development", "ip": "none" },
"events": [
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$NOW",
"install_id": "$(uuidgen | tr A-Z a-z)",
"session_id": "$(uuidgen | tr A-Z a-z)"
}
]
}
JSONОтвет 200 приходит только после записи в хранилище:
{ "accepted": 1, "rejected": [], "warnings": [], "server_time": "2026-10-14T09:15:02.318Z" }Полная сессия
session_start и session_end одной сессии делят session_id и install_id. У session_end
обязательно поле duration_ms — время на переднем плане в миллисекундах, от 0 до 86 400 000. Без
него событие отклоняется с кодом duration_required.
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
INSTALL_ID=$(uuidgen | tr A-Z a-z)
SESSION_ID=$(uuidgen | tr A-Z a-z)
curl -sS https://i.analytics.example.com/v1/events \
-H "Content-Type: application/json" \
-H "X-App-Key: $AM_PK" \
--data-binary @- <<JSON
{
"sent_at": "$NOW",
"context": {
"platform": "server",
"os_name": "Linux",
"app_version": "1.0.0",
"device_type": "server",
"environment": "development",
"ip": "none"
},
"events": [
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$SESSION_ID"
},
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_end",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$SESSION_ID",
"duration_ms": 60000
}
]
}
JSONВ приложении session_end уходит в момент ухода в фон, а duration_ms считается по монотонным часам
(см. Сессии).
Батч из нескольких событий
В одном запросе — от 1 до 500 событий и не больше 1 МиБ после распаковки: так офлайн-очередь досылает
накопленное. context батча действует на все события, а context события перекрывает его поле за
полем. В примере две сессии одной установки, и вторая случилась уже после обновления приложения до
1.1.0:
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
INSTALL_ID=$(uuidgen | tr A-Z a-z)
FIRST=$(uuidgen | tr A-Z a-z)
SECOND=$(uuidgen | tr A-Z a-z)
curl -sS https://i.analytics.example.com/v1/events \
-H "Content-Type: application/json" \
-H "X-App-Key: $AM_PK" \
--data-binary @- <<JSON
{
"sent_at": "$NOW",
"context": { "platform": "server", "app_version": "1.0.0", "environment": "development", "ip": "none" },
"events": [
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$FIRST"
},
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_end",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$FIRST",
"duration_ms": 95000
},
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$SECOND",
"context": { "app_version": "1.1.0" }
},
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_end",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$SECOND",
"duration_ms": 42000,
"context": { "app_version": "1.1.0" }
}
]
}
JSONУ каждого события свой insert_id: по нему сервер отсекает повторы. Если insert_id повторяется
внутри батча, учитывается первое событие, а ответ содержит предупреждение
duplicate_insert_id_in_batch. Невалидное событие не валит батч: остальные будут приняты, а оно
окажется в rejected.
user_id после входа
До входа события идут без user_id. После входа добавляйте ID аккаунта из своего бэкенда во все
события, после выхода — снова отправляйте без него. Сессия, начатая анонимно, может закончиться уже с
user_id:
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
INSTALL_ID=$(uuidgen | tr A-Z a-z)
SESSION_ID=$(uuidgen | tr A-Z a-z)
USER_ID="acct_8f14e45f" # ID аккаунта в вашем бэкенде, не email и не телефон
curl -sS https://i.analytics.example.com/v1/events \
-H "Content-Type: application/json" \
-H "X-App-Key: $AM_PK" \
--data-binary @- <<JSON
{
"sent_at": "$NOW",
"context": { "platform": "server", "environment": "development", "ip": "none" },
"events": [
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$SESSION_ID"
},
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_end",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$SESSION_ID",
"user_id": "$USER_ID",
"duration_ms": 120000
}
]
}
JSONАнонимные события установки сервер задним числом припишет этому пользователю: в отчётах это один
человек u:acct_8f14e45f, а не два (см. Идентичность). user_id,
похожий на email или телефон, отклоняется с кодом user_id_looks_like_pii.
Офлайн-досыл
У батча два времени, и оба — по часам устройства:
timestampсобытия — когда событие произошло;sent_atбатча — когда отправлен этот HTTP-запрос.
Сервер считает время события как received_at + (timestamp − sent_at), где received_at — время
приёма по его часам. Поэтому событие из офлайн-очереди попадает в тот день, когда оно произошло, даже
если часы устройства спешат или отстают. Пример — сессия двухдневной давности, отправленная сейчас:
FMT="+%Y-%m-%dT%H:%M:%S.000Z"
# Двое суток назад: первая команда — для GNU date (Linux), вторая — для BSD date (macOS).
STARTED=$(date -u -d "-2 days" "$FMT" 2>/dev/null || date -u -v-2d "$FMT")
ENDED=$(date -u -d "-2 days +5 minutes" "$FMT" 2>/dev/null || date -u -v-2d -v+5M "$FMT")
NOW=$(date -u "$FMT")
INSTALL_ID=$(uuidgen | tr A-Z a-z)
SESSION_ID=$(uuidgen | tr A-Z a-z)
curl -sS https://i.analytics.example.com/v1/events \
-H "Content-Type: application/json" \
-H "X-App-Key: $AM_PK" \
--data-binary @- <<JSON
{
"sent_at": "$NOW",
"context": { "platform": "server", "environment": "development", "ip": "none" },
"events": [
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$STARTED",
"install_id": "$INSTALL_ID",
"session_id": "$SESSION_ID"
},
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_end",
"timestamp": "$ENDED",
"install_id": "$INSTALL_ID",
"session_id": "$SESSION_ID",
"duration_ms": 300000
}
]
}
JSONЧто важно для очереди:
sent_at— момент этой попытки: выставляйте его заново при каждом повторе, аtimestampсобытий не трогайте;- окно досыла — 7 дней включительно. Более старое событие отклоняется с кодом
event_too_oldиretryable: false, поэтому удаляйте такие события из очереди до отправки; - время больше чем на 10 минут в будущем заменяется временем приёма, а ответ содержит предупреждение
timestamp_in_future_clamped.
Подробнее — в разделе Время и офлайн-очередь.
Отладка: environment development
context.environment — production (по умолчанию) или development. Отчёты показывают production,
а development включается переключателем «Окружение», поэтому тестовые запросы не портят цифры. Все
примеры на этой странице отправляют development:
"context": { "platform": "server", "environment": "development", "ip": "none" }Отправленные события и причины отказов видны на вкладке «Интеграция». Для боевых событий уберите
environment или передайте "production". Как и остальные поля контекста, environment можно задать
в context отдельного события.
gzip
Большие батчи выгодно сжимать. Из алгоритмов сжатия сервер понимает только gzip: другое значение
Content-Encoding вернёт 400 unsupported_content_encoding. Лимит 1 МиБ считается после распаковки.
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
cat > batch.json <<JSON
{
"sent_at": "$NOW",
"context": { "platform": "server", "environment": "development", "ip": "none" },
"events": [
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$NOW",
"install_id": "$(uuidgen | tr A-Z a-z)",
"session_id": "$(uuidgen | tr A-Z a-z)"
}
]
}
JSON
gzip -c batch.json | curl -sS https://i.analytics.example.com/v1/events \
-H "Content-Type: application/json" \
-H "Content-Encoding: gzip" \
-H "X-App-Key: $AM_PK" \
--data-binary @-Повторы при 429 и 503
Батч целиком повторяют в двух случаях:
429(больше 2000 событий в секунду на приложение) и503(хранилище не подтвердило запись) — через столько секунд, сколько сервер указал в заголовкеRetry-After;500и ошибки сети — с задержкой 1, 2, 4 … 300 с и разбросом ±20 %.
Скрипт генерирует insert_id один раз, до цикла: если первый запрос дошёл, а ответ потерялся, повтор
не создаст дубликат. sent_at выставляется заново при каждой попытке.
INSERT_ID=$(uuidgen | tr A-Z a-z) # один на все попытки
INSTALL_ID=$(uuidgen | tr A-Z a-z)
SESSION_ID=$(uuidgen | tr A-Z a-z)
EVENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
backoff=1
for attempt in 1 2 3 4 5 6 7 8; do
cat > batch.json <<JSON
{
"sent_at": "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")",
"context": { "platform": "server", "environment": "development", "ip": "none" },
"events": [
{
"insert_id": "$INSERT_ID",
"name": "session_start",
"timestamp": "$EVENT_TIME",
"install_id": "$INSTALL_ID",
"session_id": "$SESSION_ID"
}
]
}
JSON
status=$(curl -sS -o response.json -D headers.txt -w "%{http_code}" \
https://i.analytics.example.com/v1/events \
-H "Content-Type: application/json" \
-H "X-App-Key: $AM_PK" \
--data-binary @batch.json) || status=000
case "$status" in
200) break ;;
429 | 503) # пауза — из заголовка Retry-After
pause=$(tr -d '\r' < headers.txt | awk -F': *' 'tolower($1) == "retry-after" { print $2 }')
pause=${pause:-5} ;;
4??) break ;; # 400, 401, 403, 413, 415: повтор не поможет
*) # 5xx и ошибки сети (000): 1, 2, 4 … 300 с, разброс ±20 %
pause=$(( backoff * (80 + RANDOM % 41) / 100 ))
pause=$(( pause < 1 ? 1 : pause ))
backoff=$(( backoff * 2 > 300 ? 300 : backoff * 2 )) ;;
esac
echo "попытка $attempt: HTTP $status, повтор через $pause с" >&2
sleep "$pause"
done
if [ "$status" = 200 ]; then
cat response.json
else
echo "батч не отправлен: HTTP $status" >&2
[ -f response.json ] && cat response.json >&2
false # ненулевой код выхода для cron и CI
fiПочему остальные 4xx не повторяются:
400,413,415— батч сломан, и тот же запрос получит тот же ответ. Исправьте клиент: поле с ошибкой подскажутerror.codeиerror.details;401,403— ключ отозван или приложение в архиве. Повтор не поможет, пока человек не выпустит новый ключ или не вернёт приложение из архива. События при этом не удаляйте;429— единственный временный4xx: лимит освободится черезRetry-Afterсекунд.
События, которые пришли в rejected с retryable: true (сейчас это только install_rate_limited),
отправьте следующим батчем не раньше чем через 60 с. Полная таблица действий — в разделе
Ошибки и повторы.
Разбор ответа через jq
На 200 сервер возвращает IngestResult: accepted — число принятых событий, rejected —
отклонённые события с кодом причины, warnings — события, принятые с поправкой, server_time — время
сервера. В примере второе событие — session_end без duration_ms:
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
INSTALL_ID=$(uuidgen | tr A-Z a-z)
SESSION_ID=$(uuidgen | tr A-Z a-z)
curl -sS https://i.analytics.example.com/v1/events \
-H "Content-Type: application/json" \
-H "X-App-Key: $AM_PK" \
-o response.json \
--data-binary @- <<JSON
{
"sent_at": "$NOW",
"context": { "platform": "server", "environment": "development", "ip": "none" },
"events": [
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$SESSION_ID"
},
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_end",
"timestamp": "$NOW",
"install_id": "$INSTALL_ID",
"session_id": "$SESSION_ID"
}
]
}
JSON
jq -r '"принято: \(.accepted)"' response.json
jq -r '.rejected[] | "отклонено events[\(.index)]: \(.code) в \(.field), повторять: \(.retryable)"' response.json
jq -r '.warnings[] | "предупреждение events[\(.index)]: \(.code)"' response.jsonпринято: 1
отклонено events[1]: duration_required в events[1].duration_ms, повторять: falseВ скрипте удобно завершаться с ошибкой, если сервер отклонил хотя бы одно событие: jq -e возвращает
ненулевой код, когда выражение ложно.
jq -e '.rejected == []' response.json > /dev/nullОшибка батча (4xx, 5xx) приходит в другом формате — в конверте error. Его request_id
совпадает с заголовком X-Request-Id, по нему запрос находится в логах сервера:
jq -r '.error | "\(.code): \(.message) (request_id \(.request_id))"' response.jsonRead API
Отчёты читаются тем же curl, но с другим адресом и ключом. Секретный ключ sk_… создаётся в
«Настройки → API-ключи» и показывается один раз; для отчётов нужен scope read. В отличие от pk_…,
этот ключ не вшивают в приложение: он открывает отчёты.
export AM_SK="sk_…"Список приложений
curl -sS https://api.analytics.example.com/v1/apps \
-H "Authorization: Bearer $AM_SK" \
| jq -r '.items[] | [.id, .platform, .timezone, .name] | @tsv'0199d5f0-0000-7000-8000-000000000002 ios Europe/Moscow Notes
0199d5f0-0000-7000-8000-000000000003 android Europe/Moscow NotesАрхивные приложения добавляет параметр include_archived=true.
Отчёт: активные по дням
id из списка — это app_id в адресе отчёта. Ряд активных пользователей по дням за неделю:
APP_ID="0199d5f0-0000-7000-8000-000000000002"
curl -sS -G "https://api.analytics.example.com/v1/apps/$APP_ID/audience/timeseries" \
-H "Authorization: Bearer $AM_SK" \
--data-urlencode "from=2026-10-06" \
--data-urlencode "to=2026-10-12" \
--data-urlencode "metric=active" \
--data-urlencode "granularity=day" \
--data-urlencode "compare=none" \
| jq -r '.points[] | "\(.ts[0:10])\t\(.value)"'2026-10-06 40987
2026-10-07 41230
2026-10-08 40876
2026-10-09 41502
2026-10-10 39811
2026-10-11 38420
2026-10-12 40115ts — начало корзины со смещением часового пояса приложения, поэтому первые 10 символов — дата
отчёта. metric — active, new, sessions или avg_duration_ms; granularity — hour (период
до 7 дней), day, week или month. Общие параметры и остальные эндпоинты — в разделе
Read API: обзор.
Read API ограничивает частоту: больше 10 запросов в секунду на ключ получают 429 rate_limited с
Retry-After. Повторяйте такие запросы по той же логике, что и батчи ingest.
Удаление данных пользователя
Запрос на удаление (например, по GDPR) выполняется через Read API ключом со scope delete.
subject_type — user (по user_id: события пользователя и всех привязанных к нему установок) или
install (по install_id). Удалить приложение целиком можно только в дашборде.
APP_ID="0199d5f0-0000-7000-8000-000000000002"
REQUEST_ID=$(curl -sS -X POST "https://api.analytics.example.com/v1/apps/$APP_ID/deletion-requests" \
-H "Authorization: Bearer $AM_SK" \
-H "Content-Type: application/json" \
-d '{"subject_type": "user", "subject_value": "acct_8f14e45f"}' \
| jq -r .id)
while :; do
STATUS=$(curl -sS "https://api.analytics.example.com/v1/apps/$APP_ID/deletion-requests/$REQUEST_ID" \
-H "Authorization: Bearer $AM_SK" | jq -r .status)
echo "$STATUS"
case "$STATUS" in
pending | running) sleep 10 ;;
*) break ;; # done, failed или ошибка запроса
esac
doneСоздание отвечает 202 и возвращает запрос со статусом pending. Повтор с теми же параметрами, пока
удаление не завершилось, вернёт тот же запрос:
{
"id": "0199d7a2-4c1e-7b3d-9a5f-2e8c6d0b1f34",
"app_id": "0199d5f0-0000-7000-8000-000000000002",
"subject_type": "user",
"subject_value": "acct_8f14e45f",
"status": "pending",
"source": "api",
"requested_at": "2026-10-14T09:20:11.042Z",
"started_at": null,
"completed_at": null,
"error": null
}Статус читается ключом со scope read или delete. done — данные удалены из всех таблиц, failed —
удаление не удалось после нескольких попыток, причина — в поле error. Подробнее — в разделе
Приватность и GDPR.
Чтобы отслеживать свои события (purchase, note_created), отправляйте их в тех же батчах, внутри сессии. Имена, свойства и пределы — в Своих событиях.