API и ключи интеграции

Создайте ключ в кабинете и подключите товары, услуги и вопрос / ответ: запросы, JSON, полный импорт и обработка ошибок.

Внешний API синхронизирует ровно три справочника вашего сайта: товары (product), услуги (service) и вопрос / ответ (qa). Один ключ принадлежит одному источнику одного сайта. Он читает и меняет записи своего источника; вручную созданные записи и записи других источников через этот ключ не видны. API переписки, управления аккаунтами и администратора в эту интеграцию не входят.

Ключ и первый запрос

Откройте кабинет, создайте сайт и перейдите в «Интеграции». Для создания ключа и всех запросов внешнего API нужен действующий расширенный доступ. Если он выключен или закончился, кабинет объяснит причину; включение и продление выполняет администратор сервиса.

  1. Укажите понятное название источника, например «Каталог интернет-магазина», и нажмите «Создать ключ».
  2. Скопируйте показанный ключ в защищённое окружение своего серверного приложения. Список источников и повторный вход в кабинет не раскрывают сохранённый секрет.
  3. Используйте проверку подключения в кабинете или выполните GET /check из примера ниже.
  4. Создайте тестовую запись через POST /records и проверьте её в разделе «Справочники» кабинета.

Передавайте ключ только в заголовке Authorization: Bearer. Не размещайте его в адресной строке, HTML сайта, коде виджета или браузерном JavaScript. Примеры ниже выполняются на сервере вашей интеграции; ключ вводится без записи в историю команд. Отдельные cookie, CSRF-токен и заголовок версии сборки для /api/external не нужны.

Подготовка окружения
# Выполняйте команды в Bash на сервере своей интеграции.
export CONSUL_API_BASE='https://consul.miada.ru/api/external'
read -r -s -p 'API-ключ Consul: ' CONSUL_API_KEY
export CONSUL_API_KEY
printf '\n'
Безопасная проверка ключа и доступа
curl --fail-with-body --silent --show-error "$CONSUL_API_BASE/check" \
  -H "Authorization: Bearer ${CONSUL_API_KEY}"

Успех GET /check — HTTP 200 и объект {ok:true, source:{id,name}, site:{id,name}, access_until:…}. Проверка ничего не создаёт. Успешный GET /records возвращает массив; [] означает, что у этого источника ещё нет записей.

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

Форматы трёх справочников

Тело POST /records — один объект с external_id, external_revision и content. external_id — постоянный идентификатор в вашей системе, до 200 символов; он уникален внутри источника и типа записи. external_revision — целое число от нуля, увеличивающееся при изменении данных. content.kind имеет только значения product, service или qa. Не передавайте в JSON неописанные поля.

Сохраните следующий объект в qa.json. Он пригоден для первого запроса. Общие поля content: обязательное непустое title (до 300 символов), description (до 20 000), url (полный URL или null), tags (до 50 меток, каждая до 100 символов). Для qa дополнительно обязателен непустой answer (до 20 000 символов).

qa.json — вопрос / ответ
{
  "external_id": "delivery",
  "external_revision": 1,
  "content": {
    "kind": "qa",
    "title": "Как получить заказ?",
    "description": "Получение готовых изделий",
    "url": null,
    "tags": [
      "доставка"
    ],
    "answer": "Готовый заказ можно забрать в мастерской по предварительной договорённости."
  }
}

Товар содержит variants и offers. У каждого варианта свой id и SKU; attributes хранит строки, числа или логические значения. Предложение с variant_id ссылается на существующий вариант либо содержит null. ID вариантов и предложений не должны повторяться внутри записи.

