keel

API · Versiya v1

Keel API — dasturchilar uchun

Restoraningizni CRM, buxgalteriya, AI xizmatlar va o'z ilovangiz bilan ulang: menyu, buyurtmalar, pul harakati va real vaqtdagi webhook'lar.

Umumiy

Keel'dagi har bir restoran o'z serverida, o'z domenida ishlaydi. API ham restoranning o'z domenida:

https://<restoran-domeni>/api/open/v1

Aniq 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 /ping bilan tekshiring:
curl
curl https://<restoran-domeni>/api/open/v1/ping \
  -H "Authorization: Bearer keel_7fd73b99c1cd8ed9…"
200 OK
{
  "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:

RuxsatNima 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 day maydonini ishlating — u restoranning o'z kalendar kuni. occurredAt ni 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 null emas.

Xatolar

4xx / 5xx
{
  "error": {
    "code": "insufficient_scope",
    "message": "this API key does not have the orders:read scope"
  }
}
HTTPcode
400invalid_request
401unauthorized
403insufficient_scope
404not_found
500internal_error

Dasturda code bo'yicha qaror qiling; message odam uchun va o'zgarishi mumkin.

Buyurtmalar

GET /orders — eng yangisi birinchi. Filtrlar:

Parametr
branchIdfaqat shu filial
statuspending, confirmed, preparing, on_the_way, delivered, cancelled
createdFrom, createdToRFC 3339; boshi kiradi, oxiri kirmaydi
limit1–100, standart 50
cursoroldingi 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:

Order
{
  "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).
  • address faqat manzil bo'lsa keladi; cancelReason, tableNumber, scheduledAt bo'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, tomajburiy; restoran kunlari (2026-09-01), ikkalasi ham kiradi, yoki RFC 3339. Ko'pi bilan 31 kun.
branchIdfaqat 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.

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

MoneyEntry
{
  "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" }
}
  • amount doim musbat; ishorani direction beradi (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 (faqat revenue): type, channel, subtotal, discountTotal, pointsSpent, deliveryFee, serviceCharge.
  • paid (faqat xarid): tovar kelgan kuni yoziladi; yetkazib beruvchiga to'langanda paidAt.

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 — id bo'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_changed webhook'ini ishlating.
  • id barqaror: 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, at ga 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; qarorni kind bo'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.

HodisaQachon
order.createdbuyurtma berildi — sayt, ilova, operator, kassa, Uzum Tezkor
order.status_changedbuyurtma holati o'zgardi
money.day_changedkunning pul raqamlari o'zgardi — pastga qarang
pingega panelda «Sinab ko'rish» ni bosdi
HTTP
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.order navbatga qo'yilgan paytdagi surat, shuning uchun ketma-ket tez hodisalarda data.order.status keyingisi bo'lishi mumkin.
  • createdAt — o'zgarish bo'lgan vaqt. Oflayn ishlagan kassa savdoni keyin yuboradi, qilingan vaqti bilan.
  • 10 soniya ichida istalgan 2xx bilan 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.

Test vektori
secret = "secret"
t      = 1
body   = {}
→ t=1,v1=1122767b193110cfec322b6f199b599edbf608ed087f2d27afb0b97d99523908
Node.js
import 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));
}
Python
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)
PHP
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.

Keel API — dasturchilar uchun | Keel