Проектирование API для интеграций сводится к предсказуемости: одинаковые запросы дают одинаковые результаты, ошибки описаны и понятны программе, повторный запрос не создаёт дубль, а изменения не ломают чужие системы. Для этого заранее решают пять вещей: единый формат ошибок, ключ идемпотентности для операций записи, ограниченную постраничную выдачу (курсор для выгрузок между системами), правила версий и документацию в формате OpenAPI, которую проверяют тестами.

Статья для технических руководителей и разработчиков, которые заказывают или принимают интерфейс для обмена между системами. Примеры условные: REST поверх HTTP, данные в JSON. Если нужна серверная часть под такой интерфейс, смотрите страницу «Разработка API и бэкенда на заказ».

Коротко. Описывайте ресурсы существительными, отвечайте настоящими кодами и одним форматом ошибки, защищайте запись ключом идемпотентности или внешним идентификатором. Списки отдавайте страницами ограниченного размера, а для выгрузки в другую систему с курсором. Версию вводите в первый день, а менять формат внутри версии разрешайте только добавлением. Вебхуки подписывайте и считайте доставляемыми не меньше одного раза. Документация OpenAPI и тестовый стенд входят в приёмку.

Ресурсы и операции: из чего строится интерфейс

Ресурс — то, с чем работает клиент: заказ, контрагент, документ, остаток. У каждого ресурса свой адрес, а операции задаются методами HTTP.

  • Имена ресурсов — существительные во множественном числе. Адрес /orders, а не /createOrder. Список и один объект читаются методом GET, создание идёт через POST, частичное изменение через PATCH, удаление через DELETE.
  • Действия, которые не сводятся к правке полей (отмена, проведение, пересчёт), оформляйте отдельной операцией, например POST на /orders/{id}/cancel. Смена статуса записью в поле прячет правила перехода, а отдельная операция проверяет их и возвращает понятный результат.
  • Внешний идентификатор. Кроме своего номера записи принимайте идентификатор из системы клиента (external_id) и не допускайте двух записей с одним внешним идентификатором в пределах клиента. По нему клиент ищет записи и связывает их со своими.
  • Деньги и даты. Сумма передаётся строкой с фиксированным числом знаков либо целым числом в минимальных единицах, но не числом с плавающей точкой. Моменты времени (создание, изменение) передавайте с часовым поясом в одном формате ISO, например 2026-10-21T10:30:00+03:00. Календарные даты документов и поставок — без времени (2026-10-21): иначе при обмене с системой, которая хранит дату без пояса, она может сместиться на сутки.
  • Статусы — перечень с описанными значениями. Значения и допустимые переходы описаны в документации: клиенту нужно знать и что значит «в работе», и откуда в этот статус можно попасть. Там же записано, что перечень может пополняться и клиент терпимо относится к незнакомому значению; без такой записи каждое новое значение ломает клиентов.

Коды ответов и формат ошибок

Клиент-программа решает, что делать, по коду ответа и телу ошибки. Если сервер на любую ошибку отвечает 200 и пишет причину текстом, клиенту остаётся разбирать строку, а любая правка формулировки ломает интеграцию. Коды выбирают так, чтобы по ним было видно, что делать дальше.

КодКогда возвращаемЧто делает клиент
200, 201, 202, 204Успех. 201 — создано, адрес нового объекта в заголовке Location; 202 — принято, но ещё выполняется, в теле номер задачи; 204 — успех без телаПродолжает работу; после 202 читает статус задачи
400Запрос не разобран: сломан JSON, нет обязательного параметра, неверный форматИсправляет запрос; повтор без изменений бессмыслен
401Ключ или токен не предъявлен либо недействителенПолучает новый токен или проверяет ключ
403Ключ верный, но прав на операцию нетНе повторяет; обращается за правами
404Объекта нет или он не виден этому ключуПроверяет идентификатор
409Конфликт состояния: устаревшая версия записи, повтор внешнего идентификатора, недопустимый переход статусаПеречитывает объект и решает, что делать
422Формат верный, но данные не проходят проверку правилИсправляет данные по списку ошибок
429Превышена частота запросовЖдёт время из заголовка Retry-After
500, 503Ошибка на стороне сервера или временная недоступностьПовторяет позже с растущей паузой; запись — с ключом идемпотентности или внешним идентификатором

