Службы доставки: API для подключения

Эта статья – для разработчика курьерской службы. Прочитав её, вы сможете принимать заказы заведений, которые работают на Shuqr, и сообщать им, где сейчас курьер.

Что это#

Shuqr – система для кафе и ресторанов: касса, кухня, онлайн-заказы. Заведение может подключить вашу службу у себя в настройках. Тогда Shuqr сам отправляет вам оплаченные заказы на доставку, а вы присылаете обратно этапы: курьер назначен, забрал заказ, доставил.

Договор с Shuqr не нужен. Условия, цены и расчёты вы обсуждаете напрямую с заведением.

Как это работает#

  1. Заведение подключает вашу службу у себя в настройках и нажимает «Проверить подключение» – Shuqr присылает ping.
  2. Когда пора везти заказ, Shuqr присылает delivery.create. В течение 10 секунд вы отвечаете своим номером заявки id.
  3. Курьер движется – вы отправляете этапы на callbackUrl из заказа: назначен, едет в заведение, забрал заказ.
  4. Этап delivered закрывает заказ у заведения: он становится «Выдан». По пути кассир и гость видят курьера и текущий этап.
  5. Пока курьер не забрал заказ, заведение может его отменить. Тогда придёт delivery.cancel.

Как проходит подключение#

  1. Вы даёте заведению адрес своего сервера (только https://) и ключ подписи – длинную случайную строку, которую придумываете сами. У каждого заведения ключ свой.
  2. Заведение открывает в Shuqr «Настройки → Доставка», в блоке «Кто везёт» добавляет «Другая служба по API» и вводит ваш адрес, ключ и цену доставки – её платит гость. Время доставки для гостя можно указать по желанию.
  3. Когда заказ оплачен и пора везти, 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-Signaturesha256= и подпись тела в hex. Как проверять – в разделе «Проверка подписи». Если заведение не ввело ключ, заголовка не будет – такой запрос лучше отклонить.

ping – проверка связи

Когда заведение нажимает «Проверить подключение» в настройках, Shuqr присылает ping с подписью. Ответьте любым кодом 2xx – заведение увидит «Подключение работает». Неверная подпись – ответьте 401, и заведение увидит, что ключ нужно проверить.

JSON
{ "event": "ping" }

delivery.create – новый заказ

Приходит, когда пора везти оплаченный заказ. Момент выбирает заведение: сразу после того, как заказ принят, когда кухня начала готовить, когда заказ готов, или вручную – кассир нажимает кнопку. Поэтому курьеру иногда придётся подождать в заведении. Номер orderNumber поможет забрать нужный заказ.

JSON
{
  "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 считает, что вы ищете курьера.

JSON
{ "id": "A-1042", "status": "searching" }

Если тот же заказ передадут ещё раз (например, после отказа), придёт новый deliveryId – это новая заявка. Повтор с тем же deliveryId – тот же заказ: верните уже созданную заявку.

delivery.cancel – отмена

Приходит, когда заведение отменило заказ или вернуло гостю деньги, а также когда вы слишком долго ищете курьера (срок задаёт заведение). В поле id – ваш номер заявки из ответа на delivery.create.

JSON
{ "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 и ничего не делайте с заказом.
Node.js (без библиотек, встроенный модуль http)
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 нужен всегда.

JSON
Пример тела
{
  "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. Дальше решает кассир.
JSON
1. Гость отказался, курьер везёт заказ обратно
{ "status": "returning", "id": "A-1042", "reason": "guestRefused" }
JSON
2. Курьер вернул заказ в заведение
{ "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: подпишите тестовое тело сами и отправьте себе запрос, как в примере ниже.
Shell
Тестовый запрос с подписью (bash, openssl, curl)
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. Укажите адрес сервера и время запроса – так мы быстрее найдём его в журнале.