product.json — товар с вариантом и предложением
{
  "external_id": "mug-ceramic",
  "external_revision": 1,
  "content": {
    "kind": "product",
    "title": "Керамическая кружка",
    "description": "Кружка ручной работы объёмом 300 мл.",
    "url": null,
    "tags": [
      "посуда"
    ],
    "variants": [
      {
        "id": "green",
        "sku": "MUG-GREEN",
        "attributes": {
          "Цвет": "Зелёный",
          "Объём, мл": 300
        }
      }
    ],
    "offers": [
      {
        "id": "retail-green",
        "variant_id": "green",
        "price": "1500.00",
        "currency": "RUB",
        "unit": "шт.",
        "available": true,
        "quantity": "5",
        "checked_at": "2026-10-31T10:00:00Z",
        "fresh_until": "2026-11-01T10:00:00Z",
        "conditions": "Самовывоз из мастерской"
      }
    ]
  }
}

price и quantity передаются десятичными строками: до 12 цифр в целой части и до 4 после точки. null означает неизвестное значение, строка «0» — ноль. available: true, false или null; currency — код из трёх заглавных латинских букв. checked_at и fresh_until — UTC-даты проверки и окончания актуальности; fresh_until не может быть раньше checked_at. В примере товара даты показывают формат: перед загрузкой укажите реальные сроки своего предложения. Истёкшее предложение хранится, но не подтверждает актуальную цену и наличие.

service.json — услуга с диапазоном цены
{
  "external_id": "ceramic-workshop",
  "external_revision": 1,
  "content": {
    "kind": "service",
    "title": "Мастер-класс по керамике",
    "description": "Индивидуальное занятие для начинающих.",
    "url": null,
    "tags": [
      "обучение"
    ],
    "includes": "Материалы, работа с мастером и обжиг",
    "exclusions": "Доставка готового изделия",
    "requirements": "Предварительная запись",
    "geography": "В мастерской",
    "duration_minutes": 90,
    "pricing": {
      "kind": "range",
      "amount": "2500.00",
      "maximum": "3500.00",
      "currency": "RUB"
    }
  }
}

Для услуги pricing.kind: fixed — фиксированная цена, from — цена «от», range — диапазон, individual — индивидуальный расчёт. amount и maximum используют те же десятичные строки или null; для известного диапазона maximum не меньше amount. duration_minutes — положительное целое число или null. includes, exclusions, requirements и geography описывают состав, исключения, требования и место оказания услуги.

Создание, обновление, чтение и удаление

  • GET /api/external/check — проверить ключ, источник, сайт и срок доступа.
  • GET /api/external/records — прочитать до 500 записей своего источника; after_id задаёт следующую страницу.
  • POST /api/external/records — создать или обновить одну запись по external_id и content.kind.
  • DELETE /api/external/records/:id — удалить запись по внутреннему id с operation_id и expected_revision в JSON-теле.
Создать вопрос / ответ из qa.json
curl --fail-with-body --silent --show-error "$CONSUL_API_BASE/records" \
  -H "Authorization: Bearer ${CONSUL_API_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary @qa.json

HTTP 200 подтверждает сохранение. POST возвращает сохранённую запись с внутренним id и revision. Чтобы изменить её, сохраните прежние external_id и kind, увеличьте external_revision и отправьте полное новое content тем же POST. Одинаковая внешняя ревизия с теми же данными безопасно повторяется; с другими данными или более старая ревизия вызывает CONFLICT. revision из ответа — отдельная внутренняя ревизия для защиты удаления от устаревших данных.

Прочитать записи и следующую страницу
curl --fail-with-body --silent --show-error "$CONSUL_API_BASE/records" \
  -H "Authorization: Bearer ${CONSUL_API_KEY}"

# Следующая страница: передайте id последней записи предыдущей страницы.
read -r -p 'Последний id предыдущей страницы: ' CONSUL_AFTER_ID
curl --fail-with-body --silent --show-error "$CONSUL_API_BASE/records?after_id=$CONSUL_AFTER_ID" \
  -H "Authorization: Bearer ${CONSUL_API_KEY}"
Пример ответа GET /records
[
  {
    "id": "5f497861-39c1-4aba-9f87-e19d0bb5cdee",
    "kind": "qa",
    "external_id": "delivery",
    "external_revision": 1,
    "content": {
      "kind": "qa",
      "title": "Как получить заказ?",
      "description": "Получение готовых изделий",
      "url": null,
      "tags": [
        "доставка"
      ],
      "answer": "Готовый заказ можно забрать в мастерской по предварительной договорённости."
    },
    "revision": 1
  }
]

