Asosiy kontentga o'tish
MilliySavdo

Webhooklar

Buyurtma hodisalarini serveringizga real vaqtda oling: eventlar, payload, Standard Webhooks imzosini tekshirish (Python, Node.js, PHP), qayta yuborish va dedup.

Sozlash#

  1. Kabinet → Webhooklar da HTTPS endpoint qo'shing, rejim (test/live) va hodisalarni tanlang.
  2. Ko'rsatilgan imzo kalitini (whsec_…) serveringizda saqlang — u faqat bir marta ko'rsatiladi (keyin «Kalit» tugmasi bilan qayta ko'rish mumkin).
  3. Imzoni tekshiruvchi handler yozing va Test yuborish tugmasi bilan sinang (webhook.test hodisasi).

Faqat ommaviy HTTPS

URL https:// bilan boshlanishi va ommaviy hostga ishora qilishi kerak. localhost, xususiy IP (10.x, 192.168.x…), metadata manzillari bloklanadi (SSRF himoyasi). Redirectlar kuzatilmaydi. Lokal sinov uchun ngrok yoki Cloudflare Tunnel kabi vositalardan foydalaning.

Hodisalar#

EventQachon
order.createdBuyurtma yaratildi, raqam ajratildi
order.sms_receivedSMS kod keldi (data.order.sms_code)
order.cancelledBuyurtma bekor qilindi, pul qaytdi
order.expired20 daqiqada SMS kelmadi, pul qaytdi
order.failedProvayder xatosi, pul qaytdi
webhook.testKabinetdagi «Test yuborish» tugmasi

Payload#

Body — JSON. data.order obyekti GET /v1/orders/{id} javobi bilan bir xil.

Headerlar
POST /webhooks/milliysavdo HTTP/1.1
Content-Type: application/json
webhook-id: evt_3f9a2c7d1e5b8a0c4d6e2f1a
webhook-timestamp: 1792116832
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
Body
{
  "id": "evt_3f9a2c7d1e5b8a0c4d6e2f1a",
  "object": "event",
  "type": "order.sms_received",
  "mode": "test",
  "api_version": "2026-10-16",
  "created_at": "2026-10-16T02:13:52.502316+00:00",
  "data": {
    "order": {
      "id": "ord_test_14",
      "object": "order",
      "mode": "test",
      "status": "sms_received",
      "phone": "+1888582997",
      "sms_code": "636841",
      "country": {
        "id": 6,
        "name": "United States",
        "iso_code": null
      },
      "service": {
        "code": "907",
        "title": "Telegram"
      },
      "price": {
        "amount": "3842.44",
        "amount_cents": 384244,
        "retail_amount": "4269.38",
        "retail_amount_cents": 426938,
        "discount_percent": "10.00",
        "currency": "UZS"
      },
      "created_at": "2026-10-16T02:13:52.251868Z",
      "expires_at": "2026-10-16T02:33:52.251868Z",
      "completed_at": "2026-10-16T02:13:52.301158Z"
    }
  }
}

Imzoni tekshirish#

MilliySavdo Standard Webhooks spetsifikatsiyasidan foydalanadi:

  1. Imzolanadigan satr: {webhook-id}.{webhook-timestamp}.{xom body}.
  2. Kalit: whsec_ prefiksidan keyingi qismni base64-decode qilingan baytlar.
  3. Imzo: base64(HMAC-SHA256(kalit, satr)), headerda v1,<imzo>.
  4. Rotatsiya paytida headerda bo'sh joy bilan ajratilgan bir nechta imzo bo'ladi — biri mos kelsa yetarli.
  5. |hozir − webhook-timestamp| > 300 soniya bo'lsa, rad eting (replay himoyasi).
  6. Solishtirishni constant-time funksiya bilan bajaring.

Xom body

Imzo o'zgartirilmagan body baytlari ustida hisoblanadi. JSON ni parse qilib, qayta serializatsiya qilsangiz, imzo mos kelmaydi.
import base64
import hashlib
import hmac
import json
import time

TOLERANCE_SECONDS = 300


def verify_webhook(secret: str, headers: dict, raw_body: bytes, now: int | None = None) -> dict:
    """secret: "whsec_..." ; headers: kichik harfli kalitlar ; raw_body: xom bayt."""
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signature_header = headers.get("webhook-signature")
    if not msg_id or not timestamp or not signature_header:
        raise ValueError("Imzo headerlari yo'q")

    now = int(time.time()) if now is None else now
    if abs(now - int(timestamp)) > TOLERANCE_SECONDS:
        raise ValueError("Timestamp juda eski yoki kelajakda")

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = hmac.new(key, signed, hashlib.sha256).digest()

    for part in signature_header.split(" "):
        version, _, sig = part.partition(",")
        if version == "v1" and hmac.compare_digest(base64.b64decode(sig), expected):
            return json.loads(raw_body)
    raise ValueError("Imzo mos emas")


# Flask misoli:
# @app.post("/webhooks/milliysavdo")
# def milliysavdo_webhook():
#     headers = {k.lower(): v for k, v in request.headers.items()}
#     event = verify_webhook(os.environ["MILLIYSAVDO_WEBHOOK_SECRET"], headers, request.get_data())
#     ...
#     return "", 200

Express misoli#

server.js
const express = require("express");
const { verifyWebhook } = require("./verify-webhook");

const app = express();
const seen = new Set(); // ishlab chiqarishda: Redis / DB (webhook-id bo'yicha dedup)

// MUHIM: express.raw — imzo xom body ustida hisoblanadi
app.post("/webhooks/milliysavdo", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = verifyWebhook(process.env.MILLIYSAVDO_WEBHOOK_SECRET, req.headers, req.body);
  } catch (err) {
    return res.status(400).send(err.message);
  }
  if (seen.has(req.headers["webhook-id"])) return res.sendStatus(200); // takror
  seen.add(req.headers["webhook-id"]);

  if (event.type === "order.sms_received") {
    console.log("SMS kod:", event.data.order.sms_code);
  }
  res.sendStatus(200); // 10 soniya ichida 2xx qaytaring
});

