API CalcRest для интеграций

Внешняя система по разрешению владельца читает ингредиенты, рецептуры, полуфабрикаты, граммовки, потери и КБЖУ. Цены и себестоимость — по отдельному праву. API только читает: записи в рецептуры и справочник нет ни в одном методе.

Рабочий API подключается на тарифах Про и Шеф. Песочница с демо-данными открыта всем без регистрации: интеграцию можно собрать и проверить до подключения клиента.

Песочница: попробуйте без ключа клиента

Песочница отвечает теми же методами, кодами ошибок и форматом, что и рабочий API. Только данные в ней не клиентские, а демо-кафе. Ключ публичный и один на всех.

Адресhttps://calcrest.ru/api/public/sandbox/v1
Ключcr_test_sandbox с правами на каталог и цены

Проверьте подключение:

CALCREST_API_URL='https://calcrest.ru/api/public/sandbox/v1'
CALCREST_API_KEY='cr_test_sandbox'

curl -sS "$CALCREST_API_URL/connection" \
  -H "Authorization: Bearer $CALCREST_API_KEY"

В ответе придёт демо-кафе «Песочница CalcRest». С этими двумя переменными все команды быстрого старта ниже работают без правок: выгрузка, статус, скачивание, список рецептур и карточка.

Что лежит в демо-кафе

  • Полуфабрикат «Тесто для блинов» и блюдо «Блины с ветчиной и сыром», где тесто стоит строкой-ПФ. На нём удобно проверить порядок импорта и цену ПФ.
  • Салат «Цезарь» с курицей: потери при холодной и тепловой обработке, разделы состава и строка соуса без связи со справочником (inline).
  • Сырники со сметаной в статусе trial.
  • 11 позиций своего справочника с ценами и 3 позиции общего справочника (master:201, master:202, master:204) без цены и с предупреждением price_missing.

Идентификаторы постоянные, их можно вшивать в автотесты. Например, блины: 5a0d2000-0000-4000-8000-000000000002, тесто: 5a0d2000-0000-4000-8000-000000000001.

Чем песочница отличается

  • Данные фиксированные и одинаковые для всех.
  • Выгрузка собирается примерно за 1,5 секунды и хранится 24 часа. После обновления CalcRest она может пропасть раньше: тогда создайте новую.
  • Квот на выгрузки нет. Частота запросов ограничена по IP-адресу.
  • Каждый ответ несёт заголовок X-CalcRest-Environment: sandbox. Рабочий ключ cr_live_… в песочнице не принимается, а тестовый не работает в рабочем API: придёт 401 с подсказкой.

Для перехода на данные клиента замените адрес на https://calcrest.ru/api/public/v1, а ключ на ключ клиента. Код интеграции менять не нужно.

Быстрый старт: получите всю базу

На выходе вы получите один JSON-файл: рецептуры блюд и полуфабрикатов, их состав, граммовки, потери, КБЖУ и справочник ингредиентов. Цены и себестоимость можно включить отдельно.

Создайте ключ в Профиле → Интеграции и API и подставьте его в переменную ниже. Примеры команд рассчитаны на Bash или Zsh. Для пробы на демо-данных возьмите адрес и ключ песочницы.

CALCREST_API_URL='https://calcrest.ru/api/public/v1'
CALCREST_API_KEY='ваш_API_ключ'

1. Подготовьте выгрузку

Этот запрос запускает сборку файла со всей базой. CalcRest собирает связанные рецептуры и ингредиенты в одном состоянии: если во время скачивания кто-то изменит карточку, данные в файле останутся согласованными. В API такая выгрузка называется «снимок» базы.

REQUEST_ID=$(uuidgen)

curl -sS -X POST "$CALCREST_API_URL/exports" \
  -H "Authorization: Bearer $CALCREST_API_KEY" \
  -H "Idempotency-Key: $REQUEST_ID" \
  -H "Content-Type: application/json" \
  -d '{"selection":{"mode":"all"},"includePricing":false}'

mode: "all" означает всю базу этого рабочего пространства. Предварительно получать список рецептур не нужно. Для выгрузки с ценами замените includePricing: false на true; у ключа должен быть включён доступ к ценам и себестоимости.

REQUEST_ID защищает от повторного создания одной выгрузки. При сетевом сбое повторите curl с тем же значением. Для новой выгрузки выполните REQUEST_ID=$(uuidgen) заново.

В ответе: номер выгрузки и её статус

{
  "data": {
    "id": "60000000-0000-4000-8000-000000000001",
    "status": "queued",
    "statusUrl": "/api/public/v1/exports/60000000-0000-4000-8000-000000000001"
  }
}

HTTP 202 означает, что запрос принят. Самого файла в этом ответе ещё нет. Сохраните data.id из вашего ответа как EXPORT_ID для следующих двух шагов.