Записи отсортированы по внутреннему id. Для следующей страницы задайте CONSUL_AFTER_ID равным id последней полученной записи и передайте after_id. Продолжайте до страницы меньше 500 записей; после полной страницы из 500 может следовать пустой массив. Не подставляйте external_id в этот параметр.

Удалить полученную запись
# Возьмите внутренний id и revision из ответа GET /records.
CONSUL_RECORD_ID='5f497861-39c1-4aba-9f87-e19d0bb5cdee'
curl --fail-with-body --silent --show-error -X DELETE "$CONSUL_API_BASE/records/$CONSUL_RECORD_ID" \
  -H "Authorization: Bearer ${CONSUL_API_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary '{"operation_id":"3d24021f-f50d-439e-917f-f10960517c45","expected_revision":1}'

В примере удаления замените id и expected_revision значениями из своего ответа. Создавайте новый случайный UUID operation_id для каждого нового логического удаления и сохраняйте его до получения ответа. Повтор того же удаления с тем же ID и телом безопасен. Повтор ID с другими данными или устаревшая expected_revision даёт HTTP 409; перечитайте запись перед новым осознанным действием. Успех удаления — объект {id:…}.

Изменение импортированной записи вручную в кабинете создаёт полное ручное переопределение. Последующие запросы источника обновляют исходные данные, сохраняя этот ручной вариант. GET возвращает действующее содержимое. Вернуться к данным источника можно кнопкой сброса ручных изменений в справочнике кабинета при действующем доступе.

Полный импорт

Используйте полный снимок для согласованной замены всех записей одного источника. Начало и отправка пакетов создают staging-снимок; действующие справочники меняются только после complete. У источника может быть только один незавершённый снимок. Незавершённый или отменённый импорт ничего не удаляет.

В снимок должны войти все записи источника, которые нужно сохранить. Следующий учебный снимок содержит только один вопрос / ответ: после complete остальные записи этого источника без ручных переопределений будут удалены. Для знакомства используйте отдельный тестовый источник.

1. Начать снимок и сохранить id из ответа
curl --fail-with-body --silent --show-error "$CONSUL_API_BASE/imports" \
  -H "Authorization: Bearer ${CONSUL_API_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary '{"operation_id":"412c3fc0-fbb5-4395-92b0-a4c5bbd100d1"}'

# Сохраните id из ответа в этой переменной и в журнале интеграции.
read -r -p 'id импорта: ' CONSUL_IMPORT_ID

Для нового снимка создайте новый operation_id (UUID); в примерах UUID иллюстрируют одну операцию. Сохраните ID операции и полученный id импорта в долговечном журнале интеграции. При потерянном ответе повторите начало с прежним operation_id — получите тот же импорт.

2. batch.json — пакет из одной записи
{
  "operation_id": "0197ecce-dbe2-4e1b-b63f-70321b28cd62",
  "records": [
    {
      "external_id": "delivery",
      "external_revision": 1,
      "content": {
        "kind": "qa",
        "title": "Как получить заказ?",
        "description": "Получение готовых изделий",
        "url": null,
        "tags": [
          "доставка"
        ],
        "answer": "Готовый заказ можно забрать в мастерской по предварительной договорённости."
      }
    }
  ]
}
3. Отправить пакет
curl --fail-with-body --silent --show-error "$CONSUL_API_BASE/imports/$CONSUL_IMPORT_ID/batches" \
  -H "Authorization: Bearer ${CONSUL_API_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary @batch.json

В одном пакете допускается до 500 записей. Для каждого нового пакета нужен отдельный постоянный operation_id. При временной ошибке повторите тот же пакет с тем же ID и теми же данными. Повтор ID с другим содержимым и повтор одной записи в нескольких пакетах снимка дают CONFLICT. Все записи трёх типов могут входить в один снимок.

