Проектирование API для интеграций сводится к предсказуемости: одинаковые запросы дают одинаковые результаты, ошибки описаны и понятны программе, повторный запрос не создаёт дубль, а изменения не ломают чужие системы. Для этого заранее решают пять вещей: единый формат ошибок, ключ идемпотентности для операций записи, ограниченную постраничную выдачу (курсор для выгрузок между системами), правила версий и документацию в формате OpenAPI, которую проверяют тестами.
Статья для технических руководителей и разработчиков, которые заказывают или принимают интерфейс для обмена между системами. Примеры условные: REST поверх HTTP, данные в JSON. Если нужна серверная часть под такой интерфейс, смотрите страницу «Разработка API и бэкенда на заказ».
Ресурсы и операции: из чего строится интерфейс
Ресурс — то, с чем работает клиент: заказ, контрагент, документ, остаток. У каждого ресурса свой адрес, а операции задаются методами 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}]}- Первый запрос с этим ключом выполняется, сервер сохраняет ответ вместе с ключом и отпечатком тела запроса.
- Повтор с тем же ключом и тем же телом возвращает сохранённый ответ. Второго заказа нет.
- Тот же ключ с другим телом — ошибка, например 422: клиент перепутал ключи или повторил ключ для нового запроса, и выполнять другой запрос под уже занятым ключом нельзя.
- Два одновременных запроса с одним ключом: второй ждёт или получает 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 или одной связки. Цену двигают число потребителей интерфейса, качество чужих систем и требования к надёжности. Ступени и сроки указаны на странице «Тарифы».