API для сайтов и лендингов

Делайте сайты для магазинов, кафе, ресторанов и доставок на Jooptor. Меню, цены и стоп-лист сайт берёт из панели, а заказы и заявки с сайта попадают в ту же панель — рядом с заказами из Instagram, WhatsApp и Telegram.

Один источник менюВладелец меняет цену или ставит блюдо в стоп-лист в панели — сайт видит это через минуту.
Цены считает серверСайт присылает только id позиций и количество. Подменить цену или скидку из браузера нельзя.
Заказы — в работуСтатусы, оплата, «Передать курьеру», уведомления в Telegram — всё уже в панели.

Базовый адрес:

https://europe-west1-supercrm-a250d.cloudfunctions.net/api/v1

Только HTTPS. Тела запросов и ответов — JSON (UTF-8). Машиночитаемое описание: openapi.json — импортируется в Postman, Insomnia, генераторы клиентов.

Быстрый старт

  1. Зарегистрируйте бизнес на jooptor.xyz/register. Делаете сайт для клиента — попросите владельца пригласить вас в команду (Настройки → Команда) или передать ключ.
  2. Получите ключ. Настройки → Интеграции → «Сайты и лендинги»: включите «Принимать заказы с сайтов», нажмите «Сохранить» — появится ключ site_….
  3. Ограничьте домены. Там же добавьте домен сайта (например, cafe.kg) — ключ перестанет работать на чужих страницах.
  4. Загрузите меню одним запросом:
curl https://europe-west1-supercrm-a250d.cloudfunctions.net/api/v1/menu \
  -H "Authorization: Bearer site_ВАШ_КЛЮЧ"

Ключи и доступ

Ключ сайта — публикуемый: он рассчитан на то, чтобы лежать в JavaScript страницы, как публичный ключ платёжной системы. Поэтому он даёт ровно то, что и так видит покупатель:

МожноНельзя
читать меню, цены, стоп-лист, точки самовывозавидеть закупочные и минимальные цены, скрытые позиции
оформлять заказы и заявки — по ценам серверазадавать цену, скидку или сумму заказа
узнать статус заказа, который оформил этот ключчитать чужие заказы, клиентов, переписку

Передавайте ключ в заголовке:

Authorization: Bearer site_ВАШ_КЛЮЧ

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.moderestaurant — блюда (у них нет остатков), shop — товары
items[].availablefalse — в стоп-листе или закончилось: покажите «нет в наличии», но не давайте заказать
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"
}
ПолеПравила
items1–50 позиций, qty 1–999. Одинаковые productId складываются.
name1–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 символов, необязательно
langru | 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": "у окна" }
ПолеПравила
kindbooking | callback | question | other (по умолчанию)
dateкак ввёл посетитель, до 40 символов
guests1–500
textдо 1000 символов

Ответ 201: { "id": "…" }.

Идемпотентность

Сеть рвётся, покупатели жмут кнопку дважды. Чтобы повтор не создал второй заказ, передавайте в POST /orders и POST /requests заголовок Idempotency-Key — уникальную строку на одну попытку оформления (8–128 символов: буквы, цифры, _ . : -; удобно crypto.randomUUID()).

Ошибки

Любая ошибка — JSON с текстом для человека, машинным кодом и идентификатором запроса (он же в заголовке X-Request-Id — назовите его, когда пишете в поддержку):

{
  "error": "Часть товаров закончилась",
  "code": "items_unavailable",
  "requestId": "0b6c2f7e-…",
  "details": { "problems": [{ "productId": "p-lagman", "available": 0 }] }
}
HTTPcodeКогда и что делать
400invalid_requestТело не по схеме. issues — список {path, message}, подсветите поля формы.
400invalid_phoneНеполный номер.
401missing_key, invalid_keyНет заголовка Authorization или ключ неизвестен (сменили в панели).
403key_disabledПриём с сайтов выключен в панели.
403origin_not_allowedДомен страницы не в списке разрешённых.
403customer_blockedБизнес заблокировал этот номер.
404not_foundНет такого адреса или заказа.
409items_unavailableПозиция в стоп-листе или закончилась: details.problems — какие и сколько осталось. Обновите меню и корзину.
409 / 422idempotency_*См. идемпотентность.
429rate_limitedЛимит запросов. Подождите Retry-After секунд.
5xxserver_errorПовторите с тем же Idempotency-Key через пару секунд.

Новые коды могут появляться — обрабатывайте неизвестный код по HTTP-статусу.

Лимиты и кэш

Безопасность и персональные данные

На стороне API:

На стороне вашего сайта:

Нашли уязвимость? Напишите в поддержку в Telegram — не публикуйте детали, пока мы не исправим.

Готовая страница

Минимальный рабочий пример без библиотек: меню, корзина, заказ на самовывоз с идемпотентностью и обработкой ошибок.

<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-*.