Выбор между 400 и 422 вторичен: важно, чтобы он был одинаковым во всём интерфейсе и записан в документации. Для чужого объекта можно возвращать 404, а не 403, чтобы не раскрывать, что такой объект существует.

Тело ошибки имеет одну структуру на весь интерфейс: стабильный машинный код (его не переименовывают), сообщение для человека, список полей с причинами и номер запроса, по которому ошибку находят в журнале. Клиент решает по коду, а сообщение лишь показывает пользователю. Стек вызовов и тексты внутренних исключений в ответ не попадают. За основу можно взять стандартный формат application/problem+json или описать свой, главное — единый.

{
  "code": "validation_failed",
  "message": "Количество должно быть больше нуля",
  "details": [
    {"field": "lines[0].qty", "reason": "must_be_positive"}
  ],
  "request_id": "b1c2d3e4"
}

Повторы запросов и ключ идемпотентности

Запрос ушёл, ответ не пришёл. Клиент не знает, выполнена ли операция, и повторяет её. Для чтения это безопасно. Для записи это риск дубля: два заказа, два платежа, два документа. По определению HTTP повтор не меняет итоговое состояние у чтения, замены целиком (PUT) и удаления (DELETE), а у создания через POST меняет. Поэтому повтор POST допустим только вместе с ключом идемпотентности или внешним идентификатором, который база не даёт записать дважды. PUT и DELETE повторять можно, но клиент должен уметь обработать ответ на повтор, например 404 на повторное удаление.

Решение для POST — ключ идемпотентности. Клиент создаёт случайный идентификатор на каждую логическую операцию и передаёт его в заголовке. Сервер запоминает ключ вместе с результатом.

POST /v1/orders
Idempotency-Key: 6f1c0d2e-9a47-4b8e-8c55-3d2f7a1b9e10
Content-Type: application/json

{"external_id": "WEB-428", "customer_id": 311,
 "lines": [{"sku": "A-100", "qty": 2}]}
  1. Первый запрос с этим ключом выполняется, сервер сохраняет ответ вместе с ключом и отпечатком тела запроса.
  2. Повтор с тем же ключом и тем же телом возвращает сохранённый ответ. Второго заказа нет.
  3. Тот же ключ с другим телом — ошибка, например 422: клиент перепутал ключи или повторил ключ для нового запроса, и выполнять другой запрос под уже занятым ключом нельзя.
  4. Два одновременных запроса с одним ключом: второй ждёт или получает 409. Операция выполняется один раз.

Реализация идемпотентности может сломаться в трёх местах. Если операция пишет в ту же базу, запись ключа и результат операции должны быть в одной транзакции, иначе сбой между ними даст дубль или потерянную операцию. Если у операции есть внешнее действие (запрос в 1С, платёж, письмо), одной транзакции мало: фиксируйте состояние «в работе» до вызова, передавайте во внешнюю систему собственный идентификатор операции и делайте повторный вызов безопасным. Срок хранения ключей ограничен и указан в документации, иначе таблица ключей растёт бесконечно. И для обмена между системами нужна вторая линия защиты: внешний идентификатор, который база не позволяет записать дважды.

Та же логика действует у получателя: если вебхук или сообщение из очереди пришло повторно, обработка не должна создавать вторую запись. Как это закладывается в обмен с учётной системой, описано на странице «Интеграция 1С».

Постраничная выдача, фильтры и ограничение частоты

Страницы

Список без ограничения размера однажды перестанет помещаться в ответ, в память сервера или во время ожидания клиента. Есть два основных способа:

  • Номер страницы и размер (offset). Просто, и можно перейти на любую страницу. Но если данные меняются между запросами, записи пропускаются или повторяются, а на больших смещениях база читает и отбрасывает много строк.
  • Курсор. Ответ содержит непрозрачную строку, по которой сервер находит место, где остановился. Устойчив к добавлению и удалению записей, но на произвольную страницу не перейти.

Для интерфейса с номерами страниц подходит первый способ, для выгрузки данных в другую систему — курсор. Максимальный размер страницы задаёт сервер. В ответе есть курсор следующей страницы или явный признак, что страниц больше нет.

Выгрузка изменений

Система, которая синхронизирует данные, спрашивает: что изменилось с прошлого раза. У фильтра по времени изменения (updated_after) две ловушки. Сортировать нужно по паре «время изменения, идентификатор», иначе записи, изменённые в одну и ту же секунду, пропадают на границе страниц. И клиент берёт небольшой запас по времени и спокойно принимает повторы: идемпотентность это допускает. Удалённые записи тоже передают, признаком или отдельным списком, иначе клиент вечно хранит то, чего уже нет.

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

