API для сайтов и лендингов
Делайте сайты для магазинов, кафе, ресторанов и доставок на Jooptor. Меню, цены и стоп-лист сайт берёт из панели, а заказы и заявки с сайта попадают в ту же панель — рядом с заказами из Instagram, WhatsApp и Telegram.
Базовый адрес:
https://europe-west1-supercrm-a250d.cloudfunctions.net/api/v1
Только HTTPS. Тела запросов и ответов — JSON (UTF-8). Машиночитаемое описание: openapi.json — импортируется в Postman, Insomnia, генераторы клиентов.
Быстрый старт
- Зарегистрируйте бизнес на jooptor.xyz/register. Делаете сайт для клиента — попросите владельца пригласить вас в команду (Настройки → Команда) или передать ключ.
- Получите ключ. Настройки → Интеграции → «Сайты и лендинги»: включите «Принимать заказы с сайтов», нажмите «Сохранить» — появится ключ
site_…. - Ограничьте домены. Там же добавьте домен сайта (например,
cafe.kg) — ключ перестанет работать на чужих страницах. - Загрузите меню одним запросом:
curl https://europe-west1-supercrm-a250d.cloudfunctions.net/api/v1/menu \
-H "Authorization: Bearer site_ВАШ_КЛЮЧ"
Ключи и доступ
Ключ сайта — публикуемый: он рассчитан на то, чтобы лежать в JavaScript страницы, как публичный ключ платёжной системы. Поэтому он даёт ровно то, что и так видит покупатель:
| Можно | Нельзя |
|---|---|
| читать меню, цены, стоп-лист, точки самовывоза | видеть закупочные и минимальные цены, скрытые позиции |
| оформлять заказы и заявки — по ценам сервера | задавать цену, скидку или сумму заказа |
| узнать статус заказа, который оформил этот ключ | читать чужие заказы, клиентов, переписку |
Передавайте ключ в заголовке:
Authorization: Bearer site_ВАШ_КЛЮЧ
- Разрешённые домены. Если список задан, запросы принимаются только со страниц этих доменов и их поддоменов (по заголовку
Origin). Пустой список — любой сайт; для боевого сайта задайте домен. - Смена ключа. «Создать новый ключ» в панели отключает старый. Делайте так, если ключ попал не туда или подрядчик закончил работу.
- Выключение. Переключатель «Принимать заказы с сайтов» отключает ключ целиком: все запросы получают
403 key_disabled. - Изменения настроек ключа — включение, выключение, новый ключ, список доменов — доходят до API в течение 30 секунд. Проверяете подключение сразу после сохранения в панели — подождите полминуты.
- Для сайтов, сделанных до появления заголовка, ключ можно передать первым сегментом пути:
/api/v1/site_…/menu. В новых сайтах используйте заголовок — так ключ не попадает в логи по URL.
Меню
GET /menu
Категории, позиции в порядке меню, точки самовывоза и данные о бизнесе.
{
"business": {
"name": "Чайхана №1", "description": "Плов и шашлык", "phone": "+996555001122",
"workingHours": "10:00–23:00", "mode": "restaurant", "country": "KG", "currency": "KGS",
"paymentDetails": "MBank +996 555 …", "paymentQrUrl": "https://…"
},
"categories": [{ "id": "c-hot", "name": "Горячее" }],
"items": [{
"id": "p-plov", "name": "Плов чайханский", "description": "Рис девзира, баранина",
"priceKgs": 350, "photoUrls": ["https://…/plov.jpg"], "categoryId": "c-hot",
"attributes": { "Выход": "350 г", "Время приготовления": "15 мин" },
"available": true
}],
"locations": [{ "id": "l-1", "name": "Центр", "address": "Бишкек, ул. Киевская 100", "landmark": "напротив ЦУМа" }]
}
| Поле | Что значит |
|---|---|
business.mode | restaurant — блюда (у них нет остатков), shop — товары |
items[].available | false — в стоп-листе или закончилось: покажите «нет в наличии», но не давайте заказать |
items[].attributes | свободные поля карточки: выход, калорийность, острота, цвет, размер… |
locations | точки самовывоза; id нужен для заказа с самовывозом |
Заказы
POST /orders
POST /api/v1/orders
Authorization: Bearer site_ВАШ_КЛЮЧ
Content-Type: application/json
Idempotency-Key: 6f1c2a1e-6c1d-4a7e-9a55-0b7c3f0e9d21
{
"items": [{ "productId": "p-plov", "qty": 2 }],
"name": "Айгуль",
"phone": "0555 12 34 56",
"fulfillment": {
"method": "delivery",
"address": "Бишкек, Киевская 100, кв 12",
"location": { "lat": 42.8746, "lng": 74.6122, "accuracy": 25 }
},
"comment": "Без лука, к 19:00",
"lang": "ru"
}
| Поле | Правила |
|---|---|
items | 1–50 позиций, qty 1–999. Одинаковые productId складываются. |
name | 1–80 символов |
phone | номер страны бизнеса. КР: +996 и 9 цифр — подходят 0555…, 555…, +996 555 …. КЗ: +7 и 10 цифр. |
fulfillment | Самовывоз: {"method":"pickup","locationId":"l-1"}.Доставка: address одной строкой (5–200 символов) или объектом {city, street, house, flat, comment} — тогда city, street, house обязательны. location необязательна: менеджер и курьер получат ссылку на карту. |
comment | до 500 символов, необязательно |
lang | ru | ky | uz — язык текстов ошибок |
Лишние поля отклоняются (400 invalid_request) — так опечатка в имени поля не теряется молча. Ответ 201:
{
"id": "k3x9q2m1b7aa", "number": "k3x9q2m1", "status": "new", "totalKgs": 700,
"items": [{ "id": "p-plov", "name": "Плов чайханский", "qty": 2, "priceKgs": 350 }],
"paymentUrl": null,
"paymentDetails": "MBank +996 555 …",
"paymentQrUrl": "https://…"
}
Покажите покупателю number. Если подключена онлайн-оплата, в paymentUrl будет ссылка — отправьте покупателя по ней. Иначе покажите paymentDetails / paymentQrUrl или «менеджер перезвонит». Заказ сразу появляется в панели и в чате заказов Telegram.
GET /orders/{id}
Статус заказа для страницы «Спасибо» или отслеживания. Только заказы, оформленные этим ключом, и без персональных данных.
{ "id": "k3x9q2m1b7aa", "number": "k3x9q2m1", "status": "confirmed", "totalKgs": 700, "paid": false, "createdAt": "2026-11-10T14:05:11.204Z" }
Статусы: new → confirmed → shipped (передан курьеру) → done, или cancelled. Опрашивайте не чаще раза в 15 секунд.
Заявки
POST /requests
Заявка без корзины: бронь стола, обратный звонок, вопрос. Попадает в панель во вкладку «Заказы → Заявки» и в чат заказов Telegram.
{ "kind": "booking", "name": "Бакыт", "phone": "0700 11 22 33", "date": "пт, 19:30", "guests": 4, "text": "у окна" }
| Поле | Правила |
|---|---|
kind | booking | callback | question | other (по умолчанию) |
date | как ввёл посетитель, до 40 символов |
guests | 1–500 |
text | до 1000 символов |
Ответ 201: { "id": "…" }.
Идемпотентность
Сеть рвётся, покупатели жмут кнопку дважды. Чтобы повтор не создал второй заказ, передавайте в POST /orders и POST /requests заголовок Idempotency-Key — уникальную строку на одну попытку оформления (8–128 символов: буквы, цифры, _ . : -; удобно crypto.randomUUID()).
- Повтор с тем же ключом и тем же телом вернёт тот же заказ (тот же
idи номер) в его текущем состоянии — с заголовкомIdempotent-Replayed: true. Порядок полей в теле значения не имеет. - Тот же ключ с другим телом —
422 idempotency_conflict. Первый запрос ещё выполняется —409 idempotency_in_progress, повторите через секунду. - Если запрос завершился ошибкой, ключ освобождается — исправьте данные и повторите с ним же.
- Повтор уже выполненного запроса не тратит лимит.
- Если попытка оборвалась на нашей стороне, ключ освобождается не позже чем через 2 минуты.
- Ключ хранится 24 часа. Новый заказ — новый ключ.
Ошибки
Любая ошибка — JSON с текстом для человека, машинным кодом и идентификатором запроса (он же в заголовке X-Request-Id — назовите его, когда пишете в поддержку):
{
"error": "Часть товаров закончилась",
"code": "items_unavailable",
"requestId": "0b6c2f7e-…",
"details": { "problems": [{ "productId": "p-lagman", "available": 0 }] }
}
| HTTP | code | Когда и что делать |
|---|---|---|
| 400 | invalid_request | Тело не по схеме. issues — список {path, message}, подсветите поля формы. |
| 400 | invalid_phone | Неполный номер. |
| 401 | missing_key, invalid_key | Нет заголовка Authorization или ключ неизвестен (сменили в панели). |
| 403 | key_disabled | Приём с сайтов выключен в панели. |
| 403 | origin_not_allowed | Домен страницы не в списке разрешённых. |
| 403 | customer_blocked | Бизнес заблокировал этот номер. |
| 404 | not_found | Нет такого адреса или заказа. |
| 409 | items_unavailable | Позиция в стоп-листе или закончилась: details.problems — какие и сколько осталось. Обновите меню и корзину. |
| 409 / 422 | idempotency_* | См. идемпотентность. |
| 429 | rate_limited | Лимит запросов. Подождите Retry-After секунд. |
| 5xx | server_error | Повторите с тем же Idempotency-Key через пару секунд. |
Новые коды могут появляться — обрабатывайте неизвестный код по HTTP-статусу.
Лимиты и кэш
- Чтение — до 600 запросов в минуту с одного IP-адреса на ключ. Запросы без ключа или с неверным ключом — до 30 в минуту с IP. Сверх этого —
429сRetry-After. POST /ordersиPOST /requests— по 30 в час с одного IP-адреса для каждого бизнеса (покупатели одного мобильного оператора часто выходят в интернет с общего адреса). Ответы несут заголовкиRateLimit-Limit,RateLimit-Remaining,RateLimit-Reset(секунд до нового окна).GET /menuкэшируется на 30 секунд (Cache-Control: private, max-age=30— только в браузере покупателя, общие прокси его не хранят); с учётом нашего кэша изменения меню доходят до сайта в пределах минуты. Загружайте меню один раз при открытии страницы, а не на каждое действие.- Остальные ответы не кэшируются (
Cache-Control: no-store).
Безопасность и персональные данные
На стороне API:
- только HTTPS; цены, скидки и суммы — с сервера; строгая проверка схемы, лишние поля отклоняются;
- ключ ограничивается доменами, отзывается одной кнопкой и не даёт доступа к клиентам и переписке;
- лимиты частоты на запись, идемпотентность заказов, идентификаторы заказов из криптостойкого генератора;
- статус заказа отдаётся только сайту, который его оформил, и без имени и телефона.
На стороне вашего сайта:
- Укажите домены в панели до запуска.
- Персональные данные. Имя, телефон и адрес — персональные данные покупателя. Рядом с кнопкой заказа дайте ссылку на политику конфиденциальности бизнеса и не храните эти данные у себя дольше, чем нужно для показа «Спасибо».
- Не логируйте тела запросов с телефонами и адресами в аналитику и сторонние сервисы.
- Защита формы от ботов: скрытое поле-ловушка (honeypot) и блокировка кнопки на время отправки. Сервер всё равно ограничивает частоту.
- Content-Security-Policy для вашего сайта: разрешите
connect-src https://europe-west1-supercrm-a250d.cloudfunctions.netи картинки меню. - Выводите названия и описания из меню как текст (
textContent), не как HTML.
Готовая страница
Минимальный рабочий пример без библиотек: меню, корзина, заказ на самовывоз с идемпотентностью и обработкой ошибок.
<div id="menu"></div>
<form id="order">
<input name="name" placeholder="Имя" required>
<input name="phone" placeholder="0555 123 456" required>
<button>Заказать</button>
<p id="result" role="status"></p>
</form>
<script type="module">
const API = 'https://europe-west1-supercrm-a250d.cloudfunctions.net/api/v1';
const KEY = 'site_ВАШ_КЛЮЧ';
const call = (path, body, idem) => fetch(API + path, {
method: body ? 'POST' : 'GET',
headers: { Authorization: 'Bearer ' + KEY, ...(body && { 'Content-Type': 'application/json', 'Idempotency-Key': idem }) },
body: body && JSON.stringify(body),
}).then(async (r) => ({ ok: r.ok, data: await r.json() }));
const { data: menu } = await call('/menu');
const cart = new Map();
for (const item of menu.items) {
const b = document.createElement('button');
b.textContent = `${item.name} — ${item.priceKgs} сом`; // текст, не HTML
b.disabled = !item.available;
b.onclick = () => cart.set(item.id, (cart.get(item.id) ?? 0) + 1);
document.querySelector('#menu').append(b);
}
let idem = crypto.randomUUID(); // одна попытка оформления
document.querySelector('#order').onsubmit = async (e) => {
e.preventDefault();
const f = new FormData(e.target), btn = e.target.querySelector('button');
btn.disabled = true;
const { ok, data } = await call('/orders', {
items: [...cart].map(([productId, qty]) => ({ productId, qty })),
name: f.get('name'), phone: f.get('phone'),
fulfillment: { method: 'pickup', locationId: menu.locations[0].id },
}, idem);
btn.disabled = false;
document.querySelector('#result').textContent = ok ? `Заказ ${data.number} принят` : data.error;
if (ok) { cart.clear(); idem = crypto.randomUUID(); } // следующий заказ — новый ключ
};
</script>
Версии и изменения
Версия — в пути (/v1). Внутри версии мы только добавляем: новые поля в ответах, новые необязательные поля в запросах, новые коды ошибок. Игнорируйте незнакомые поля ответа. Всё, что ломает совместимость, выйдет как /v2, а /v1 будет работать не меньше 12 месяцев после объявления.
| Дата | Что изменилось |
|---|---|
| 2026-11-10 | Первая публичная версия: /menu, /orders, /orders/{id}, /requests, ключ в Authorization: Bearer, Idempotency-Key, коды ошибок, заголовки RateLimit-*. |