/api/projects?offset=0Список проектов текущего посетителя. Ответ — projects и nextOffset для следующей страницы.
Интеграция расчёта мебели: аутентификация, загрузка документов, анализ проекта, диалог и получение коммерческих предложений. Готовый виджет использует те же проектные API.
Для подключения без разработки API-интерфейса используйте готовый виджет. Он открывает загрузку файлов и диалог внутри вашего сайта. Секретный ключ в код страницы вставлять не требуется: скрипт содержит публичный ID подключения. Виджет можно связать с существующей кнопкой, например «Рассчитать проект», через её HTML ID и атрибут data-trigger.
Доступны два варианта интеграции:
https://factory.ru. www и другие поддомены регистрируются отдельно.В кабинете нажмите «Код для вставки»: скрипт сразу скопируется в буфер обмена. Добавьте его перед закрывающим тегом </body> на своём сайте.
<script src="https://merillo-mebel.ru/widget.js"
data-widget="PUBLIC_WIDGET_ID"
data-mode="modal" defer></script>Скрипт сам добавляет кнопку «Рассчитать проект» в нижний угол страницы. На компьютере она открывает окно поверх сайта, на телефоне — на весь экран. Стиль загрузки, диалога и предложений совпадает с основным сервисом.
Код для вашего сайта выдаётся после пополнения и создания ключа. В примере выше PUBLIC_WIDGET_ID — обозначение. Возьмите готовый скрипт с настоящим ID из кабинета.
Установите кнопке на своём сайте уникальный id="calculate-project". Если ID уже есть, используйте его. Затем добавьте к скопированному из кабинета тегу script атрибут data-trigger="calculate-project" и разместите скрипт один раз перед </body>. Значения ID кнопки и data-trigger должны совпадать, без символа #. В этом режиме скрипт подключится к существующей кнопке и не добавит плавающую.
<button type="button" id="calculate-project">Рассчитать проект</button><script src="https://merillo-mebel.ru/widget.js"
data-widget="PUBLIC_WIDGET_ID" data-mode="modal"
data-trigger="calculate-project" defer></script>Для HTML-блоков конструкторов вставляется тот же скрипт. Проверьте опубликованную страницу: редактор конструктора может ограничивать внешние скрипты и iframe.
Кнопка находится на сайте производителя, а сессия, анализ, база знаний и проверка оплаты работают на сервере Мерилло.
В готовом виджете скрипт загружает iframe с нашего сервера. Для самостоятельного интерфейса ваш сервер обменивает API-ключ на отдельный токен посетителя. Проектные запросы интерфейса направляйте через свой сервер: браузерный CORS для обмена ключа не включён.
Внешние запросы отправляются по HTTPS на https://merillo-mebel.ru. TLS шифрует заголовки авторизации, файлы, сообщения и ответы при передаче. Оставляйте проверку TLS-сертификата включённой; не передавайте ключи по HTTP и не отключайте проверку сертификата. HTTP в локальных примерах допустим только для изолированного localhost-окружения.
Ваш браузерный интерфейс обращается к вашему серверу по HTTPS; сервер направляет запросы в Мерилло и возвращает посетителю данные ответа. Для JSON передавайте Content-Type: application/json. Токены передавайте только в заголовке Authorization: Bearer …, не в URL. Обмен API-ключа выполняется сервером без браузерных заголовков Origin и Sec-Fetch-Site.
Настройте подтверждение один раз для каждого подключения. Вам понадобятся доступ к DNS привязанного домена, действующий API-ключ из кабинета и сервер, с которого ваш интерфейс обращается в Мерилло.
https://shop.factory.ru укажите в кабинете именно этот origin. factory.ru и shop.factory.ru — разные подключения. В кабинете нажмите «Код для вставки» и скопируйте значение data-widget. Оно понадобится для DNS-записи. Для собственного интерфейса добавлять тег script на страницу не требуется.merillo-signing-seed и выведет открытый ключ. Перенесите содержимое файла в хранилище секретов сервера как MERILLO_SIGNING_SEED. API-ключ из кабинета сохраните отдельно как MERILLO_API_KEY. Оба секрета должны оставаться на сервере.https://shop.factory.ru полное имя записи — _merillo-api.shop.factory.ru. Если панель DNS автоматически дописывает зону factory.ru, в поле имени введите _merillo-api.shop. Значение записи — merillo-api-v1=ID:PUBLIC_KEY. Подставьте ID из data-widget и открытый ключ из вывода скрипта. Дождитесь, пока запись появится в публичном DNS. Для проверки можно выполнить dig +short TXT _merillo-api.shop.factory.ru, подставив свой домен.MERILLO_SITE_ORIGIN в точности как в кабинете, без пути и завершающего слеша. Если у сайта нестандартный HTTPS-порт, укажите его в origin. В имени TXT-записи используется только доменное имя. Запустите серверный пример с тестовым идентификатором посетителя. Успешный обмен возвращает HTTP 200 и токен. Пример затем создаёт тестовый проект и выводит его ID.import { generateKeyPairSync } from "node:crypto";
import { writeFileSync } from "node:fs";
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
writeFileSync("merillo-signing-seed", privateKey.export({ format: "jwk" }).d, { mode: 0o600, flag: "wx" });
console.log(publicKey.export({ format: "jwk" }).x);Для каждого запроса POST /api/v1/sessions подпишите Ed25519 восемь строк ниже в кодировке UTF-8. Разделяйте строки символом LF (\n). После последней строки перевод строки не ставьте. API_KEY_UUID — UUID между двумя точками в серверном API-ключе. ID из data-widget используется в DNS-записи.
merillo-api-v1
POST
/api/v1/sessions
https://shop.factory.ru
API_KEY_UUID
RANDOM_VISITOR_ID_1234567890
UNIX_SECONDS
RANDOM_BASE64URLПередайте Unix-время в секундах в X-Merillo-Timestamp. Сервер допускает отставание до 60 секунд и опережение до 10 секунд. Сгенерируйте 16 случайных байтов и передайте их в X-Merillo-Nonce в base64url без =. Подпись передайте в X-Merillo-Signature в том же формате. Используйте в подписи те же значения visitorId, времени и nonce, которые отправляете в запросе.
Для каждого повтора, в том числе после сетевого сбоя, сформируйте новый nonce, время и подпись. Сохраните тот же visitorId. Если обмен возвращает HTTP 403 при включённом подключении, проверьте TXT-запись, origin, синхронизацию часов сервера и формат подписи.
Если интерфейс уже подключён, добавьте DNS-запись и обновите обмен ключа. Получите новый токен для каждого посетителя при следующем обращении, сохранив его прежний visitorId. Проектные маршруты и идентификаторы проектов сохраняются.
POST https://merillo-mebel.ru/api/v1/sessions
Authorization: Bearer SERVER_API_KEY
X-Merillo-Timestamp: UNIX_SECONDS
X-Merillo-Nonce: RANDOM_BASE64URL
X-Merillo-Signature: ED25519_BASE64URL
Content-Type: application/json
{"visitorId":"RANDOM_VISITOR_ID_1234567890"}{
"token": "VISITOR_TOKEN",
"expiresIn": 86400,
"contactConsentVersion": "2026-11-06.1"
}visitorId — случайный идентификатор из подтверждённой серверной сессии посетителя: 16–128 символов A–Z, a–z, 0–9, _ и -. Не используйте телефон или email.При нулевом балансе, отключении сайта или отзыве ключа виджет блокируется, включая существующие сессии. Замена ключа отзывает прежние API-сессии; готовый виджет продолжает работать при наличии нового действующего ключа.
Каждый пример получает токен посетителя, создаёт проект и выводит его ID. Загрузка файла и запуск ИИ выполняются следующими отдельными шагами.
Node.js 24+ · встроенный fetch
import { createPrivateKey, randomBytes, sign } from "node:crypto";
const base = process.env.MERILLO_BASE_URL || "https://merillo-mebel.ru";
const key = process.env.MERILLO_API_KEY;
const visitorId = process.env.MERILLO_VISITOR_ID;
const origin = process.env.MERILLO_SITE_ORIGIN;
const seed = Buffer.from(process.env.MERILLO_SIGNING_SEED || "", "base64url");
if (!key || !visitorId || !origin || seed.length !== 32) throw new Error("Set server environment variables");
const privateKey = createPrivateKey({ format: "der", type: "pkcs8",
key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]) });
async function post(path, token, body, proof = {}) {
const response = await fetch(base + path, {
method: "POST", redirect: "error", cache: "no-store",
signal: AbortSignal.timeout(20000),
headers: { Authorization: "Bearer " + token,
"Content-Type": "application/json", ...proof },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error("Merillo HTTP " + response.status);
return response.json();
}
// visitorId comes from your visitor's server session.
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString("base64url");
const message = ["merillo-api-v1", "POST", "/api/v1/sessions", origin,
key.split(".")[1], visitorId, timestamp, nonce].join("\n");
const session = await post("/api/v1/sessions", key, { visitorId }, {
"X-Merillo-Timestamp": timestamp,
"X-Merillo-Nonce": nonce,
"X-Merillo-Signature": sign(null, Buffer.from(message), privateKey).toString("base64url"),
});
const result = await post("/api/projects", session.token, {
name: "Проект кухни",
});
console.log(result.project.id);
// Keep session.token on your server for subsequent project requests.Примеры запускаются на сервере: в обработчике вашего сайта, фоновом сервисе или локальном терминале. Сохраняйте токен в серверной сессии этого посетителя. Не отправляйте его другим пользователям.
Во всех запросах ниже используйте заголовок Authorization: Bearer VISITOR_TOKEN. ID — идентификатор проекта из ответа создания.
/api/projects?offset=0Список проектов текущего посетителя. Ответ — projects и nextOffset для следующей страницы.
/api/projects/IDПолучить метаданные проекта по его идентификатору.
/api/projectsСоздать проект: JSON {name}. Ответ — project.id.
/api/projects/ID/documentsОткрыть загрузку файла: JSON {name, mime, bytes}. Ответ — document.id и offset.
/api/projects/ID/documents/DOC_IDОтправить часть файла с Upload-Offset и Content-Type: application/octet-stream.
/api/projects/ID/documents/DOC_IDУзнать принятый offset после обрыва соединения.
/api/projects/ID/documents/DOC_IDУдалить документ. Для изменённого состава файлов понадобится новый анализ.
/api/projects/ID/documents/DOC_ID/completeЗавершить загрузку. Ответ 202; начинается проверка файла.
/api/projects/ID/documentsПолучить статусы документов, ревизии и processing с прогрессом анализа.
/api/projects/ID/analyzeЗапустить анализ, когда все файлы имеют status=ready. Ответ 202.
/api/projects/ID/chatПолучить messages, activeQuestion и offers без вызова ИИ.
/api/projects/ID/chatОтправить JSON {message}. После анализа первый шаг допускает пустую строку.
/api/projects/ID/chatОтменить активный ответ: передать тот же X-Job-Id, что был в POST.
/api/projects/IDПереименовать проект: JSON {title}.
/api/projects/IDУдалить проект посетителя и отменить активную обработку. Финансовые записи сохраняются.
Сначала получите токен через POST /api/v1/sessions, затем создайте проект. В каждом ответе проверяйте HTTP-статус до чтения данных: успешное создание возвращает 201 и JSON с объектом project. Сохраните project.id в серверной сессии посетителя и используйте его в следующих запросах.
POST https://merillo-mebel.ru/api/projects
Authorization: Bearer VISITOR_TOKEN
Content-Type: application/json
{"name":"Проект кухни"}{
"project": {
"id": "PROJECT_ID",
"title": "Проект кухни"
}
}В примерах ответов показаны используемые поля; API может возвращать дополнительные метаданные. Готовые серверные реализации этих запросов приведены в примерах на пяти языках.
Откройте загрузку через POST documents. Отправляйте байты частями до 4 МБ, начиная с offset из ответа. После каждой части берите новый offset с сервера; при потере ответа сначала проверьте его через GET. Затем вызовите /complete.
{
"name": "kitchen.pdf",
"mime": "application/pdf",
"bytes": 102400
}Дождитесь status=ready у всех файлов, затем вызовите /analyze. Опрос GET documents возвращает processing с pagesDone, pagesTotal и состоянием очереди. Когда analyzedRevision совпал с revision и анализ завершён, переходите к /chat.
POST https://merillo-mebel.ru/api/projects/PROJECT_ID/chat
Authorization: Bearer VISITOR_TOKEN
Content-Type: application/json
{"message":"Фасады матовые, мебель нужна в декабре."}Успешный POST /chat возвращает HTTP 200 и JSON с текущими messages, activeQuestion и offers. Текст сообщений находится в поле body; role различает user и assistant. Ответ с вопросом не означает, что КП уже готово: пока предложения не сформированы, offers равен null.
{
"messages": [
{
"role": "assistant",
"body": "Какой бюджет вы планируете на мебель?"
}
],
"offers": null
}Пример текста выше условный. Отображайте сообщения и варианты ответа, полученные от API, а не строки из документации. Для восстановления диалога без нового обращения к ИИ выполните GET /api/projects/PROJECT_ID/chat с тем же токеном. Поле earlierCursor используется для предыдущих сообщений: передайте его как параметр before следующего GET /chat.
Запуск анализа возвращает 202 — задача принята, но результат ещё не готов. Следите за состоянием через GET documents, а не повторным POST /analyze. При сетевом обрыве запроса диалога сначала прочитайте GET /chat: повторный POST не является идемпотентным и может привести к новому платному вызову.
Помощник использует общую базу знаний Мерилло и задаёт недостающие вопросы. GET-запросы и повторное открытие виджета не вызывают ИИ. Повторяйте POST только по явному действию посетителя: он может создать новый платный запрос.
Три варианта появляются в поле offers ответа /chat. Перед скачиванием PDF действуют те же правила контакта и согласия, что на основном сайте.
Покажите посетителю согласие на обработку данных и политику конфиденциальности. Только после явного согласия отправляйте POST /api/contact с его токеном.
{
"project_id": "PROJECT_ID",
"name": "Иван",
"phone": "+79990000000",
"contact_method": "call",
"consent": true,
"consent_version": "2026-11-06.1",
"purpose": "download"
}GET /api/contact?project_id=PROJECT_ID возвращает ранее сохранённый контакт этого посетителя и consentCurrent. Используйте это состояние, чтобы повторно не запрашивать уже подтверждённые данные.
Способы связи: call, whatsapp, telegram, max. Берите актуальную consent_version из contactConsentVersion ответа создания сессии.
GET /api/projects/PROJECT_ID/offers/ENCODED_LEVEL
Authorization: Bearer VISITOR_TOKENENCODED_LEVEL — поле level нужного предложения, закодированное для URL. Ответ — application/pdf с именем файла в Content-Disposition. Файл не скачивается до валидных контактов и согласия; без них возвращается 403 CONTACT_REQUIRED.
Загрузка сама по себе не создаёт лид. После выполнения условий обращения контакт поступает в CRM Мерилло; с заказчиком работает наш менеджер. Доступ к общей CRM производителю не выдаётся.
API-баланс общий для подключённых сайтов и учитывается отдельно от пакетов расчётов.
| Параметр | Значение |
|---|---|
| Размер одного файла | 100 МБ |
| Количество файлов в проекте | 10 |
| Суммарный размер файлов | 300 МБ |
| Страниц в одном PDF | 300 |
| Страниц в проекте | 600 |
| Размер части загрузки | 4 МБ |
PDF, DOCX, PNG, JPEG и WebP. Для новых интеграций используйте загрузку частями. Старый multipart-маршрут одного файла сохраняет лимит 15 МБ. Новое подключение допускает 100 попыток создания проекта в день; лимит может изменить администратор. Выдача сессий ограничена 120 запросами в минуту на сайт. У серверного API и готового виджета отдельные лимиты выдачи.
401Недействительный ключ или сессия. Получите новый токен для того же посетителя.
402Нужен положительный API-баланс или не хватает средств на резерв.
403Подключение отключено, подпись или источник запроса не прошли проверку либо для PDF не получен контакт.
404Проект или подключение отсутствует либо недоступно этому посетителю.
409Ключ не создан, состояние проекта изменилось или обработка уже выполняется.
429Достигнут лимит. При наличии Retry-After выдержите указанное время.
503Сервис, проверка DNS, парсер или проверка файлов временно недоступны.
При неуспешном статусе читайте поле error и, если оно есть, code в JSON ответа. Не считайте ответ успешным только по наличию JSON. Для ошибок 401/402/403 сначала устраните причину по сообщению ответа. При повторе обмена ключа сформируйте новую подпись. Временные сетевые ошибки и 5xx не гарантируют, что провайдер не успел обработать запрос.
При блокировке посетительского токена существующие проектные маршруты могут отвечать 401 или 404. Публичный ID и CSP ограничивают встраивание, но не заменяют антибот-защиту и эксплуатационные ограничения трафика.