Лимит ставят на ключ клиента, отдельно для чтения и записи. При превышении сервер отвечает 429 и сообщает в заголовке Retry-After, когда повторять. Тяжёлые операции (отчёт, массовый пересчёт, выгрузка) оформляют задачей: запрос создаёт задачу и сразу отвечает кодом 202 с её номером, статус читается отдельно, готовый результат забирается отдельным запросом. Держать соединение открытым минутами не стоит: таймауты клиента, прокси и балансировщика его оборвут, клиент повторит запрос, и без ключа идемпотентности операция выполнится дважды.

Версии: как менять API и не сломать клиентов

Системы ваших клиентов обновляются годами, и вы не управляете их релизами. Версия — договорённость с клиентом: пока он вызывает /v1, формат ответа остаётся прежним. Номер версии закладывают в первый день, в адресе (/v1/orders) или в заголовке. В адресе проще для клиентов и заметнее в журналах.

Что можно менять внутри версии

  • Добавить необязательное поле в ответ.
  • Добавить новую операцию или новый ресурс.
  • Добавить необязательное поле или параметр в запрос.

Условие одно: клиент игнорирует незнакомые поля. Запишите это правило в документацию. Клиент, который проверяет ответ по строгой схеме, сломается даже от нового поля.

Что ломает клиентов и требует новой версии

  • Удаление или переименование поля.
  • Смена типа или формата: число стало строкой, дата записана иначе.
  • Смена смысла значения при том же названии.
  • Новое обязательное поле в запросе.
  • Новое значение статуса, если в документации не сказано, что перечень может пополняться.
  • Более строгая проверка, которая отвергает запросы, раньше считавшиеся допустимыми.
  • Смена сортировки или кодов ошибок.

Как выводить старую версию

Объявить, назначить дату отключения, по журналу выяснить, какие ключи ещё вызывают старую версию, предупредить их владельцев и отключить в назначенный день. Две версии живут одновременно, и это стоит денег на поддержку, поэтому закладывайте её заранее. Не соглашайтесь на «правим тихо, клиент у нас один»: клиент один только сегодня.

Ключи, права и вебхуки с повторной доставкой

Ключи и права

  • Отдельный ключ на каждую подключаемую систему. Общий ключ нельзя отозвать у одного клиента, не остановив остальных, а в журнале не видно, кто что сделал.
  • Права по операциям и ресурсам. Чтение и запись разделены, ключ магазина читает только заказы этого магазина.
  • Проверка на сервере для каждого объекта, а не только для операции. Подстановка чужого идентификатора в адрес — типовая дыра.
  • Ключ в заголовке, а не в адресе: адреса попадают в журналы серверов и прокси.
  • Перевыпуск без остановки. На время замены действуют два ключа, старый отключается после проверки нового.

Вебхуки

Вебхук — это когда ваш сервис сам сообщает клиенту о событии, а не ждёт вопроса. У него свои правила.

  • Доставка «не меньше одного раза». Событие может прийти дважды и не по порядку. У каждого события есть идентификатор и время, получатель хранит обработанные идентификаторы и пропускает повторы.
  • Подпись и время. Отправитель считает HMAC общим секретом по строке из времени отправки и тела события, так что время входит в подпись, и передаёт время в заголовке. Получатель пересчитывает подпись своим секретом и отвергает события старше нескольких минут: без секрета событие не подделать, а перехваченное настоящее нельзя отправить повторно. Идентификаторы обработанных событий всё равно хранят: повтор внутри окна подпись не отличит.
  • Быстрый ответ. Получатель отвечает кодом 2xx сразу, а тяжёлую обработку отдаёт очереди. Долгий ответ отправитель воспринимает как сбой и повторяет доставку.
  • Повторы отправителя. При ошибке или таймауте отправитель повторяет с растущей паузой и ограниченным числом попыток. Недоставленное остаётся в журнале, откуда его можно отправить вручную.
  • Лёгкое событие. Передайте тип, идентификатор объекта и время, а полные данные получатель запросит по API. Тогда устаревшее или пришедшее не по порядку событие не навредит: получатель читает актуальное состояние.

Документация OpenAPI и тестовый стенд

