{"openapi":"3.1.0","info":{"title":"CalcRest Public Export API","version":"1.0.0-v1","description":"Чтение ингредиентов, рецептур, полуфабрикатов, граммовок, потерь и КБЖУ рабочего пространства CalcRest по ключу владельца; цены и себестоимость — по отдельному праву.\n\nAPI только читает. Записи в рецептуры и справочник нет ни в одном методе.\n\nПрофиль расчёта: `gost-recipe-v1`. Он фиксирует, какие сохранённые поля считаются источником истины для выхода и потерь. Значения существующих карточек экспорт не пересчитывает и не исправляет: спорные места помечаются предупреждениями.","contact":{"name":"Техническая поддержка CalcRest","url":"https://calcrest.ru/developers"}},"servers":[{"url":"https://calcrest.ru/api/public/v1","description":"Рабочий API: данные клиента по его ключу"},{"url":"https://calcrest.ru/api/public/sandbox/v1","description":"Песочница: демо-кафе, публичный ключ cr_test_sandbox"}],"security":[{"apiKey":[]}],"tags":[{"name":"connection","description":"Проверка подключения"},{"name":"catalog","description":"Живые списки для выбора позиций"},{"name":"exports","description":"Согласованные снимки для переноса"}],"paths":{"/connection":{"get":{"tags":["connection"],"operationId":"getConnection","summary":"Проверка ключа, рабочего пространства и действующих лимитов","responses":{"200":{"description":"Подключение действует.","content":{"application/json":{"schema":{"type":"object","required":["apiVersion","workspace","key","limits"],"properties":{"apiVersion":{"type":"string"},"workspace":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"description":"Только отображаемое название. Email, телефон и биллинг не передаются."},"key":{"type":"object"},"limits":{"type":"object"}}}}}},"400":{"description":"Неверный запрос. Коды: validation_error, invalid_cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Ключ отсутствует, отозван или истёк. Коды: invalid_api_key, api_key_expired, api_key_revoked.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Недостаточно прав или доступ по тарифу закончился. Коды: insufficient_scope, api_access_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Объект не найден. Для чужого и несуществующего ответ одинаковый. Коды: not_found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Превышен лимит. Учитывайте заголовок Retry-After. Коды: rate_limited, export_quota_exceeded, export_queue_full.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Сервис временно недоступен. Коды: temporarily_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/ingredients":{"get":{"tags":["catalog"],"operationId":"listIngredients","summary":"Пользовательский справочник и используемые позиции общего каталога","description":"Возвращает пользовательские позиции и виртуальные позиции общего справочника, на которые ссылаются строки этого рабочего пространства. Весь общий каталог CalcRest через этот метод не выгружается.\n\nПорядок обхода: сначала origin=user (по id), затем origin=referenced_master (по числовому идентификатору).","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Непрозрачный подписанный курсор предыдущей страницы. Действует 30 минут и только для тех же фильтров и того же ключа; иначе 400 invalid_cursor."},{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":200}},{"name":"includePricing","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Требует scope pricing:read. Запрос цен без него даёт 403 insufficient_scope: молча урезать ответ нельзя, иначе отсутствие цен неотличимо от их отсутствия у клиента."}],"responses":{"200":{"description":"Страница справочника.","content":{"application/json":{"schema":{"type":"object","required":["data","page"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Ingredient"}},"page":{"$ref":"#/components/schemas/Page"}}}}}},"400":{"description":"Неверный запрос. Коды: validation_error, invalid_cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Ключ отсутствует, отозван или истёк. Коды: invalid_api_key, api_key_expired, api_key_revoked.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Недостаточно прав или доступ по тарифу закончился. Коды: insufficient_scope, api_access_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Объект не найден. Для чужого и несуществующего ответ одинаковый. Коды: not_found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Превышен лимит. Учитывайте заголовок Retry-After. Коды: rate_limited, export_quota_exceeded, export_queue_full.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Сервис временно недоступен. Коды: temporarily_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/ingredients/{id}":{"get":{"tags":["catalog"],"operationId":"getIngredient","summary":"Один ингредиент","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"UUID пользовательской позиции либо `master:<id>`. Виртуальная позиция доступна только если рабочее пространство действительно на неё ссылается."},{"name":"includePricing","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Требует scope pricing:read. Запрос цен без него даёт 403 insufficient_scope: молча урезать ответ нельзя, иначе отсутствие цен неотличимо от их отсутствия у клиента."}],"responses":{"200":{"description":"Ингредиент.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Ingredient"}}}}}},"400":{"description":"Неверный запрос. Коды: validation_error, invalid_cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Ключ отсутствует, отозван или истёк. Коды: invalid_api_key, api_key_expired, api_key_revoked.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Недостаточно прав или доступ по тарифу закончился. Коды: insufficient_scope, api_access_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Объект не найден. Для чужого и несуществующего ответ одинаковый. Коды: not_found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Превышен лимит. Учитывайте заголовок Retry-After. Коды: rate_limited, export_quota_exceeded, export_queue_full.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Сервис временно недоступен. Коды: temporarily_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/recipes":{"get":{"tags":["catalog"],"operationId":"listRecipes","summary":"Список рецептур без состава","description":"Живой список для выбора позиций, а не согласованная синхронизация: при параллельных изменениях состав страниц может сдвинуться. Для полного переноса используйте снимок.\n\nБез фильтров возвращаются все состояния, включая архив и черновики — скрывать карты, которыми пользуется клиент, нельзя.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Непрозрачный подписанный курсор предыдущей страницы. Действует 30 минут и только для тех же фильтров и того же ключа; иначе 400 invalid_cursor."},{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":200}},{"name":"kind","in":"query","required":false,"schema":{"type":"string","enum":["dish","semi_finished"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["draft","trial","approved","archive"]}}],"responses":{"200":{"description":"Страница списка.","content":{"application/json":{"schema":{"type":"object","required":["data","page"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RecipeListItem"}},"page":{"$ref":"#/components/schemas/Page"}}}}}},"400":{"description":"Неверный запрос. Коды: validation_error, invalid_cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Ключ отсутствует, отозван или истёк. Коды: invalid_api_key, api_key_expired, api_key_revoked.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Недостаточно прав или доступ по тарифу закончился. Коды: insufficient_scope, api_access_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Объект не найден. Для чужого и несуществующего ответ одинаковый. Коды: not_found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Превышен лимит. Учитывайте заголовок Retry-After. Коды: rate_limited, export_quota_exceeded, export_queue_full.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Сервис временно недоступен. Коды: temporarily_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/recipes/{id}":{"get":{"tags":["catalog"],"operationId":"getRecipe","summary":"Рецептура со снимками строк состава","description":"Содержит ссылки на полуфабрикаты и полные снимки строк, но не разворачивает граф вложенных карт внутрь ответа. Для замкнутого графа используйте /exports.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"includePricing","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Требует scope pricing:read. Запрос цен без него даёт 403 insufficient_scope: молча урезать ответ нельзя, иначе отсутствие цен неотличимо от их отсутствия у клиента."}],"responses":{"200":{"description":"Рецептура.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Recipe"}}}}}},"400":{"description":"Неверный запрос. Коды: validation_error, invalid_cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Ключ отсутствует, отозван или истёк. Коды: invalid_api_key, api_key_expired, api_key_revoked.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Недостаточно прав или доступ по тарифу закончился. Коды: insufficient_scope, api_access_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Объект не найден. Для чужого и несуществующего ответ одинаковый. Коды: not_found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Превышен лимит. Учитывайте заголовок Retry-After. Коды: rate_limited, export_quota_exceeded, export_queue_full.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Сервис временно недоступен. Коды: temporarily_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/exports":{"post":{"tags":["exports"],"operationId":"createExport","summary":"Создать неизменяемый снимок","description":"Бизнес-данные не изменяются. Задание выполняется асинхронно; опрашивайте статус с учётом Retry-After: 2 секунды, затем 5–10.\n\nПовтор с тем же Idempotency-Key и тем же телом возвращает то же задание и не расходует квоту. Тот же ключ с другим телом даёт 409 idempotency_conflict.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","format":"uuid"},"description":"Новый UUID для новой выгрузки, прежний — для повтора после сетевой ошибки. Соответствие хранится 30 дней."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["selection"],"properties":{"selection":{"type":"object","required":["mode"],"properties":{"mode":{"type":"string","enum":["all","ids"]},"recipeIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500,"description":"Обязателен и непуст при mode=ids; при mode=all не принимается."}}},"includePricing":{"type":"boolean","default":false}}},"examples":{"selected":{"summary":"Выбранные рецептуры с ценами","value":{"selection":{"mode":"ids","recipeIds":["20000000-0000-4000-8000-000000000001"]},"includePricing":true}},"all":{"summary":"Вся база","value":{"selection":{"mode":"all"}}}}}}},"responses":{"200":{"description":"Повтор для уже завершённого задания."},"202":{"description":"Задание принято.","headers":{"Location":{"schema":{"type":"string"}},"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Неверный запрос. Коды: validation_error, invalid_cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Ключ отсутствует, отозван или истёк. Коды: invalid_api_key, api_key_expired, api_key_revoked.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Недостаточно прав или доступ по тарифу закончился. Коды: insufficient_scope, api_access_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Объект не найден. Для чужого и несуществующего ответ одинаковый. Коды: not_found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Ключ идемпотентности переиспользован с другим телом. Коды: idempotency_conflict.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"410":{"description":"Снимок для этого ключа истёк; нужен новый UUID. Коды: export_expired.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"Тело больше 256 KiB. Коды: request_too_large.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Выборка заведомо превышает лимит. Коды: selection_too_large.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Превышен лимит. Учитывайте заголовок Retry-After. Коды: rate_limited, export_quota_exceeded, export_queue_full.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Сервис временно недоступен. Коды: temporarily_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/exports/{id}":{"get":{"tags":["exports"],"operationId":"getExport","summary":"Статус задания","description":"Отвечает 200 и для упавшего задания: сам статус прочитан успешно, а причина лежит в поле problem. Доступно только создавшему задание ключу.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Статус.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ExportStatus"}}}}}},"400":{"description":"Неверный запрос. Коды: validation_error, invalid_cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Ключ отсутствует, отозван или истёк. Коды: invalid_api_key, api_key_expired, api_key_revoked.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Недостаточно прав или доступ по тарифу закончился. Коды: insufficient_scope, api_access_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Объект не найден. Для чужого и несуществующего ответ одинаковый. Коды: not_found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Превышен лимит. Учитывайте заголовок Retry-After. Коды: rate_limited, export_quota_exceeded, export_queue_full.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Сервис временно недоступен. Коды: temporarily_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/exports/{id}/content":{"get":{"tags":["exports"],"operationId":"downloadExport","summary":"Скачать содержимое снимка","description":"Файл доступен 24 часа после готовности и только создавшему его ключу. Долговечная публичная ссылка хранилища не выдаётся: отзыв ключа обязан закрывать доступ.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"JSON-снимок, сжатый gzip.","headers":{"Content-Encoding":{"schema":{"type":"string","enum":["gzip"]}},"X-Data-Hash":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportSnapshot"}}}},"400":{"description":"Неверный запрос. Коды: validation_error, invalid_cursor.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Ключ отсутствует, отозван или истёк. Коды: invalid_api_key, api_key_expired, api_key_revoked.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Недостаточно прав или доступ по тарифу закончился. Коды: insufficient_scope, api_access_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Объект не найден. Для чужого и несуществующего ответ одинаковый. Коды: not_found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Снимок ещё не готов или задание упало. Коды: export_not_ready, export_failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"410":{"description":"Срок хранения содержимого истёк. Коды: export_expired.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Превышен лимит. Учитывайте заголовок Retry-After. Коды: rate_limited, export_quota_exceeded, export_queue_full.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Сервис временно недоступен. Коды: temporarily_unavailable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Ключ вида `cr_live_<id>_<secret>` в заголовке Authorization. В URL, query и cookie запрещён. Хранится на сервере партнёра, не в клиентском приложении."}},"schemas":{"Quantity":{"type":"object","required":["value","unit"],"properties":{"value":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},"unit":{"type":"string","enum":["g","ml","pcs"]}},"description":"Нормализованное количество. Отсутствует целиком (null), если исходная единица не имеет подтверждённого значения — нуля вместо неизвестности не бывает."},"SourceQuantity":{"type":"object","required":["value","unit"],"properties":{"value":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},"unit":{"type":"string"}},"description":"Исходные число и обозначение единицы, как их сохранил пользователь."},"Money":{"type":"object","required":["amount","currency"],"properties":{"amount":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},"currency":{"type":"string","enum":["RUB"]}}},"Nutrition":{"type":"object","required":["caloriesKcal","proteinsG","fatsG","carbsG"],"properties":{"caloriesKcal":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]},"proteinsG":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]},"fatsG":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]},"carbsG":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]}}},"DefaultLosses":{"type":"object","required":["coldPercent","boilPercent","fryPercent","stewPercent","bakePercent"],"properties":{"coldPercent":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]},"boilPercent":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]},"fryPercent":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]},"stewPercent":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]},"bakePercent":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]}}},"PricingBlock":{"type":"object","required":["packageQuantity","sourcePackageQuantity","packagePrice","priceUpdatedAt","priceDateSource"],"properties":{"packageQuantity":{"oneOf":[{"$ref":"#/components/schemas/Quantity"},{"type":"null"}]},"sourcePackageQuantity":{"oneOf":[{"$ref":"#/components/schemas/SourceQuantity"},{"type":"null"}]},"packagePrice":{"oneOf":[{"$ref":"#/components/schemas/Money"},{"type":"null"}]},"priceUpdatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"priceDateSource":{"type":"string","enum":["history","catalog_record","unknown"],"description":"history — есть запись истории ровно для этой цены и фасовки; catalog_record — только дата карточки справочника, она говорит о моменте учёта в CalcRest и не подтверждает согласование с поставщиком; unknown — источник не доказан."}}},"Warning":{"type":"object","required":["code","entityType","entityId","componentId","field","message"],"properties":{"code":{"type":"string","enum":["unit_ambiguous","legacy_loss_fallback","calculation_ambiguous","nutrition_unverified","price_date_unverified","price_missing","yield_unknown","empty_recipe","archived_dependency","paired_recipe_conflict","malformed_row_metadata","duplicate_sort_order","invalid_loss_value","image_unavailable","semi_finished_profile_divergence","semi_finished_yield_unknown","inline_requires_mapping"]},"entityType":{"oneOf":[{"type":"string","enum":["ingredient","recipe","component","export"]},{"type":"null"}]},"entityId":{"oneOf":[{"type":"string"},{"type":"null"}]},"componentId":{"oneOf":[{"type":"string"},{"type":"null"}]},"field":{"oneOf":[{"type":"string"},{"type":"null"}]},"message":{"type":"string","description":"Текст для человека. Машина ветвится по code."}}},"Ingredient":{"type":"object","required":["id","origin","name","updatedAt","category","unit","sourceUnit","nutritionPer100g","defaultLosses","warnings"],"properties":{"id":{"type":"string","description":"UUID пользовательской позиции либо виртуальный идентификатор вида `master:<id>` для позиции общего справочника, на которую ссылается это рабочее пространство."},"origin":{"type":"string","enum":["user","referenced_master"]},"name":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"category":{"oneOf":[{"type":"string"},{"type":"null"}]},"unit":{"oneOf":[{"type":"string","enum":["g","ml","pcs"]},{"type":"null"}]},"sourceUnit":{"type":"string"},"nutritionPer100g":{"$ref":"#/components/schemas/Nutrition"},"defaultLosses":{"$ref":"#/components/schemas/DefaultLosses"},"pricing":{"$ref":"#/components/schemas/PricingBlock","description":"Присутствует только при scope pricing:read и includePricing=true."},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/Warning"}}}},"Component":{"type":"object","required":["id","sortOrder","section","reference","name","sourceUnit","sourceQuantity","gross","afterCold","output","processing","losses","outputSource","nutritionPer100g","warnings"],"properties":{"id":{"type":"string","description":"Стабилен только в пределах версии рецептуры: сохранение карточки пересоздаёт строки состава. Соответствие продуктов хранится по reference, не по этому id."},"sortOrder":{"type":"integer"},"section":{"oneOf":[{"type":"string"},{"type":"null"}]},"reference":{"type":"object","required":["type","id"],"properties":{"type":{"type":"string","enum":["ingredient","recipe","inline"]},"id":{"oneOf":[{"type":"string"},{"type":"null"}]}},"description":"У inline id всегда null: такой строке не соответствует никакая сущность."},"name":{"type":"string"},"sourceUnit":{"type":"string"},"sourceQuantity":{"oneOf":[{"$ref":"#/components/schemas/SourceQuantity"},{"type":"null"}]},"gross":{"oneOf":[{"$ref":"#/components/schemas/Quantity"},{"type":"null"}]},"afterCold":{"oneOf":[{"$ref":"#/components/schemas/Quantity"},{"type":"null"}]},"output":{"oneOf":[{"$ref":"#/components/schemas/Quantity"},{"type":"null"}]},"processing":{"type":"string","enum":["none","boil","fry","stew","bake"]},"losses":{"type":"object","required":["coldPercent","heatPercent","mode"],"properties":{"coldPercent":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]},"heatPercent":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}],"description":"Может быть отрицательным: −150 % означает рост массы, 50 г дают 125 г выхода."},"mode":{"type":"string","enum":["snapshot","linked","unknown"]}}},"outputSource":{"type":"string","enum":["formula","manual","unknown"]},"nutritionPer100g":{"$ref":"#/components/schemas/Nutrition"},"pricing":{"allOf":[{"$ref":"#/components/schemas/PricingBlock"},{"type":"object","properties":{"mode":{"type":"string","enum":["snapshot","linked","manual","unknown"]},"source":{"type":"string","enum":["row","ingredient","recipe","unknown"]}}}]},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/Warning"}}}},"RecipePairing":{"type":"object","required":["recipeId","role","state"],"properties":{"recipeId":{"oneOf":[{"type":"string"},{"type":"null"}]},"role":{"oneOf":[{"type":"string","enum":["tech","calc"]},{"type":"null"}]},"state":{"type":"string","enum":["reciprocal","one_sided","conflict"]}},"description":"Связь техкарты и калькуляционной карты, когда это две отдельные записи. Сервер их не сливает: составы и цены могут различаться, вариант выбирает пользователь в предпросмотре."},"RecipeHead":{"type":"object","required":["id","pairing","name","updatedAt","kind","type","status","number","category","collections","basis","sourceServings","calculationProfile","yield","image","warnings"],"properties":{"id":{"type":"string"},"pairing":{"oneOf":[{"$ref":"#/components/schemas/RecipePairing"},{"type":"null"}]},"name":{"type":"string"},"updatedAt":{"type":"string","format":"date-time","description":"Не годится как основа инкрементальной синхронизации: изменение ингредиента или вложенного полуфабриката меняет результат, не трогая эту дату."},"kind":{"type":"string","enum":["dish","semi_finished"]},"type":{"type":"string","enum":["dish","cake","cocktail","coffee","other"]},"status":{"type":"string","enum":["draft","trial","approved","archive"]},"number":{"oneOf":[{"type":"string"},{"type":"null"}]},"category":{"oneOf":[{"type":"string"},{"type":"null"}]},"collections":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}}},"basis":{"type":"object","required":["quantity","unit"],"properties":{"quantity":{"type":"string","enum":["1"]},"unit":{"type":"string","enum":["recipe"]}},"description":"Одна сохранённая расчётная норма. Для полуфабриката это может быть партия, а не порция."},"sourceServings":{"type":"integer","description":"Информационное поле. НЕ множитель: состав и себестоимость уже приведены к одной сохранённой норме, умножать их на это число нельзя."},"calculationProfile":{"type":"string","enum":["gost-recipe-v1"]},"yield":{"type":"object","required":["quantity","source"],"properties":{"quantity":{"oneOf":[{"$ref":"#/components/schemas/Quantity"},{"type":"null"}]},"source":{"type":"string","enum":["saved","calculated","unknown"]}}},"image":{"oneOf":[{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}},{"type":"null"}]},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/Warning"}}}},"Recipe":{"allOf":[{"$ref":"#/components/schemas/RecipeHead"},{"type":"object","required":["technology","presentation","storage","components","nutrition"],"properties":{"technology":{"oneOf":[{"type":"string"},{"type":"null"}]},"presentation":{"oneOf":[{"type":"string"},{"type":"null"}]},"storage":{"oneOf":[{"type":"string"},{"type":"null"}]},"components":{"type":"array","items":{"$ref":"#/components/schemas/Component"}},"nutrition":{"type":"object","required":["state","total","per100g"],"properties":{"state":{"$ref":"#/components/schemas/CalculationState"},"total":{"oneOf":[{"$ref":"#/components/schemas/Nutrition"},{"type":"null"}]},"per100g":{"oneOf":[{"$ref":"#/components/schemas/Nutrition"},{"type":"null"}]}}},"pricing":{"type":"object","required":["state","perRecipe","perKg","knownSubtotal","pricedRows","totalRows","salePrice","markupPercent"],"properties":{"state":{"$ref":"#/components/schemas/CalculationState"},"perRecipe":{"oneOf":[{"$ref":"#/components/schemas/Money"},{"type":"null"}],"description":"Полная стоимость нормы. null при неполной цене состава — известная часть лежит в knownSubtotal и полной суммой не притворяется."},"perKg":{"oneOf":[{"$ref":"#/components/schemas/Money"},{"type":"null"}]},"knownSubtotal":{"oneOf":[{"$ref":"#/components/schemas/Money"},{"type":"null"}]},"pricedRows":{"type":"integer"},"totalRows":{"type":"integer"},"salePrice":{"oneOf":[{"$ref":"#/components/schemas/Money"},{"type":"null"}]},"markupPercent":{"oneOf":[{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"Десятичное число строкой: точка, без пробелов и знака валюты."},{"type":"null"}]}}}}}]},"CalculationState":{"type":"string","enum":["complete","partial","missing","unsupported"],"description":"complete означает полноту исходных данных для формулы, а не лабораторную или юридическую достоверность."},"RecipeListItem":{"allOf":[{"$ref":"#/components/schemas/RecipeHead"},{"type":"object","required":["componentsCount"],"properties":{"componentsCount":{"type":"integer"}}}],"description":"Строка списка: без components, technology, presentation, storage, nutrition и pricing. Список не считает стоимость всего графа, includePricing здесь не поддерживается."},"Page":{"type":"object","required":["nextCursor","hasMore"],"properties":{"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}]},"hasMore":{"type":"boolean"}}},"Problem":{"type":"object","required":["type","title","status","detail","instance","code","requestId"],"properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"code":{"type":"string"},"requestId":{"type":"string"},"errors":{"type":"array","items":{"type":"object","required":["field","code","message"],"properties":{"field":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"}}}},"retryAfter":{"type":"integer"}}},"ExportStatus":{"type":"object","required":["id","status","createdAt","capturedAt","completedAt","expiresAt","counts","contentBytes","dataHash","contentUrl","problem"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","ready","failed","expired"]},"createdAt":{"type":"string","format":"date-time"},"capturedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"completedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"expiresAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"counts":{"type":"object","properties":{"recipes":{"oneOf":[{"type":"integer"},{"type":"null"}]},"ingredients":{"oneOf":[{"type":"integer"},{"type":"null"}]},"components":{"oneOf":[{"type":"integer"},{"type":"null"}]}}},"contentBytes":{"oneOf":[{"type":"integer"},{"type":"null"}]},"dataHash":{"oneOf":[{"type":"string"},{"type":"null"}]},"contentUrl":{"oneOf":[{"type":"string"},{"type":"null"}]},"problem":{"oneOf":[{"type":"object","properties":{"code":{"type":"string","enum":["dependency_cycle","missing_reference","reference_conflict","export_too_large","snapshot_timeout","export_timeout"]},"details":{}}},{"type":"null"}]}}},"ExportSnapshot":{"type":"object","required":["meta","data","warnings"],"properties":{"meta":{"type":"object","required":["schemaVersion","exportId","workspaceId","capturedAt","selection","rootRecipeIds","includePricing","recipeSetComplete","ingredientSetComplete","counts","catalogCoverage","dataHash"],"properties":{"schemaVersion":{"type":"string","enum":["1.0"]},"exportId":{"type":"string"},"workspaceId":{"type":"string"},"capturedAt":{"type":"string","format":"date-time"},"selection":{"type":"object"},"rootRecipeIds":{"type":"array","items":{"type":"string"}},"includePricing":{"type":"boolean"},"recipeSetComplete":{"type":"boolean","description":"Истинно только у успешного mode=all без урезания. Не означает отсутствия inline-строк и не охватывает весь общий справочник — см. catalogCoverage."},"ingredientSetComplete":{"type":"boolean"},"counts":{"type":"object"},"catalogCoverage":{"type":"object","required":["userReferencedRows","masterReferencedRows","inlineRows","semiFinishedRows"],"properties":{"userReferencedRows":{"type":"integer"},"masterReferencedRows":{"type":"integer"},"inlineRows":{"type":"integer","description":"Строки без связи со справочником. Требуют сопоставления у импортёра и не объединяются по названию."},"semiFinishedRows":{"type":"integer"}}},"dataHash":{"type":"string","description":"SHA-256 канонического JSON объекта data после применения прав. Одинаковые данные дают одинаковый hash; изменение вложенного полуфабриката его меняет."}}},"data":{"type":"object","required":["ingredients","recipes"],"properties":{"ingredients":{"type":"array","items":{"$ref":"#/components/schemas/Ingredient"}},"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}}}},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/Warning"}}}}},"responses":{}},"x-error-types":{"base":"https://calcrest.ru/developers/api/errors"},"x-limits":{"requestsPerMinutePerKey":60,"requestsPerMinutePerWorkspace":120,"exportsPerHour":6,"exportsPerDay":60,"selectionRecipeIds":500,"graphRecipes":2000,"graphIngredients":10000,"graphComponents":25000,"semiFinishedDepth":20,"requestBodyBytes":262144,"snapshotContentBytes":26214400}}