API управления

Соглашения

Постраничность курсором, конверт ошибки, коды ответов, ограничение частоты и его заголовки, выбор песочницы и подтверждения необратимых операций.

На этой странице · 9

Правила, общие для всех маршрутов. Дальше в справочнике они не повторяются.

Тело запроса

Management API принимает только JSON. Заголовок Content-Type: application/json обязателен для запросов с телом.

  • Пустое тело при этом заголовке считается за {} — так удобнее вызывать маршруты вроде POST /keys/{id}/rotate, где тела нет, а curl и fetch заголовок всё равно ставят.
  • Тело, ушедшее формой (curl -d '…' без Content-Type), распознаётся отдельно, иначе ответ звучал бы как «поле title обязательно» на запрос, где title как раз прислан.
curl -s -X POST "$STEND/api/v1/sandboxes" -H "Authorization: Bearer $STEND_KEY" -d '{"name":"x"}'
{
  "error": "VALIDATION",
  "message": "Тело запроса должно быть JSON с заголовком «Content-Type: application/json». Похоже, JSON ушёл как поле формы"
}

Битый JSON при верном заголовке отвечает иначе:

{ "error": "INVALID_JSON", "message": "Тело запроса не разбирается как JSON" }

Постраничность

Списки отдают { items, nextCursor }. Курсор — идентификатор последней отданной записи; смещением (offset) листать нельзя: журнал пополняется во время листания, и страница 2 повторила бы часть страницы 1.

Параметры списков

ПараметрЗначение
limitот 1 до 100, по умолчанию 25
cursorзначение nextCursor предыдущей страницы
nextCursornull — записей больше нет
curl -s "$STEND/api/v1/logs?limit=2" -H "Authorization: Bearer $STEND_KEY"
{
  "items": [
    { "id": "cmtty0oo8001h7am3cac6d6jj", "publicId": "req_890aa03867", "serviceCode": "bitrix24", "endpoint": "/crm.deal.list", "statusCode": 404 },
    { "id": "cmtty0oo8001g7am3to5x7fvq", "publicId": "f0c9ea91aae8abf9", "serviceCode": "ozon", "endpoint": "/v3/posting/fbs/list", "statusCode": 200 }
  ],
  "nextCursor": "cmtty0oo8001g7am3to5x7fvq"
}

Негодный курсор — 400, а не пустая страница. Молчаливая пустота читалась бы как «данных больше нет» и обманывала бы скрипт, сохранивший курсор между запусками:

{
  "error": "VALIDATION",
  "message": "Курсор «zzz» недействителен: такой записи больше нет или она не подходит под фильтры",
  "issues": ["cursor: запросите первую страницу без курсора"]
}

Курсор действителен только при том же наборе фильтров: с другими фильтрами запись в выборку не попадёт и придёт та же 400.

Конверт ошибки

Один и тот же во всём API:

{
  "error": "VALIDATION",
  "message": "Некорректные данные запроса",
  "issues": [
    "body.path: Путь мока начинается с /custom/ — по этому адресу его вызывает песочница",
    "body.title: Название не короче 2 символов"
  ]
}

error — машинный код, message — объяснение по-русски, issues — разбор ошибок валидации по полям (только там, где он есть).

Коды ответов

HTTP-коды

КодКогда
200успешное чтение и изменение
201создание объекта (POST /keys, /sandboxes, /mocks, /webhooks, /scenarios, /apps, /mocks/import, ротация ключа)
202работа принята и идёт в фоне (POST /bursts, POST /scenarios/{id}/run, POST /webhooks/{id}/test)
400ошибка проверки данных, битый JSON, негодный курсор, неверное подтверждение
401нет ключа или сессии, ключ не опознан, прислан ключ песочницы
403нет нужной области доступа или попытка выдать права шире своих
404объект не найден или принадлежит другому аккаунту
409конфликт с текущим состоянием: имя занято, объект уже в этом состоянии
422запрос понятен, но запрещён правилом продукта: потолок, зависимость, необратимость
429превышено ограничение частоты

Чужой объект отвечает 404, а не 403: по разнице кодов чужие идентификаторы перебирались бы на существование.