OpenAPI — файл в формате YAML или JSON, в котором интерфейс описан так, чтобы его читали и человек, и программа. По нему собирают страницу документации, заготовки клиентов и тесты, сверяющие ответы сервера со схемами.

  • Документация совпадает с поведением. Либо сначала пишут описание, а код строят по нему, либо описание генерируют из кода. В обоих случаях в сборке есть контрактные тесты: они прогоняют запросы по описанию и падают при расхождении. Если описание генерируется из кода, этого мало: схема, построенная из того же кода, сама с собой не спорит, поэтому нужны ещё тесты на реальные сценарии.
  • Кроме схем в документации есть примеры запроса и ответа для каждой операции, таблица ошибок, правила повторов и ключа идемпотентности, лимиты, политика версий и порядок получения ключа.
  • Тестовый стенд. Отдельный адрес, отдельные ключи, тестовые данные и способ их сбросить. Стенд умеет вести себя плохо по запросу: отвечать 429 и 503, задерживать ответ, присылать событие дважды. Клиент проверяет обработку сбоев до боевого запуска, а не во время него.
  • Боевых персональных данных на стенде нет.
  • Журнал изменений ведётся как часть интерфейса: о новом поле или отключении версии клиент узнаёт из документа, а не из аварии.

Ошибки проектирования и чек-лист приёмки

Восемь типичных ошибок и то, что от них происходит у клиента.

Ошибка проектированияЧто случается у клиентаКак делать
Все ошибки приходят с кодом 200 и текстом в телеКлиент не отличает успех от отказа и разбирает текст, который потом поменялиНастоящие коды ответов и единый формат ошибки с машинным кодом
Запись без защиты от повторовСеть моргнула, и в учёте два одинаковых заказаКлюч идемпотентности и внешний идентификатор, который база не даёт записать дважды
Поле переименовали в той же версииЧужая интеграция молча перестала читать данныеВнутри версии только добавление; переименование — новая версия с периодом параллельной работы
Новое значение статуса без предупрежденияКлиентский код не знает значения и падает или пропускает заказПеречень описан, клиентам сказано терпимо относиться к незнакомым значениям, новые значения анонсируются
Один общий ключ на всехКлюч утёк у одной системы или подрядчик ушёл, и менять приходится ключ у всех сразу; в журнале не видно, кто что сделалКлюч на каждую систему, права по операциям, перевыпуск без остановки
Права проверяются по операции, но не по объектуКлюч одного клиента читает объекты другого, подставив чужой идентификаторПроверка принадлежности объекта на сервере в каждой операции
Вебхуки без подписи и повторовПодделанное событие принято, а временно недоступный получатель потерял событияПодпись, идентификатор события, повторы с паузой, журнал недоставленных
Тяжёлая операция выполняется внутри запросаКлиент ждёт до таймаута, повторяет запрос, и операция может выполниться дважды, если запись не защищенаЗадача: ответ сразу с номером, статус отдельным запросом, результат отдельно

Чек-лист приёмки API

Ошибки из таблицы на приёмке проверяют тестами, а не обещанием подрядчика. По пунктам:

  • Описание OpenAPI лежит в репозитории и совпадает с поведением: контрактные тесты проходят. Версия указана, политика изменений и дата отключения старых версий описаны.
  • У каждой ошибки настоящий код и единый формат; тексты исключений и стек в ответ не попадают.
  • Запрос записи, отправленный дважды, не создаёт дубль: проверено, а не заявлено.
  • Списки отдаются страницами ограниченного размера; выгрузка изменений не пропускает записи на границе страниц и передаёт удалённые.
  • Ключ каждой системы отзывается и перевыпускается без остановки обмена; подстановка чужого идентификатора из-под любого ключа не открывает чужие объекты.
  • Лимиты частоты работают, ответ 429 содержит Retry-After.
  • Вебхуки подписаны, у событий есть идентификатор, при ошибке доставка повторяется, список недоставленного есть.
  • Есть тестовый стенд с отдельными ключами и тестовыми данными, он умеет имитировать сбои.
  • Поведение при сбое смежной системы проверено отключением: ничего не потеряно, оповещение ушло.
  • Профиль нагрузки согласован (кто и какие операции вызывает, когда бывают пики, как растёт объём данных), перед промышленной эксплуатацией проверка прошла на данных сопоставимого объёма, измеренные значения записаны в протокол испытаний. Цифра «выдержит столько-то запросов» без такого профиля ничего не значит.