2. Проверьте, готов ли файл

EXPORT_ID='вставьте_data.id_из_ответа'

curl -sS "$CALCREST_API_URL/exports/$EXPORT_ID" \
  -H "Authorization: Bearer $CALCREST_API_KEY"

Первый раз проверьте через 2 секунды. Если файл ещё готовится, повторяйте этот GET-запрос каждые 5 секунд. Если сервер вернул заголовок Retry-After, подождите не меньше указанного в нём числа секунд.

Сколько ждать? Время зависит от объёма базы и очереди. Фиксированного срока и оценки оставшегося времени API пока не возвращает. Две секунды — время до первой проверки, а не обещание готовности. Переходите к скачиванию, когда data.status станет ready.

  • queued — выгрузка ожидает своей очереди.
  • processing — CalcRest собирает файл.
  • ready — файл готов; в data.contentUrl появится адрес скачивания.
  • failed — собрать файл не удалось; причина в data.problem. Остановите проверку и разберите ошибку.
  • expired — срок хранения закончился; создайте новую выгрузку.

Этот запрос возвращает статус, даже если сборка завершилась ошибкой. Проверяйте data.status: одного HTTP 200 недостаточно. Повторно запускать шаг 1 во время ожидания не нужно.

3. Скачайте готовый JSON-файл

curl -fS --compressed "$CALCREST_API_URL/exports/$EXPORT_ID/content" \
  -H "Authorization: Bearer $CALCREST_API_KEY" \
  -o calcrest-export.json

Команда сохранит файл calcrest-export.json в текущую папку. Флаг --compressed распакует ответ сервера автоматически. В файле:

  • data.recipes — рецептуры блюд и полуфабрикатов с составом и расчётами.
  • data.ingredients — пользовательский справочник и используемые позиции общего справочника.
  • meta.capturedAt — момент, на который зафиксированы данные.
  • warnings — замечания к исходным данным, например незаполненные значения.

Продукты без связи со справочником остаются внутри состава рецептуры с пометкой reference.type: "inline". При переносе их тоже нужно учитывать.

Готовый файл хранится 24 часа с момента готовности. Его можно скачать повторно тем же действующим ключом. Позже потребуется новая выгрузка; точный срок указан в data.expiresAt ответа на шаге 2.

Если нужны отдельные рецептуры

Список пригодится, чтобы показать пользователю его блюда и дать выбрать, какие перенести. Для выгрузки всей базы достаточно трёх шагов выше.

Получите список для выбора

curl -sS "$CALCREST_API_URL/recipes?limit=100" \
  -H "Authorization: Bearer $CALCREST_API_KEY"

В data придёт до 100 рецептур с ID, названием, статусом, категорией и числом строк состава (componentsCount). Состав, технология, КБЖУ и себестоимость в список не входят.Их можно получить для одной карточки следующим запросом или в JSON-выгрузке.

Если рецептур больше 100

API отдаёт длинный список частями. Например, 230 рецептур придут за три запроса: 100, ещё 100 и последние 30. В каждом ответе есть блок page:

{
  "page": {
    "hasMore": true,
    "nextCursor": "строка_для_получения_следующей_части"
  }
}

hasMore: true означает, что есть продолжение. Скопируйте значение page.nextCursor целиком и передайте его в следующий запрос. Это готовая отметка «продолжить отсюда», её не нужно расшифровывать или составлять самостоятельно.

CURSOR='вставьте_page.nextCursor_из_последнего_ответа'

curl -sS --get "$CALCREST_API_URL/recipes" \
  -H "Authorization: Bearer $CALCREST_API_KEY" \
  --data-urlencode "limit=100" \
  --data-urlencode "cursor=$CURSOR"

Добавляйте data из каждого ответа к общему списку. Когда hasMore станет false, а nextCursor — null, вы получили всё. Если рецептур 100 или меньше, это произойдёт уже в первом ответе.

Каждая отметка продолжения действует 30 минут. Если она истекла, начните чтение списка с первого запроса. Используйте тот же API-ключ и те же фильтры: например, если искали q=суп, передавайте этот поиск и в следующих запросах. При смене фильтров начните новый список без cursor.

Получите одну карточку целиком

Возьмите id нужной рецептуры из списка:

RECIPE_ID='вставьте_id_рецептуры'

curl -sS "$CALCREST_API_URL/recipes/$RECIPE_ID" \
  -H "Authorization: Bearer $CALCREST_API_KEY"

В data придёт карточка с составом, граммовками, потерями, технологией и КБЖУ. Для цен добавьте к адресу ?includePricing=true и используйте ключ с доступом к ценам. Если в составе есть полуфабрикат, здесь будет ссылка на его ID; для переноса блюда со всеми вложенными полуфабрикатами используйте выгрузку.

Выгрузите только выбранные карточки