app.listen(3000);

Javob berish va qayta yuborish#

  • 10 soniya ichida istalgan 2xx qaytaring. Og'ir ishni navbatga qo'ying va darhol javob bering.
  • Boshqa status, timeout yoki tarmoq xatosi — qayta yuboriladi: jami 8 tagacha urinish, oraliqlar 10s, 1m, 5m, 30m, 2h, 6h, 12h.
  • Barcha urinishlar muvaffaqiyatsiz bo'lsa, yetkazish dead holatiga o'tadi — kabinetdan qo'lda qayta yuborish mumkin.
  • 25 ta ketma-ket muvaffaqiyatsiz yetkazishdan keyin endpoint avtomatik o'chiriladi; tuzatib, kabinetda qayta yoqing.

Idempotentlik (dublikatlar)#

Bir xil hodisa bir necha marta kelishi mumkin (masalan, javobingiz yo'lda yo'qolsa). webhook-id qayta urinishlarda o'zgarmaydi — uni bazada saqlang va takrorini o'tkazib yuboring. Tartib kafolatlanmaydi: holatni har doim data.order.status bo'yicha yangilang yoki shubha bo'lsa GET /v1/orders/{id} bilan tekshiring.

Kalit rotatsiyasi#

Kabinetda «Rotatsiya» yangi whsec_ kalit yaratadi. O'tish davrida (0–72 soat) har bir so'rov ikki kalit bilan imzolanadi, shuning uchun yangi kalitni bemalol deploy qilasiz. Yuqoridagi kodlar bir nechta imzoni qo'llab-quvvatlaydi.

Sinash#

  • Kabinetdagi Test yuborish — darhol webhook.test yuboradi va HTTP kod, kechikishni ko'rsatadi (daqiqasiga 5 tagacha).
  • Test kalit bilan sandbox buyurtma yarating — test endpointlarga haqiqiy order.* hodisalari keladi.
  • «Yetkazish tarixi» bo'limida har bir urinishning status kodi, xatosi va javob parchasini ko'rasiz.