Машинные коды из GET /api/v1/meta: UNAUTHORIZED, SANDBOX_KEY_NOT_ALLOWED, FORBIDDEN, SANDBOX_NOT_FOUND, NOT_FOUND, VALIDATION, INVALID_JSON, CONFIRM_MISMATCH, CONFLICT, UNPROCESSABLE, RATE_LIMITED, INTERNAL. Отдельные маршруты добавляют свои: TARGET_NOT_ALLOWED, EVENT_SERVICE_MISMATCH, UNKNOWN_EVENT, NO_WEBHOOK, NO_WEBHOOK_FOR_EVENT, WEBHOOK_PAUSED, NO_AGENT, TOO_MANY_BURSTS, NO_KEY.

422, а не 429, у потолков продукта

Потолок одновременных серий событий отвечает 422, хотя в кабинете это 429. В Management API код 429 занят ограничением частоты и приходит с Retry-After; клиент с обычной логикой «429 → подождать и повторить» на потолке продукта уходил бы в бесконечный повтор.

Ограничение частоты

240 запросов в минуту на субъект — на ключ, а для браузера на сессию. Значение задаётся переменной MGMT_RATE_LIMIT и приходит в GET /api/v1/meta. Ёмкость равна минутной норме, поэтому короткая пачка запросов проходит целиком, а ровный поток упирается в норму.

Заголовки есть на каждом ответе:

HTTP/1.1 200 OK
x-ratelimit-limit: 240
x-ratelimit-remaining: 239
x-ratelimit-reset: 0

При превышении:

HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 240
x-ratelimit-remaining: 0
x-ratelimit-reset: 1
retry-after: 1
{
  "error": "RATE_LIMITED",
  "message": "Слишком много запросов к Management API: не больше 240 в минуту. Повторите через 1 с."
}

X-RateLimit-Reset — секунды до освобождения хотя бы одного запроса, а не время полного восстановления ведра.

У выгрузки журнала своё, куда более строгое ведро — шесть выгрузок в минуту: каждая читает десятки тысяч записей в памяти того же процесса, что обслуживает мок-шлюз. Считается оно отдельно, поэтому обычные запросы от этого не страдают — X-RateLimit-Remaining в таком ответе показывает остаток общей нормы:

HTTP/1.1 429 Too Many Requests
x-ratelimit-remaining: 231
retry-after: 10
{
  "error": "RATE_LIMITED",
  "message": "Выгрузок журнала не больше 6 в минуту. Повторите через 10 с или сузьте период полями from и to"
}

Лимит считается в памяти процесса

При нескольких инстансах APIStend норма станет кратно мягче — это осознанный размен: поход в общее хранилище на каждый запрос стоил бы дороже самого запроса.

Идемпотентности нет

Заголовка Idempotency-Key в Management API нет, и повторный POST создаёт второй объект. Безопасность повтора обеспечивают сами операции:

  • уникальность имени песочницы и пары «метод + путь» у мока — повтор даёт 409;
  • повторный импорт того же файла спецификации кладёт уже существующие пути в skipped, а не перезаписывает;
  • POST /mocks/{id}/enable и /disable при повторе ничего не меняют и не считаются ошибкой (changed: false);
  • POST /alerts/{id}/read не переписывает время первого прочтения (alreadyRead: true).

Для остальных операций повтор после сетевой ошибки нужно проверять чтением: запросить список и убедиться, что объект не создан дважды.

Выбор песочницы

Почти всё в API живёт внутри песочницы. Она ищется по порядку:

  1. 1

    :sandboxId в пути

    Только у маршрутов /sandboxes/{sandboxId}….

  2. 2

    ?sandboxId= в строке запроса

    Работает у всех маршрутов, привязанных к песочнице.

  3. 3

    Песочница ключа

    Серверный ключ выпущен внутри песочницы, и она считается текущей.

  4. 4

    Первая песочница аккаунта

    Так работает cookie-сессия кабинета. У кого одна песочница — тот про неё вообще не вспоминает.

Если песочницы нет вовсе, приходит 404 SANDBOX_NOT_FOUND с подсказкой создать её через POST /api/v1/sandboxes.

Подтверждения необратимых операций

Удаление ключа, песочницы, приложения и аккаунта требует повторить имя объекта в теле запроса — confirmName (у аккаунта — login). Несовпадение даёт 400 с отдельным кодом:

{
  "error": "CONFIRM_MISMATCH",
  "message": "Удаление необратимо. Повторите имя песочницы в поле confirmName — ожидается «ci-doc»"
}

Очистка журнала подтверждается флагом confirm: true.

Даты и время

Все даты — ISO 8601 в UTC (2026-09-09T10:15:33.470Z). Фильтры from и to в журнале принимают и смещение: 2026-09-09T00:00:00+03:00 разбирается верно, пересчитывать в UTC руками не нужно.