Выполните шаг 1 быстрого старта с новым REQUEST_ID, заменив тело запроса после -d на такое:

{
  "selection": {
    "mode": "ids",
    "recipeIds": ["ID_первой_рецептуры", "ID_второй_рецептуры"]
  },
  "includePricing": false
}

Подставьте реальные ID из списка. CalcRest добавит используемые полуфабрикаты, в том числе вложенные, и связанные ингредиенты автоматически. Затем проверьте готовность и скачайте файл по шагам 2 и 3.

Что означают поля

ПолеСмысл
basisВсегда одна сохранённая расчётная норма. Состав и себестоимость уже приведены к ней.
sourceServingsЧисло порций исходной карточки. Не множитель: умножать на него граммовки и стоимость нельзя, они уже приведены к норме.
gross → afterCold → outputЗакладка, масса после холодной обработки, масса после тепловой. Проценты применяются последовательно. Тепловые потери могут быть отрицательными: −150 % означает рост массы.
outputSourcemanual означает, что выход задан руками и заменяет расчётный; сохранённые проценты при этом остаются в ответе.
reference.typeingredient — позиция справочника (UUID или виртуальный master:<id>); recipe — полуфабрикат; inline — продукт без связи, требует сопоставления у вас.
stateПолнота исходных данных для формулы. При partial известная часть стоимости лежит в knownSubtotal, а perRecipe равен null: неполная сумма не выдаётся как полная.
nullЗначение неизвестно. Ноль вместо неизвестности не подставляется никогда — ни для цены, ни для массы, ни для КБЖУ.

Как импортировать корректно

  1. Разберите data.ingredients на пользовательские позиции (origin=user) и виртуальные позиции общего справочника (origin=referenced_master).
  2. Покажите пользователю сопоставление inline-строк со своей номенклатурой. Объединять их по названию нельзя: одинаковые имена у разных поставщиков — обычное дело.
  3. Если у рецептуры заполнено pairing, это техкарта и калькуляционная карта одного блюда двумя записями. Попросите пользователя выбрать вариант; автоматический импорт обоих создаёт дубль.
  4. Сначала создайте простые полуфабрикаты, затем те, которые их используют, и после них блюда. Стоимость строки-полуфабриката уже учтена один раз — прибавлять к ней его сырьё нельзя.
  5. Сохраните соответствие (workspaceId, entityType, id) → ваш ID. Повторная выгрузка обновляет связанные объекты и не создаёт дублей.
  6. Не удаляйте ничего автоматически. Отсутствие объекта в выборочной выгрузке, 404, сбой или окончание тарифа — не сигнал к удалению.

Проверяйте meta.catalogCoverage. Поле meta.ingredientSetComplete относится к справочнику, но не означает отсутствия inline-строк: считать data.ingredients полным перечнем сырья без этой проверки нельзя.

Ошибки и повторы

Ошибки приходят в формате application/problem+json (RFC 9457). Ветвитесь по полю code, а не по тексту. В каждом ответе есть requestId — приложите его к обращению в поддержку.

{
  "type": "https://calcrest.ru/developers/api/errors#insufficient_scope",
  "title": "API key lacks the required scope",
  "status": 403,
  "detail": "API key is missing the required scope: pricing:read.",
  "instance": "/api/public/v1/ingredients",
  "code": "insufficient_scope",
  "requestId": "0b7c…"
}

Повторяйте сетевые ошибки, 429 и 503 с экспоненциальной задержкой и джиттером, соблюдая Retry-After. Ответы 400, 401, 403, 404 и 422 бесконечно повторять бессмысленно — они не изменятся сами.

При повторе POST /exports после разрыва связи передавайте прежний Idempotency-Key: вы получите то же задание, а квота не спишется дважды. Новый UUID нужен для новой выгрузки.

Ограничения

  • 60 запросов в минуту на ключ и 120 на рабочее пространство.
  • 6 заданий экспорта в час и 60 в сутки на рабочее пространство.
  • До 500 явно выбранных рецептур в mode=ids.
  • Тело запроса до 256 KiB, файл выгрузки до 25 MiB до сжатия.
  • Глубина вложенности полуфабрикатов до 20 уровней.

Чего API не делает

  • Отдаёт текущую сохранённую рецептуру, а не содержание последней утверждённой ТТК по ГОСТ. Статус approved — состояние карточки, не электронная подпись.
  • Не формирует PDF и XLSX: только структурированный JSON.
  • Не пишет данные и не является двусторонней синхронизацией.
  • Список рецептур содержит краткие сведения для выбора карточек. Для переноса базы используйте JSON-выгрузку: рецептуры, полуфабрикаты и ингредиенты в ней зафиксированы на один момент времени.

Вопросы по интеграции — в поддержку CalcRest с указанием requestId из ответа.