Общий размер JSON-тела одного запроса — не больше 2 МиБ (2 097 152 байта в UTF-8). Предел 500 записей не отменяет ограничение размера: разбивайте большие снимки на меньшие пакеты и по числу записей, и по байтам. Слишком большой запрос получает HTTP 413 и не должен повторяться с тем же большим телом.

4. Опубликовать снимок после всех пакетов
curl --fail-with-body --silent --show-error "$CONSUL_API_BASE/imports/$CONSUL_IMPORT_ID/complete" \
  -H "Authorization: Bearer ${CONSUL_API_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary '{"expected_count":1}'

expected_count — общее количество записей всех пакетов, а не число пакетов. Пример выше публикует одну запись. Успех — {ok:true,count:1}; повтор complete с тем же expected_count безопасен, другое количество даёт CONFLICT. Публикация атомарно обновляет записи источника и помечает отсутствующие в полном снимке как удалённые, кроме записей с ручным переопределением. Пустой снимок с expected_count:0 удаляет все записи источника без ручных переопределений: используйте его только намеренно.

Отменить неопубликованный снимок
curl --fail-with-body --silent --show-error "$CONSUL_API_BASE/imports/$CONSUL_IMPORT_ID/abort" \
  -H "Authorization: Bearer ${CONSUL_API_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary '{}'

Повтор abort уже отменённого импорта возвращает {ok:true}; опубликованный импорт отменить нельзя. После сбоя продолжайте прежний снимок с сохранёнными ID, а при изменении исходных данных отмените его и начните новый. Состояние импортов можно увидеть в «Интеграциях» кабинета. При отзыве ключа или окончании доступа новые действия и публикация снимка запрещены.

Ошибки, повторы и ограничения

Успешные ответы — обычные JSON-объекты или массивы без общей обёртки data. Ошибка содержит объект error с code, message и retryable. Точный текст message может уточняться; интеграция должна ориентироваться на HTTP-статус и code.

Формат ошибки
{
  "error": {
    "code": "CONFLICT",
    "message": "Запись изменилась",
    "retryable": false
  }
}
  • HTTP 400, VALIDATION — неверный JSON, неизвестные поля или значения. Исправьте запрос; не повторяйте прежнее тело бесконечно.
  • HTTP 401, UNAUTHORIZED — ключ отсутствует, неверен, заменён или отозван. Проверьте секрет в окружении или создайте замену в кабинете.
  • HTTP 403, ACCESS_EXPIRED — расширенный доступ выключен или закончился. Внешний API полностью приостановлен, включая чтение и удаление.
  • HTTP 404, NOT_FOUND — запись или импорт не существует либо не принадлежит этому источнику. Чужие данные не раскрываются.
  • HTTP 409, CONFLICT — устаревшая ревизия, повтор ID с другими данными или несовместимое состояние импорта. Перечитайте состояние и исправьте причину.
  • HTTP 413, VALIDATION — JSON-тело больше 2 МиБ. Уменьшите тело или число записей в пакете; повтор прежнего большого запроса не поможет.
  • HTTP 5xx или retryable:true — временная серверная ошибка. Повторяйте с паузой и ограничением числа попыток; сохраняйте внешнюю ревизию, ID операций, пакет и expected_count.

Разрыв связи не доказывает, что операция не выполнилась. Повторяйте прежнюю логическую операцию, сохраняя её идентификаторы. Не создавайте новые ID после каждого сетевого сбоя. Храните журнал интеграции отдельно от ключа и исключайте заголовок Authorization из журналов запросов.

После окончания доступа справочники сохраняются. Их просмотр и удаление остаются доступны через кабинет и мобильное приложение. Продление доступа выполняется администратором; внешний API снова использует действующий ключ. В проекте нет платёжных тарифов и пользовательских квот токенов.

Все инструкции

Начните с одного разговора.

Подключите Consul к своему сайту и отвечайте так, как удобно вам.

Подключить сайт
Демонстрация виджета