Службы доставки: API для подключения
Эта статья – для разработчика курьерской службы. Прочитав её, вы сможете принимать заказы заведений, которые работают на Shuqr, и сообщать им, где сейчас курьер.
Что это#
Shuqr – система для кафе и ресторанов: касса, кухня, онлайн-заказы. Заведение может подключить вашу службу у себя в настройках. Тогда Shuqr сам отправляет вам оплаченные заказы на доставку, а вы присылаете обратно этапы: курьер назначен, забрал заказ, доставил.
Договор с Shuqr не нужен. Условия, цены и расчёты вы обсуждаете напрямую с заведением.
Как это работает#
- Заведение подключает вашу службу у себя в настройках и нажимает «Проверить подключение» – Shuqr присылает
ping. - Когда пора везти заказ, Shuqr присылает
delivery.create. В течение 10 секунд вы отвечаете своим номером заявкиid. - Курьер движется – вы отправляете этапы на
callbackUrlиз заказа: назначен, едет в заведение, забрал заказ. - Этап
deliveredзакрывает заказ у заведения: он становится «Выдан». По пути кассир и гость видят курьера и текущий этап. - Пока курьер не забрал заказ, заведение может его отменить. Тогда придёт
delivery.cancel.
Как проходит подключение#
- Вы даёте заведению адрес своего сервера (только
https://) и ключ подписи – длинную случайную строку, которую придумываете сами. У каждого заведения ключ свой. - Заведение открывает в Shuqr «Настройки → Доставка», в блоке «Кто везёт» добавляет «Другая служба по API» и вводит ваш адрес, ключ и цену доставки – её платит гость. Время доставки для гостя можно указать по желанию.
- Когда заказ оплачен и пора везти, Shuqr отправляет вам запрос
delivery.create. Дальше вы сообщаете этапы доставки.
Заведение может подключить несколько служб и выбрать, какая основная. Если вы откажетесь от заказа или не ответите, заказ уйдёт следующей службе или кассиру заведения.
Несколько заведений
У каждого заведения свой ключ подписи. Проще всего дать каждому заведению свой адрес, например https://your-service.example.com/shuqr/cafe-pelmen, где cafe-pelmen – код заведения, который вы придумали сами. Пришёл запрос – по коду из адреса найдите ключ этого заведения и уже им проверьте подпись.
В delivery.create есть и поле restaurantId – номер заведения в Shuqr. По нему тоже можно понять, чей это заказ. Но верьте ему только после проверки подписи.
В ping поля restaurantId нет. Это ещё одна причина дать каждому заведению отдельный адрес.
Что нужно сделать у себя#
- Адрес HTTPS, который принимает
POSTс JSON и понимает три события:ping,delivery.create,delivery.cancel. - Отдельный адрес и ключ для каждого заведения (см. «Несколько заведений»).
- Проверку подписи
X-Shuqr-Signatureна каждом запросе. - Защиту от повторов: второй
delivery.createс тем жеdeliveryIdне должен создавать вторую заявку. - Ваш номер заявки
idв ответе наdelivery.create. - Отправку этапов на
callbackUrlиз заказа – при каждом изменении.
Запросы от Shuqr#
Shuqr отправляет запрос POST на адрес, который заведение указало в настройках. Тело – JSON в кодировке UTF-8.
| Заголовок | Значение |
|---|---|
Content-Type | Всегда application/json. |
X-Shuqr-Event | Тип события: ping, delivery.create или delivery.cancel. То же значение лежит в поле event тела. |
X-Shuqr-Signature | sha256= и подпись тела в hex. Как проверять – в разделе «Проверка подписи». Если заведение не ввело ключ, заголовка не будет – такой запрос лучше отклонить. |
ping – проверка связи
Когда заведение нажимает «Проверить подключение» в настройках, Shuqr присылает ping с подписью. Ответьте любым кодом 2xx – заведение увидит «Подключение работает». Неверная подпись – ответьте 401, и заведение увидит, что ключ нужно проверить.
{ "event": "ping" }delivery.create – новый заказ
Приходит, когда пора везти оплаченный заказ. Момент выбирает заведение: сразу после того, как заказ принят, когда кухня начала готовить, когда заказ готов, или вручную – кассир нажимает кнопку. Поэтому курьеру иногда придётся подождать в заведении. Номер orderNumber поможет забрать нужный заказ.
{
"event": "delivery.create",
"restaurantId": "cmh8x2p1q0000ab3c",
"deliveryId": "cmhf2k9x80001qa7z",
"orderNumber": "42",
"callbackUrl": "https://shuqr.ru/api/delivery/webhook/webhook?delivery=cmhf2k9x80001qa7z&sig=5e1c0f3a9b2d4e6f8a7b9c0d1e2f3a4b",
"pickup": {
"lat": 55.7601,
"lng": 37.6187,
"address": "Moscow, Tverskaya st, 7",
"contact": { "name": "Cafe Pelmen", "phone": "+74950000000" }
},
"dropoff": {
"lat": 55.7712,
"lng": 37.6329,
"address": "Moscow, Sretensky blvd, 4, apt 15",
"comment": "apt 15, entrance 2, floor 4, intercom 15",
"details": {
"city": "Moscow",
"street": "Sretensky blvd",
"house": "4",
"block": null,
"flat": "15",
"entrance": "2",
"floor": "4",
"intercom": "15",
"geoQuality": 0
},
"contact": { "name": "Anna", "phone": "+79990001122" }
},
"items": [
{ "name": "Pelmeni", "quantity": 2, "price": 390 },
{ "name": "Mors", "quantity": 1, "price": 150 }
],
"total": 1180,
"price": 250,
"currency": "RUB",
"scheduledAt": null,
"comment": "Please call when you arrive",
"locale": "ru",
"paid": true
}| Поле | Что это |
|---|---|
restaurantId | Номер заведения в Shuqr. По нему можно понять, чей это заказ, – но только после проверки подписи. Подробнее – в разделе «Несколько заведений». |
deliveryId | Номер доставки в Shuqr. Храните его: по нему Shuqr отменяет заявку, а вы защищаетесь от повторов. |
orderNumber | Номер заказа, который видят гость и кассир. Курьер называет его, когда забирает заказ. |
callbackUrl | Адрес, куда присылать этапы этой доставки. Сохраните его вместе с заявкой. |
pickup | Откуда забрать: координаты lat/lng, адрес address и contact заведения (название и телефон). Телефон может быть null, если заведение его не указало. |
dropoff | Куда везти: координаты, адрес и contact гостя (имя и телефон). dropoff.comment – квартира, подъезд, этаж и домофон одной строкой на языке заведения; dropoff.details – тот же адрес по частям (см. ниже). Оба поля могут отсутствовать. Телефон гостя может быть null – гость его не оставил. Тогда курьер звонит в заведение (контакт из pickup). |
items | Состав заказа: название, количество и цена за штуку. |
total | Сумма заказа. Пригодится для страховки груза. Деньги с гостя брать не нужно. |
price | Цена доставки, которую заведение указало в настройках. |
currency | Валюта всех сумм, код ISO 4217, например RUB. |
scheduledAt | К какому времени привезти (ISO 8601). null – как можно скорее. |
comment | Комментарий гостя и кассира к заказу: просьбы, «позвонить заранее». Квартира и подъезд – в dropoff. Может быть null. |
locale | Язык заведения, например ru или en. |
paid | Всегда true: заказ оплачен. |
Адрес гостя по частям
Если адрес выбран из подсказки, в dropoff.details приходят его части – по ним удобно искать дом в своей базе и показывать курьеру квартиру и подъезд отдельно. Все поля необязательные: у заказа, где адрес набран вручную, частей может не быть.
| Поле | Что это |
|---|---|
city | Город или населённый пункт. |
street | Улица с типом («ул Тверская»). |
house | Номер дома. |
block | Корпус, строение или литера с типом («к 2»). |
flat | Квартира или офис. |
entrance | Подъезд – указал гость. |
floor | Этаж – указал гость. |
intercom | Код домофона – указал гость. |
geoQuality | Точность координат: 0 – точно дом, 1 – ближайший дом, 2 – улица, 3–4 – населённый пункт или город, 5 – координат нет. При 2 и выше курьеру стоит уточнить адрес у гостя. |
Суммы – в рублях (или другой валюте из currency), не в копейках. Могут быть дробными, например 249.5.
Код 2xx и JSON с вашим номером заявки в id. В status можно сразу передать этап – обычно searching. Если status нет, Shuqr считает, что вы ищете курьера.
{ "id": "A-1042", "status": "searching" }Если тот же заказ передадут ещё раз (например, после отказа), придёт новый deliveryId – это новая заявка. Повтор с тем же deliveryId – тот же заказ: верните уже созданную заявку.
delivery.cancel – отмена
Приходит, когда заведение отменило заказ или вернуло гостю деньги, а также когда вы слишком долго ищете курьера (срок задаёт заведение). В поле id – ваш номер заявки из ответа на delivery.create.
{ "event": "delivery.cancel", "deliveryId": "cmhf2k9x80001qa7z", "id": "A-1042" }Отменили – ответьте 2xx. Такой заявки нет – 404, это тоже считается отменой. Курьер уже везёт заказ и отменить нельзя – 409.
Если вы ответили 409, заведение увидит, что отменить не удалось. Свяжитесь с ним, чтобы договориться, что делать с заказом.
Как Shuqr понимает ваш ответ
| Код ответа | Что значит для Shuqr |
|---|---|
2xx | Принято. |
401, 403 | Неверный ключ. Заведение увидит, что ключ надо проверить. |
422 | Вы не возите по этому адресу. |
409 | Только для отмены: отменить уже нельзя, курьер везёт заказ. |
404 | Только для отмены: такой заявки нет, считаем отменённой. |
4xx | Отказ от заказа. |
5xx | Служба недоступна. |
При любой ошибке на delivery.create заказ уходит запасной службе заведения или кассиру. Сам Shuqr этот запрос не повторяет.
В ответе с ошибкой можно передать поле error с понятным текстом. Shuqr сохранит его в истории доставки – так проще разобраться в сбое.
Проверка подписи#
Подпись доказывает, что запрос пришёл от Shuqr и его никто не менял. Shuqr берёт тело запроса как есть и считает от него HMAC-SHA256 с ключом, который заведение ввело в настройках. Результат в hex с приставкой sha256= лежит в заголовке X-Shuqr-Signature. Вы делаете то же самое у себя и сравниваете строки.
- Считайте подпись от «сырого» тела – ровно тех байтов, что пришли. Если сначала разобрать JSON и собрать заново, подпись не сойдётся.
- Сравнивайте функцией с постоянным временем:
hash_equalsв PHP,crypto.timingSafeEqualв Node.js. - Подпись не сошлась – ответьте
401и ничего не делайте с заказом.
const http = require('node:http');
const crypto = require('node:crypto');
// The signing key you gave to the restaurant. With several restaurants give each one its own address,
// e.g. /shuqr/cafe-pelmen, and look up that restaurant's key by the code from req.url.
const SECRET = process.env.SHUQR_SECRET;
// The signature is calculated over the raw request body, byte for byte.
function isFromShuqr(rawBody, header) {
const expected = Buffer.from('sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex'));
const received = Buffer.from(header || '');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
function reply(res, code, data) {
res.writeHead(code, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
http.createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const rawBody = Buffer.concat(chunks);
if (!isFromShuqr(rawBody, req.headers['x-shuqr-signature'])) {
return reply(res, 401, { error: 'Bad signature' });
}
const event = JSON.parse(rawBody.toString('utf8'));
if (event.event === 'ping') {
return reply(res, 200, { ok: true });
}
if (event.event === 'delivery.create') {
// Your code: save the order together with event.callbackUrl.
// If an order with this event.deliveryId already exists, return it, do not create a second one.
const order = saveOrder(event);
return reply(res, 200, { id: order.id, status: 'searching' });
}
if (event.event === 'delivery.cancel') {
// Your code: cancel the order. false = the courier already has the food.
const cancelled = cancelOrder(event.deliveryId);
return cancelled ? reply(res, 200, { ok: true }) : reply(res, 409, { error: 'Courier already picked up the order' });
}
reply(res, 400, { error: 'Unknown event' });
});
}).listen(8080);Оба примера проверены на настоящих запросах Shuqr. Функции saveOrder и cancelOrder – ваш код: сохранить заявку и отменить её.
Как сообщать этапы доставки#
Когда у заявки что-то меняется, отправьте POST с JSON на callbackUrl из заказа. Используйте адрес точно в том виде, в каком он пришёл, ничего в нём не меняйте.
Поля
| Поле | Что это |
|---|---|
status | Обязательно. Этап доставки – одно из значений ниже. |
id | Ваш номер заявки. |
courier | Курьер: name и phone. Их увидят кассир и гость. |
trackingUrl | Ссылка, где гость видит курьера на карте. Только https:// или http://. |
etaDropoffAt | Когда курьер будет у гостя, ISO 8601 с часовым поясом, например 2026-11-01T19:40:00+03:00. |
price | Итоговая цена доставки у вас. Попадёт в расходы заведения. Если не прислать, расходом считается цена из настроек заведения. |
reason | Почему доставка не состоялась – для returning, cancelled и failed. Значения – в таблице причин ниже. |
Присылайте только то, что изменилось: поле, которого нет в запросе, остаётся прежним. Только status нужен всегда.
{
"status": "assigned",
"id": "A-1042",
"courier": { "name": "Oleg", "phone": "+79990003344" },
"trackingUrl": "https://track.example.com/A-1042",
"etaDropoffAt": "2026-11-01T19:40:00+03:00",
"price": 250
}Этапы
| status | Что значит |
|---|---|
searching | Ищете курьера. |
assigned | Курьер назначен. |
to_pickup | Курьер едет в заведение. |
picked_up | Курьер забрал заказ. |
delivered | Гость получил заказ. Конец доставки: заказ в Shuqr закрывается и становится «Выдан». |
returning | Курьер не смог вручить заказ и везёт его обратно в заведение. |
cancelled | Вы отменили доставку. Конец доставки. |
failed | Не смогли взять заказ: не нашли курьера или не возите туда. Конец доставки. |
Обычный путь: searching → assigned → to_pickup → picked_up → delivered.
- Этапы идут только вперёд. Если прислать более ранний этап, Shuqr его пропустит. Исключение – курьер отказался до того, как забрал заказ: можно вернуть
searchingи искать другого. delivered,cancelledиfailed– последние. После них Shuqr ничего не меняет, поэтому итоговую цену присылайте не позже последнего этапа.failedилиcancelledдо того, как курьер забрал заказ, – Shuqr сразу передаёт заказ запасной службе заведения или кассиру.- Курьер забрал заказ, но вручить не смог: пришлите
returningс причиной, напримерguestRefused. Когда курьер вернёт заказ в заведение, пришлитеcancelledс причинойreturned. Дальше решает кассир.
{ "status": "returning", "id": "A-1042", "reason": "guestRefused" }{ "status": "cancelled", "id": "A-1042", "reason": "returned" }Причины
Первые четыре причины – для отказа до того, как курьер забрал заказ. Остальные – когда курьер уже забрал заказ, но вручить не смог.
| reason | Когда присылать |
|---|---|
courierNotFound | Не нашли курьера. |
courierCancelled | Курьер отказался от заказа. |
notDeliverable | Не возите по этому адресу. |
rejected | Отказ по другой причине. |
guestNoAnswer | Гость не отвечает или не открывает дверь. |
wrongAddress | Неверный адрес. |
guestRefused | Гость отказался от заказа. |
orderDamaged | Заказ испорчен или повреждён. |
courierProblem | Курьер не может довезти заказ (поломка и т. п.). |
handoverFailed | Не удалось вручить по другой причине. |
returned | Курьер вернул заказ в заведение. |
Другие значения reason Shuqr не запоминает, но сам этап примет.
Что ответит Shuqr
| Код | Что значит |
|---|---|
200 | Принято. Так же Shuqr отвечает на повтор или на этап, который уже не нужен, – повторять не надо. |
401 | Адрес изменён или от другой доставки, тело не JSON или неизвестный status. Повтор не поможет – проверьте запрос. |
Нет ответа или код 5xx – повторите запрос чуть позже.
curl -X POST "https://shuqr.ru/api/delivery/webhook/webhook?delivery=cmhf2k9x80001qa7z&sig=5e1c0f3a9b2d4e6f8a7b9c0d1e2f3a4b" \
-H "Content-Type: application/json" \
-d '{"status": "picked_up", "id": "A-1042"}'Как проверить#
- Отдельной тестовой среды у Shuqr пока нет. Проверяют подключение вместе с заведением, которое хочет работать с вами.
- Ваш адрес должен быть доступен из интернета по HTTPS. Чтобы проверить сервер на своём компьютере, используйте туннель, например ngrok или Cloudflare Tunnel.
- Записывайте в журнал каждый входящий запрос целиком: заголовки и сырое тело. Так проще разобраться, если подпись не сходится.
- Подпись можно проверить без Shuqr: подпишите тестовое тело сами и отправьте себе запрос, как в примере ниже.
BODY='{"event":"ping"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SHUQR_SECRET" | sed 's/^.* //')
curl -X POST "https://your-service.example.com/shuqr" \
-H "Content-Type: application/json" \
-H "X-Shuqr-Event: ping" \
-H "X-Shuqr-Signature: sha256=$SIG" \
-d "$BODY"Затем попросите заведение оформить тестовый заказ с доставкой и оплатить его. Вы получите delivery.create и сможете пройти все этапы. Отмена заказа в Shuqr пришлёт delivery.cancel.
Вопросы#
Если что-то непонятно или не работает, напишите нам или на support@shuqr.ru. Укажите адрес сервера и время запроса – так мы быстрее найдём его в журнале.