Getting started
Build and test on the sandbox first. You receive your live key after the sign-off tests pass.
Test keys only work on the sandbox, and live keys only work on live. Send your key on every request:
Authorization: Bearer hylo_test_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- Keep the key on your server only. Never put it in an app or a web page.
- If a key may have leaked, tell HYLO. We revoke it within a minute and issue a new one.
- Limit: 300 requests per minute per key. Above that you get
429; wait and retry.
Errors
Every error has the same shape. Include the requestId when you report a problem.
{ "error": { "code": "invalid_request", "message": "prepMinutes must be a whole number from 1 to 120.", "requestId": "a1b2c3" } }| HTTP | Meaning |
|---|---|
| 400 | Something in your request is wrong. Fix it before retrying. |
| 401 | The key is missing, wrong, revoked, or for the other environment. |
| 404 | No such outlet or order, or it isn't one of yours. |
| 409 | The order can't take this action now, for example it was already handled on the HYLO Partner app. |
| 422 | The menu has errors, listed in details. Nothing was changed. |
| 429 | Too many requests. |
| 5xx | A problem on HYLO's side. Retry after a few seconds. |
Outlets
HYLO links your outlets to your account. Use HYLO's outlet id in every call.
GET/outlets
{ "outlets": [ { "id": "abc123", "name": "Your Kitchen — Manjeri", "open": true, "selfDelivery": true,
"menu": { "lastPushAt": "2026-11-01T06:30:00.000Z", "items": 84 } } ] }GET/outlets/{outletId}
Returns the same object for a single outlet.
POST/outlets/{outletId}/status
Open or close the outlet for orders. until and reason are optional. Without until, the outlet stays closed until you send { "open": true }.
{ "open": false, "until": "2026-11-01T18:00:00+05:30", "reason": "Kitchen maintenance" }Stock
POST/outlets/{outletId}/stock
{ "type": "item", "ids": ["b1", "d1"], "inStock": false, "until": "2026-11-01T22:00:00+05:30" }type:item,sizeoraddon. Use your size or option ids with the matching type.untilis optional and only used withinStock: false. The item comes back on its own at that time.
Response
{ "ok": true, "updatedItems": 2, "notFound": [] }Webhooks
Give HYLO one https address. HYLO sends a JSON POST to it for every change to your orders.
| Event | When |
|---|---|
order.placed | A new order to prepare. A scheduled order arrives when it's due. |
order.accepted | Accepted, by you or on the HYLO Partner app |
order.ready | Food marked ready |
order.rider_assigned | A rider took the order. For your own delivery, this is your delivery staff member, with name and phone. |
order.picked_up | The rider picked up the food |
order.delivered | Delivered to the customer |
order.rejected | Rejected before it was accepted |
order.cancelled | Cancelled after it was accepted, by HYLO support |
ping | A test event sent from HYLO |
{
"id": "83xzotNylGYcY1TkmFSl_placed",
"type": "order.placed",
"apiVersion": "v1",
"createdAt": "2026-11-01T07:21:05.120Z",
"data": { "order": { /* the order object */ } }
}Your server must
- Check the signature and refuse the event if it doesn't match.
- Answer
2xxwithin 8 seconds. Do slow work after you answer. - Ignore duplicates using the
Hylo-Event-Idheader (the same asidin the body), because the same event can arrive twice. - Expect events out of order now and then. Each event carries the full current order, and
order.updatedAttells you which copy is newest.
If your server doesn't answer 2xx, HYLO retries after 1, 2, 5, 10 and 30 minutes, then after 1, 2, 4 and 8 hours. You can also fetch orders at any time with GET /orders.
Headers
Hylo-Event-Id: 83xzotNylGYcY1TkmFSl_placed Hylo-Event-Type: order.placed Hylo-Signature: t=1761981665,v1=5d0c…
Checking the signature
The signature is an HMAC-SHA256 of <t>.<raw body> using your webhook secret (whsec_…), which HYLO gives you separately from the API key.
const crypto = require('crypto'); // rawBody = the request body exactly as received (a string, before JSON.parse) function isFromHylo(rawBody, signatureHeader, webhookSecret) { const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('='))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes = replay const expected = crypto.createHmac('sha256', webhookSecret).update(`${t}.${rawBody}`).digest('hex'); return !!parts.v1 && expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); }
The order object
{
"id": "83xzotNylGYcY1TkmFSl",
"shortId": "83XZOTNY",
"outletId": "abc123",
"status": "placed",
"placedAt": "2026-11-01T07:21:04.000Z",
"scheduledFor": null,
"prepMinutes": null,
"updatedAt": "2026-11-01T07:21:04.000Z",
"customer": { "firstName": "Anil", "area": "Manjeri" },
"items": [
{
"line": 1, "itemId": "b1", "name": "Chicken Biryani", "quantity": 2,
"size": { "id": "b1-full", "name": "Full" },
"addons": [ { "groupId": "g-extra", "group": "Extras", "id": "raita", "name": "Raita", "price": 20 } ],
"unitPrice": 280, "unitPriceBeforeOffer": 280, "lineTotal": 560
}
],
"note": "Less spicy please",
"cutlery": false,
"payment": { "method": "cash", "cashToCollect": 688.25 },
"amounts": { "currency": "INR", "items": 560, "restaurantDiscount": 0, "packing": 0, "delivery": 30, "total": 590, "payout": 512.4 },
"delivery": { "by": "restaurant", "rider": null },
"cancelReason": null
}| Field | Meaning |
|---|---|
status | scheduled, placed, accepted, ready, picked_up, delivered, rejected or cancelled |
items[].unitPrice | Your own price for one unit, including size and add-ons, after your own item offer. Ids are your ids from the menu push. |
amounts.total | The order's value at your prices, before GST: items − your share of any coupon + packing + your delivery fee. Discounts paid by HYLO never reduce it. |
amounts.payout | What HYLO pays you for this order, after commission. |
payment.cashToCollect | Cash your delivery staff collect from the customer, only on cash orders with your own delivery. Otherwise 0. |
customer | First name and area only. Your delivery staff see the address and phone number in the HYLO Rider app. |
Order actions
GET/orders?outletId=…&since=…&limit=…
Orders placed since since, oldest first. since is an ISO time at most 7 days ago, and defaults to the last 24 hours. limit is at most 100; hasMore: true means call again with a later since. Use this to catch up after downtime.
GET/orders/{orderId}
Returns one order.
POST/orders/{orderId}/accept
{ "prepMinutes": 20 }prepMinutes is optional (1–120). Calling accept twice is safe, and the second call answers "alreadyDone": true.
POST/orders/{orderId}/ready
Marks the food ready. If the order wasn't accepted yet, it's accepted first.
POST/orders/{orderId}/reject
{ "reason": "out_of_stock", "note": "", "outOfStockItemIds": ["d1"] }reason:out_of_stock,too_busy,closingorother.otherneeds anote.- Only a new order (
placed) can be rejected. To cancel an accepted order, call HYLO support. - Items listed in
outOfStockItemIdsare switched off until midnight. - The customer is refunded automatically.
409 already_handled, and the message gives the order's current status.Going live
On the sandbox, with your test outlet, show each of these working end to end. HYLO then issues your live key and switches on one outlet first.
- A menu push with sizes and add-ons, then the same push again, with nothing changed.
- An item switched out of stock and back, and the outlet closed and reopened.
- An order received by webhook with its signature checked, accepted with a prep time, then marked ready.
- An order rejected as out of stock.
- Your server down for a few minutes, then catching up from the retries and from
GET /orders. - Your delivery staff delivering a test order with the HYLO Rider app.
Data & security
- Use customer data from HYLO only to prepare and deliver that order. Don't use it for marketing, and don't share it.
- Store keys and webhook secrets only on your server, and tell HYLO at once if one may have leaked.
- Use of the API is covered by the HYLO Partner API Agreement, signed before your live key is issued.