Запросы, версии и безопасные повторы
Формат запроса и ответа
GET читает данные; создание, изменение, архивирование, удаление и переходы выполняются POST. Поддерживаются только описанные GET/POST-пути. Не передавайте GET-тело, _method или заголовки переопределения метода.
Команда с телом JSON требует Content-Type: application/json, корректный JSON-объект и Idempotency-Key. Если у команды нет полей, передайте {}. Максимальный размер JSON — 2 МиБ. Неизвестные поля, недопустимые вложенные ключи и запись в поля только для чтения отклоняются. Повторяющиеся ключи JSON не принимаются.
Имена полей — snake_case. Даты: YYYY-MM-DD; моменты: ISO 8601 с Z или явным смещением; недельное время: HH:mm в context.timezone. Точные допустимые форматы и дробная часть времени приведены у параметров операции. updated_at возвращается без дробной части.
Деньги передаются как {"amount":"1250.00","currency":"RUB"}. amount — десятичная строка без экспоненты и разделителей тысяч; точность определяется валютой и ограничениями ресурса. Валюта совпадает с context.currency, конвертации нет. Количество и нормочасы — десятичные строки. Скидка строки акта — сумма на всю строку; скидка договора — процент.
Успешный JSON содержит data и meta, список также содержит внешние links пагинации. POST возвращает meta.operation_id, meta.affected_resources, meta.warnings; Location указывает на /api/v1/api-operations/{id}. Элемент affected_resources: {type,id,version,deleted}. Для удалённого объекта version=null. Предупреждение имеет code, message, resource_refs; пустой массив означает отсутствие предупреждений.
Каждый HTTP-ответ получает новый заголовок X-Request-Id. Поля request_id в JSON нет. Входной клиентский X-Request-Id не становится серверным ID. У 304 нет тела, скачивание файла не имеет JSON-обёртки. POST, повтор запроса, ошибки и скачивание используют Cache-Control: private, no-store.
Idempotency-Key
Формат ключа: 1–128 ASCII-символов A-Z, a-z, 0-9, ., _, :, -. Сохраните ключ вместе с URL, телом, файлами и предусловиями до отправки. Область уникальности — подключение внутри организации. Один ключ на другом пути того же подключения вызывает конфликт.
Повтор принятой команды с прежним ключом и тем же содержимым не выполняет действие заново. Проверяются метод, путь, тело запроса, версии и содержимое файлов. Порядок ключей JSON-объекта несущественен; порядок элементов массива, тип значения, ведущие нули, null и отсутствие ключа значимы. Для multipart/form-data учитываются kind, итоговое имя и содержимое файла, а также условия проверки версии. Разделитель частей запроса не влияет на сравнение.
| Ситуация | Действие клиента |
|---|---|
| Потерян ответ, превышение времени ожидания, 5xx | Повторить исходный запрос с прежним ключом и предусловиями либо прочитать известную операцию |
Получен исходный 200/201 и Idempotency-Replayed: true |
Применить сохранённый результат; свежую карточку получить отдельным GET |
409 OPERATION_IN_PROGRESS |
Учесть Retry-After и опрашивать операцию; второго исполнителя не создавать |
409 IDEMPOTENCY_CONFLICT |
Проверить, что ключ не использован для другого пути, тела, версии или файла |
409 OPERATION_OUTCOME_UNKNOWN |
Проверять журнал и сверять исход; не создавать новую операцию без проверки результата |
409 IDEMPOTENCY_RESULT_EXPIRED |
Получить operation_id из ошибки, проверить журнал и текущие ресурсы; ключ остаётся занятым |
412 VERSION_MISMATCH |
Прочитать состояние, согласовать изменение; новое действие получает новый ключ |
| Ошибка проверки данных до принятия | Исправить данные; без сохранённых изменений и записи об операции операция не была принята |
При повторе запроса проверяется текущий доступ ко всем данным сохранённого результата. Если доступ утрачен, исходное тело не выдаётся частично: возможны 401/403/404. Ротация секрета подключения сама по себе не освобождает ключ и не меняет запись об операции.
Полное тело ответа хранится 24 часа. После его очистки запись об операции сохраняется: тот же ключ не позволяет выполнить действие заново. У асинхронной операции сроки хранения ответа о принятии и окончательного результата отсчитываются отдельно. При повторе запроса версии data.version и предупреждения из сохранённого ответа не пересчитываются. external_id дополняет эту защиту, но не заменяет её.
If-Resource-Version
Из GET карточки или списка скопируйте data.version и передайте без кавычек в If-Resource-Version. Сравнивается версия конкретной записи, а не глобальный номер состояния. Отсутствие обязательной версии даёт 428 VERSION_REQUIRED, неверный формат — 422 INVALID_VERSION, устаревшая версия — 412 VERSION_MISMATCH.
Заголовок содержит одно значение до 128 ASCII-символов из букв, цифр, ., _, ~, -. Кавычки, пробелы, *, W/, запятые и несколько значений не принимаются. В текущей реализации публичная версия — строка Unix-секунд. Клиент сравнивает версии только на равенство и не вычисляет их самостоятельно.
| Команда | Владелец версии |
|---|---|
| Создание самостоятельной сущности | Заголовок не нужен |
| Изменение, переход, удаление карточки | Карточка из пути |
| Добавление работы, товара, оборудования в акт | Акт; для существующих единиц также expected_versions |
| Изменение/удаление строки акта | Сама строка; состояние родительского акта проверяется отдельно |
| Добавление пункта оборудования/сценария, позиции комплекта, варианта поля | Соответствующий родитель |
| Изменение/удаление дочернего пункта | Дочерняя карточка |
| Загрузка вложения акта/выезда | По схеме загрузки, без дополнительного заголовка версии родителя |
Установка/замена фото оборудования (kind=photo) |
Оборудование, включая первую фотографию |
Добавление документа оборудования (kind=document) |
Заголовок версии не нужен |
| Удаление вложения | JSON-карточка вложения |
| Создание выезда | act_version в JSON из GET акта |
| schedule/assign/unschedule и отметки выезда | Выезд |
| initialize/ручной пункт результата ТО | Заявка ТО |
| Изменение пункта результата ТО | Обязательная целая version в JSON, без If-Resource-Version |
| execution заявки ТО | Версия заявки в заголовке и целые версии переданных пунктов в JSON |
| Групповое plan-visit | expected_versions каждого выбранного оборудования или заявки, без общего заголовка |
| Показание наработки | Заголовок не требуется |
| Рабочие окна и исключения сотрудника | Агрегат графика; версия одинакова для разных периодов GET |
Полные исключения, владельцы версий маршрута и условия составной команды перечислены у ресурса. Лишний If-Resource-Version у команды без такого предусловия даёт UNEXPECTED_RESOURCE_VERSION. POST не поддерживает If-Match, If-None-Match, If-Unmodified-Since: их наличие даёт UNSUPPORTED_CONDITIONAL_HEADER.
При повторе принятого запроса запись об операции возвращается до повторной проверки старой версии. Не меняйте только заголовок версии, сохраняя ключ уже принятой операции: это другой отпечаток запроса.
Секундная точность updated_at
data.version — строка целого числа Unix-секунд сохранённого updated_at. Например, updated_at="2026-11-04T09:00:00Z" соответствует version="1793782800". Для старого updated_at=null используется "0". Сам updated_at, если предусмотрен ресурсом, возвращается в UTC с точностью до секунды; null сохраняется.
Сервер проверяет версию и сохраняет изменения как одну операцию. Несовпадение сохранённой секунды обнаруживается. Два изменения внутри одной секунды могут иметь одинаковую версию. Поэтому успешная команда может вернуть прежнее значение version, а равенство версий не доказывает равенство карточек. Изменения без обновления updated_at также не обнаруживаются этой проверкой.
Практические последствия:
- Читайте актуальную карточку перед изменением и выполняйте собственные записи одного ресурса последовательно. Это снижает риск, но не исключает одновременное изменение другим пользователем в ту же секунду.
- Не используйте
versionкак монотонный счётчик, порядок событий или доказательство отсутствия изменений. - При синхронизации применяйте полученное актуальное состояние даже при прежней версии. Для перечитывания используйте события, перекрывающиеся интервалы и полную сверку.
updated_atиmeta.server_timeне являются моментом завершения транзакции и не доказывают полноту уже видимых изменений. Долгая транзакция может стать видимой после прохода с более ранним маркером.- Если рабочий процесс требует обнаружить каждое конкурентное изменение или сохранить каждое промежуточное состояние, такой гарантии текущий контракт не предоставляет. Искусственная задержка клиента не превращает секундный маркер в строгую ревизию.
ETag представления
ETag — отдельный сильный валидатор конкретного GET-представления. Он не равен data.version и не используется для команд. Карточки изменяемых ресурсов выдают ETag; у других GET его наличие определяется операцией. Изменение данных, действующих прав или варианта кодирования может изменить ETag при прежней версии.
Передайте полученный тег с кавычками в If-None-Match. После текущей авторизации совпадение даёт 304 без тела; несовпадение — 200 с актуальным JSON. У 304 остаются ETag, новый X-Request-Id, Cache-Control и Vary. Отозванный доступ не превращается в 304. Ответы JSON GET используют Cache-Control: private, no-cache, Vary учитывает авторизацию и кодирование.
curl --silent --show-error --include \
--header "Authorization: Bearer $SERVICEAPP_TOKEN" \
--header 'If-None-Match: "<ETAG_FROM_PREVIOUS_GET>"' \
"$API/customers/$CUSTOMER_ID"
Поддерживаются identity и gzip. Если клиент запрещает оба, возвращается 406 NOT_ACCEPTABLE. POST и повторные запросы не возвращают ETag; актуальное представление после команды читается отдельным GET.
Асинхронные операции и журнал
Ответ 202 подтверждает принятие и сохранение запроса. Выполнение ещё может продолжаться. Обычно тело содержит data={operation_id,state:"processing"}, тот же идентификатор в meta, а также заголовки Location и Retry-After: 2. Для файловых операций и расчёта маршрута структура data описана отдельно. Повтор принятого POST возвращает исходный ответ о принятии даже после завершения работы. Текущее состояние читайте в GET /api-operations/{id} с разрешением api_operations:read.
state |
Значение |
|---|---|
processing |
Выполнение продолжается; опрос состояния возвращает 200 и Retry-After: 2 |
succeeded |
Подтверждённый успех; http_status и сохранённый response доступны до очистки тела |
failed |
Подтверждённый сбой; изучите error и items, когда они есть |
needs_reconciliation |
Нужна сверка исхода; автоматическое повторное выполнение прекращается |
Для processing/needs_reconciliation финальные http_status и response равны null. response_available=false означает, что тела нет; это не разрешение выполнить команду повторно. Пакет может сохранить подтверждённые результаты отдельных items и общий failed/needs_reconciliation. Проверяйте каждый элемент, не объявляйте весь пакет успешным по наличию части результатов.
Проверяйте состояние с интервалом не меньше указанного в Retry-After; при длительном ожидании увеличивайте интервал. GET журнала только читает состояние и не запускает работу заново. Если первый ответ потерян и идентификатор операции неизвестен, повторите исходный POST без изменений. Список журнала по умолчанию охватывает последние 30 дней; максимальный период — 366 дней. Поля журнала.
Расчёт маршрутов имеет явные команды advance, apply, cancel. Опрос состояния карточки или журнала сам эти команды не исполняет. Используйте контракт маршрутов, где указаны версии шага, бюджет и окончательное применение. Для большого парка регламента используйте контракт регламентов.