Что делаем мы и с чего начать

Мы проектируем и разрабатываем один API или одну интеграционную связку: от описания OpenAPI до очередей, журнала и сопровождения. Публичные платформы для неограниченного круга внешних разработчиков с порталом и биллингом не строим: ключи, права и лимиты делаем для партнёров и систем, которых можно перечислить. Услуга описана на странице «Разработка API и бэкенда на заказ». Если нужно связать 1С с сайтом, CRM или маркетплейсом, начинаем со схемы обмена: интеграция 1С, интеграция сайта с 1С, разбор CommerceML. Если хватает стандартного обмена, так и скажем.

Начать можно с проверки того, что у вас есть: пройдите по чек-листу выше с вашим интерфейсом или с описанием, которое прислал подрядчик. Если разбора за несколько дней недостаточно, обследование стоит 200 000 ₽ за один-два процесса и 350 000 ₽, когда в обмене участвует несколько систем. Оно вычитается из первого этапа, если продолжаете с нами. Один API или одну связку можно проверить лёгким пилотом за 150 000–400 000 ₽ и 2–8 недель. Цены и сроки ступеней — на странице тарифов. На выходе обследования карта процессов, требования, план по этапам и вилка бюджета, а при нескольких системах ещё схема обменов. Порядок один: сначала согласованный объём (обследование или, для небольшого пилота, письменный список «входит / откладываем»), затем контракт OpenAPI первым шагом разработки; его согласуем с вами до кода. Исходники и документация передаются вам по акту.

Частые вопросы

Какой протокол выбрать для интеграции: REST или что-то другое?

Если нет причины для другого, берите REST поверх HTTP с данными в JSON. Другой способ выбирают по конкретному ограничению: SOAP, когда смежная система предоставляет только его; обмен файлами, когда у системы нет программного интерфейса; очередь сообщений, когда отправитель не должен ждать получателя. Причину записывают в описание обмена, чтобы через год было понятно, почему здесь не REST.

Нужна ли версия в адресе, если клиент всего один?

Нужна. Сегодня клиент один, завтра появится второй, а у первого к тому времени будет собственный релизный цикл. Задним числом версию приходится вводить вместе с миграцией всех уже подключённых клиентов, поэтому её закладывают в первый день.

Чем идемпотентность отличается от дедупликации?

Идемпотентность — свойство операции: повтор даёт тот же результат и не создаёт вторую запись. Дедупликация — один из механизмов: получатель распознаёт повтор по ключу или идентификатору события и отбрасывает его. Ключ идемпотентности использует дедупликацию, но обещает клиенту ещё и тот же ответ.

Что выбрать для списков: номер страницы или курсор?

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

Как проверить, что документация соответствует реальному API?

Описание OpenAPI хранят в репозитории, а в сборке запускают контрактные тесты: они отправляют запросы по описанию и сравнивают ответы со схемами. Расхождение находит сборка, а не клиент. Примеры из документации тоже прогоняют в тестах, иначе они устаревают. Если описание генерируется из кода, одних контрактных тестов мало: нужны ещё тесты на реальные сценарии.

Сколько раз клиенту повторять запрос при ошибке сервера?

Несколько раз, а не бесконечно: пауза растёт с каждой попыткой и получает небольшой случайный разброс, чтобы клиенты не ударили по серверу одновременно. На ответ 429 клиент ждёт время из Retry-After, на 400 и 403 не повторяет совсем. После последней попытки задача уходит в очередь или оповещение. Точное число зависит от того, как долго данные остаются актуальными. Создание через POST повторяют только с ключом идемпотентности или внешним идентификатором.

Нужен ли отдельный тестовый стенд?

Да, если интерфейсом пользуется кто-то кроме вас. На стенде клиент проверяет обработку ошибок и сбоев до боевого запуска. Боевых персональных данных на стенде не хранят.

Сколько стоит спроектировать и разработать API?

Начать можно с обследования: 200 000 ₽ за один-два процесса (2–3 недели) и 350 000 ₽, когда в обмене несколько систем (3–4 недели). Сумма вычитается из первого этапа, если продолжаете с нами. Затем контракт OpenAPI первым шагом разработки и сам интерфейс, например в лёгком пилоте за 150 000–400 000 ₽ и 2–8 недель для одного API или одной связки. Цену двигают число потребителей интерфейса, качество чужих систем и требования к надёжности. Ступени и сроки указаны на странице «Тарифы».