Umumiy
Keel'dagi har bir restoran o'z serverida, o'z domenida ishlaydi. API ham restoranning o'z domenida:
https://<restoran-domeni>/api/open/v1Aniq manzilni restoran egasi panelda ko'radi: Sozlamalar → Integratsiyalar → API va webhook'lar. Bir restoranning kaliti faqat o'sha restoranni ochadi.
API ikki yo'nalishda ishlaydi: sizning dasturingiz bizdan so'raydi (API kalit bilan), va biz buyurtma yoki pul o'zgarganda sizga o'zimiz xabar beramiz (webhook).
/v1 — va'da. Bu yerdagi yo'l, maydon, header va xato kodlari o'zgarmaydi; o'zgarish kerak bo'lsa, /v2 yonida paydo bo'ladi. Obyektlarga yangi maydonlar qo'shilishi mumkin — bilmaganingizni e'tiborsiz qoldiring.
Tez boshlash
- Restoran egasi panelda kalit yaratadi va unga kerakli ruxsatni beradi. Kalit
keel_bilan boshlanadi va faqat bir marta ko'rsatiladi. - Ega sizga ikki narsa beradi: API manzili va kalit.
- Kalit to'g'riligini
/pingbilan tekshiring:
curl https://<restoran-domeni>/api/open/v1/ping \
-H "Authorization: Bearer keel_7fd73b99c1cd8ed9…"{
"restaurant": "Rayhon",
"apiVersion": "v1",
"key": { "name": "Finze", "prefix": "keel_7fd73b", "scopes": ["finance:read"] }
}Autentifikatsiya va ruxsatlar
Har so'rovda Authorization: Bearer keel_… header'i. Kalitda ruxsatlar bor:
| Ruxsat | Nima beradi |
|---|---|
menu:read | /branches/{id}/menu — menyu va nima hozir sotuvda |
orders:read | /orders, /orders/{ref} — buyurtmalar, mijoz ismi va telefoni bilan |
finance:read | /money, /money/daily, /balances, /fiscal — pul, mijoz ma'lumotisiz |
/ping va /branches uchun istalgan faol kalit yetadi. Bekor qilingan kalit keyingi so'rovdayoq ishlamaydi.
Kalit faqat serverdan ishlatiladi. Uni brauzer yoki mobil ilovaga qo'ymang — u yerda uni har kim o'qiy oladi. Brauzerdan cross-origin so'rovlar ruxsat etilmaydi.
Cheklov: bir IP manzildan daqiqasiga 120 so'rov (undan keyin 429).
Qoidalar
- Pul — so'mda butun son (UZS). Tiyin va kasr yo'q.
- Vaqt — RFC 3339, UTC (
2026-09-24T12:30:15.69Z). - Kun kerak bo'lsa
daymaydonini ishlating — u restoranning o'z kalendar kuni.occurredAtni kesib olish Toshkent vaqti bilan 19:00 dan keyingi hammasini oldingi kunga qo'yadi. - Id — 24 belgili hex. Yo'q id — bo'sh qator yoki umuman maydon yo'q, hech qachon
"000000000000000000000000"emas. - Ro'yxat doim massiv, hech qachon
nullemas.
Xatolar
{
"error": {
"code": "insufficient_scope",
"message": "this API key does not have the orders:read scope"
}
}| HTTP | code |
|---|---|
| 400 | invalid_request |
| 401 | unauthorized |
| 403 | insufficient_scope |
| 404 | not_found |
| 500 | internal_error |
Dasturda code bo'yicha qaror qiling; message odam uchun va o'zgarishi mumkin.
Buyurtmalar
GET /orders — eng yangisi birinchi. Filtrlar:
| Parametr | |
|---|---|
branchId | faqat shu filial |
status | pending, confirmed, preparing, on_the_way, delivered, cancelled |
createdFrom, createdTo | RFC 3339; boshi kiradi, oxiri kirmaydi |
limit | 1–100, standart 50 |
cursor | oldingi sahifaning nextCursor i; oxirgi sahifada "" |
GET /orders?createdFrom=2026-09-01T00:00:00%2B05:00&limit=50
{ "orders": [ Order, … ], "nextCursor": "MTc1ODcw…" }GET /orders/{ref} — bitta buyurtma, raqami (QVUU-R97U) yoki id'si bo'yicha. Buyurtma obyekti:
{
"id": "6ab517d7c71997fe78741fed",
"number": "QVUU-R97U",
"branchId": "6ab51777ac108b6304170794",
"type": "pickup",
"channel": "web",
"status": "confirmed",
"customer": { "name": "Aziz", "phone": "+998901112233" },
"items": [
{ "menuItemId": "…", "name": "Osh (palov)", "price": 45000, "qty": 2,
"options": [ { "group": "Hajmi", "choice": "Katta", "priceDelta": 10000 } ] }
],
"subtotal": 90000, "discountTotal": 0, "deliveryFee": 0,
"serviceCharge": 0, "total": 90000,
"paymentMethod": "cash", "paymentStatus": "unpaid",
"statusHistory": [
{ "status": "pending", "at": "2026-09-24T12:30:15.69Z" },
{ "status": "confirmed", "at": "2026-09-24T12:30:15.727Z" }
],
"createdAt": "2026-09-24T12:30:15.69Z"
}type:delivery,pickup,dinein(zaldagi stol),uzum_tezkor(Uzum kuryeri olib ketadi).addressfaqat manzil bo'lsa keladi;cancelReason,tableNumber,scheduledAtbo'sh bo'lsa kelmaydi.price— bitta dona narxi, variantlar qo'shilgan.
Pul daftari
GET /money (finance:read) — restoranda bo'lgan har bir pul harakati, bitta ko'rinishda va allaqachon tasniflangan. Buxgalteriya xizmatlari uchun: summa, to'lov turi, yetkazib beruvchi va xodim — mijozning ismi yoki telefoni hech qachon yo'q.
| Parametr | |
|---|---|
from, to | majburiy; restoran kunlari (2026-09-01), ikkalasi ham kiradi, yoki RFC 3339. Ko'pi bilan 31 kun. |
branchId | faqat shu filial |
GET /money?from=2026-09-01&to=2026-09-30
{
"from": "2026-08-31T19:00:00Z", "to": "2026-09-30T19:00:00Z", "currency": "UZS",
"entries": [ MoneyEntry, … ],
"totals": {
"byClass": {
"revenue": { "in": 90000, "out": 0, "count": 1 },
"cost": { "in": 0, "out": 5000000, "count": 1 }
},
"pnlNet": -4910000
}
}Har pul foyda-zarar emas. Pul kassadan bankka o'tadi, bozorchiga avans beriladi, agregator restoran allaqachon topgan pulni o'tkazadi. Ularni tushum yoki xarajat deb qo'shish bir pulni ikki marta sanaydi. Shuning uchun har yozuv o'zi nima ekanini aytadi: class va pnl.
class | pnl | Nima |
|---|---|---|
revenue | ✅ | To'langan sotuv, sotilgan kunida. Tafsiloti sale da. |
refund | ✅ | Qaytarilgan pul, qaytarilgan kunida. |
cost | ✅ | Yetkazib beruvchidan xarid, xarajat (ijara, kommunal, soliq), tashqi yetkazish xizmati. |
payroll | ✅ | To'langan ish haqi — xodimlar va kuryerlar. |
commission | ✅ | Agregator yoki ekvayring ushlab qolgan pul. |
transfer | ❌ | Xuddi o'sha pul boshqa joyda: kassa → bank, agregator → bank, kuryer → kassa. Tushum emas. |
advance | ❌ | Xodimga xarid uchun berilgan naqd (podotchet). Xarajat u sotib olgan kirimda bo'ladi — u allaqachon `cost`. |
manual | ❌ | Kassa yoki seyfga qo'lda qo'yilgan/olingan naqd, kassirning o'z category si bilan. Ko'pincha kirim yoki xarajat hujjatidagi o'sha pul. |
variance | ❌ | Smena yopilganda kassa ortiqcha (in) yoki kam (out) chiqdi. Yo'qolgan pul, sarflangan emas. |
Restoran panelidagi moliyaviy hisobot — pnl: true yozuvlarining yig'indisi, ya'ni pnlNet. Sizning davr raqamingiz undan farq qilsa — nimadir ikki marta sanalgan.
Chegirma va ballar alohida yozuv emas: pul harakatlanmagan. Ular sotuv ichida (sale.discountTotal, sale.pointsSpent); amount — haqiqatan olingan summa.
{
"id": "expense_6ab5235b666e66387034a534",
"source": "expense",
"class": "cost",
"pnl": true,
"direction": "out",
"amount": 5000000,
"occurredAt": "2026-09-23T19:00:00Z",
"day": "2026-09-24",
"branchId": "6ab52356666e66387034a523",
"method": "transfer",
"category": "ijara",
"ref": { "type": "expense", "id": "6ab5235b666e66387034a534" }
}amountdoim musbat; ishoranidirectionberadi (in/out, restoran tomonidan).source:sale,refund,delivery_service,purchase,expense,salary,courier_pay,payout,collection,advance,cash_entry,safe_entry,courier_settlement,shift_variance. Yangilari qo'shilishi mumkin — qarorni `class` bo'yicha qiling.- Ixtiyoriy:
method,methodName,category,counterparty,from/to(o'tkazmalar uchun: till, safe, bank, aggregator, courier, staff),note. sale(faqatrevenue):type,channel,subtotal,discountTotal,pointsSpent,deliveryFee,serviceCharge.paid(faqat xarid): tovar kelgan kuni yoziladi; yetkazib beruvchiga to'langandapaidAt.
Davr hech qachon yopilmaydi — qayta o'qing
Daftar so'ragan paytingizda restoran hujjatlaridan hisoblanadi. Hujjatlar tuzatiladi: ikki marta yozilgan xarajat o'chiriladi, kirim narxi tuzatiladi, qarz to'lanadi, pul bir haftadan keyin qaytariladi. Shuning uchun:
- Davrni qayta oling va saqlaganingizni almashtiring —
idbo'yicha moslang, yo'qolgan id'larni o'chiring. Qo'shib bormang. - Har kuni kamida oxirgi 7 kunni, oy yopilgach esa butun oldingi oyni qayta o'qing — yoki
money.day_changedwebhook'ini ishlating. idbarqaror: bir hujjat doim bir xil id beradi.
Kunlik yig'indilar
GET /money/daily (finance:read) — daftar kun va filial bo'yicha yig'ilgan. Xuddi /money yozuvlarining o'zi, shuning uchun ikkalasi hech qachon farq qilmaydi. Parametrlar /money niki, davr 93 kungacha (chorak).
GET /money/daily?from=2026-07-01&to=2026-09-30
{
"currency": "UZS",
"days": [
{ "day": "2026-09-24", "branchId": "6ab5…",
"byClass": {
"revenue": { "in": 4200000, "out": 0, "count": 61 },
"cost": { "in": 0, "out": 5000000, "count": 1 }
},
"revenueByMethod": { "cash": 2600000, "payme": 1600000 },
"pnlNet": -800000 }
]
}Pul qayerda — qoldiqlar
GET /balances (finance:read, ixtiyoriy branchId) — hozirgi holat. Egasining paneldagi «Pul qayerda» ekrani bilan bir xil raqamlar.
GET /balances
{
"asOf": "2026-09-24T13:29:00Z", "currency": "UZS", "branchId": "",
"cash": [
{ "kind": "safe", "name": "Seyf", "amount": 1200000, "counted": false },
{ "kind": "drawer", "name": "Kassa yashigi", "amount": 350000, "counted": false }
],
"cashTotal": 1550000,
"bank": [
{ "kind": "bank", "name": "Kapitalbank", "amount": 48000000,
"counted": true, "at": "2026-09-23T09:00:00Z" }
],
"bankTotal": 48000000,
"inTransit": [ { "kind": "rail", "name": "Uzum", "amount": 3100000, "counted": false } ],
"inTransitTotal": 3100000,
"cashLimit": 5000000, "overCashLimit": false,
"payables": { "suppliers": { "amount": 7400000, "count": 5 } },
"receivables": { "guestDebt": { "amount": 260000, "count": 3 } }
}Umumiy jami ataylab yo'q. Naqdni bugun kechqurun, bankdagini shu hafta, agregatordagini esa ular hal qilganda sarflash mumkin. Ularni qo'shish hech narsa haqida rost bo'lmagan raqam beradi.
counted: true— kimdir sanagan (bank ilovasidan o'qilgan qoldiq); eskiradi,atga qarang.counted: false— hujjatlardan yig'ilgan; hujjat yetishmasa noto'g'ri bo'ladi.- Bank — oxirgi sanalgan qoldiq, harakatlardan hisoblanmaydi: hisobga biz ko'rmaydigan pullar ham tushadi.
name— restoranning o'z so'zi; qarornikindbo'yicha qiling:safe,drawer,courier,advance,bank,rail.payables.suppliers— hali to'lanmagan kirimlar;receivables.guestDebt— hali to'lanmagan qarz sotuvlar.
Fiskal cheklar
GET /fiscal (finance:read, ≤31 kun) — davr sotuvlari bo'yicha soliq qo'mitasiga yuborilgan cheklar har holatda (oy yopilishidan oldin aynan kutilayotgan va xato berganlari quviladi) va shu davrda yopilgan smenalarning Z-hisobotlari.
GET /fiscal?from=2026-09-01&to=2026-09-30
{
"receipts": [
{ "orderId": "…", "orderNumber": "QVUU-R97U", "branchId": "…",
"kind": "sale", "total": 90000, "method": "cash",
"status": "filed", "provider": "multikassa",
"fiscalSign": "…", "receiptId": "…", "qrText": "…",
"filedAt": "…", "soldAt": "…" }
],
"zReports": [
{ "shiftId": "…", "branchId": "…", "number": "142",
"saleCash": 2600000, "saleCard": 1600000, "saleTotal": 4200000,
"saleCount": 61, "refundTotal": 0, "closedAt": "…" }
]
}kind — sale yoki refund; status — pending, filed yoki failed; error nega yuborilmaganini aytadi.
Webhook'lar
Ega panelda https:// manzilingizni qo'shadi va hodisalarni tanlaydi. U bir martalik imzo kalitini (whsec_…) oladi va sizga beradi.
| Hodisa | Qachon |
|---|---|
order.created | buyurtma berildi — sayt, ilova, operator, kassa, Uzum Tezkor |
order.status_changed | buyurtma holati o'zgardi |
money.day_changed | kunning pul raqamlari o'zgardi — pastga qarang |
ping | ega panelda «Sinab ko'rish» ni bosdi |
POST https://your-server.example/keel
Content-Type: application/json
User-Agent: Keel-Webhooks/1
Keel-Event: order.status_changed
Keel-Event-Id: evt_6ab517d7c71997fe78741fed_1
Keel-Delivery-Id: 6ab517d8c71997fe78741ff1
Keel-Signature: t=1758717015,v1=5f1c…
{
"id": "evt_6ab517d7c71997fe78741fed_1",
"type": "order.status_changed",
"createdAt": "2026-09-24T12:30:15.727Z",
"data": { "status": "confirmed", "previousStatus": "pending", "order": Order }
}data.status— shu hodisa haqidagi holat.data.ordernavbatga qo'yilgan paytdagi surat, shuning uchun ketma-ket tez hodisalardadata.order.statuskeyingisi bo'lishi mumkin.createdAt— o'zgarish bo'lgan vaqt. Oflayn ishlagan kassa savdoni keyin yuboradi, qilingan vaqti bilan.- 10 soniya ichida istalgan
2xxbilan javob bering, ishni keyin qiling. Boshqa status, timeout yoki redirect — xato; redirect kuzatilmaydi. - Xatodan keyin qayta urinish: 1 daq, 5 daq, 30 daq, 2 soat, 6 soat, 12 soat, 24 soat (8 urinish, taxminan ikki kun).
- Bir hodisa bir necha marta kelishi mumkin —
id(Keel-Event-Id) bo'yicha takrorni tashlang. - Hodisalar navbat bilan yuboriladi, lekin qayta urinish yangi hodisadan keyin kelishi mumkin — `createdAt` bo'yicha tartiblang, kelish tartibi bo'yicha emas.
money.day_changed
{
"id": "evt_money_2026-09-24_6ab5…_1f3a9c0e2b7d",
"type": "money.day_changed",
"createdAt": "2026-09-24T13:40:00Z",
"data": { "day": "2026-09-24", "branchId": "6ab5…", "pnlNet": -800000, "entries": 62 }
}Bu ishora, pul emas: shu kunni qayta o'qing (GET /money?from=<day>&to=<day>) va saqlaganingizni almashtiring. Kunning raqamini o'zgartiradigan har narsada keladi — yangi sotuv, tuzatilgan kirim, o'chirilgan xarajat — oxirgi 35 kun ichida, 10 daqiqada bir tekshiriladi. entries: 0 — kunning hamma yozuvi o'chirilgan.
Manzil qo'shilishidan oldingi kunlar uchun hech narsa yuborilmaydi — o'sha davrlarni bir marta o'zingiz o'qib oling.
Imzoni tekshirish
Keel-Signature: t=<unix seconds>,v1=<hex>
v1 = hex( HMAC-SHA256( secret, t + "." + raw_request_body ) )Xom body baytlari ishlatiladi — JSON'ni parse qilishdan oldin. Imzo mos kelmasa yoki t soatingizdan 5 daqiqadan ko'p farq qilsa — so'rovni rad eting. Vaqt imzo ichida bo'lgani uchun eski yetkazmani qayta yuborib bo'lmaydi.
secret = "secret"
t = 1
body = {}
→ t=1,v1=1122767b193110cfec322b6f199b599edbf608ed087f2d27afb0b97d99523908import crypto from "node:crypto";
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return parts.v1?.length === want.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(want));
}import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", 0))
if not t or abs(time.time() - t) > 300:
return False
want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts.get("v1", ""), want)function verify(string $secret, string $header, string $rawBody): bool {
parse_str(str_replace(',', '&', $header), $p);
$t = (int)($p['t'] ?? 0);
if (!$t || abs(time() - $t) > 300) return false;
$want = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($want, $p['v1'] ?? '');
}Ega imzo kalitini almashtirsa, navbatda turgan yetkazmalar yangisi bilan imzolanadi.