Webhooklar
Buyurtma hodisalarini serveringizga real vaqtda oling: eventlar, payload, Standard Webhooks imzosini tekshirish (Python, Node.js, PHP), qayta yuborish va dedup.
Sozlash#
- Kabinet → Webhooklar da HTTPS endpoint qo'shing, rejim (test/live) va hodisalarni tanlang.
- Ko'rsatilgan imzo kalitini (
whsec_…) serveringizda saqlang — u faqat bir marta ko'rsatiladi (keyin «Kalit» tugmasi bilan qayta ko'rish mumkin). - Imzoni tekshiruvchi handler yozing va Test yuborish tugmasi bilan sinang (
webhook.testhodisasi).
Faqat ommaviy HTTPS
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#
| Event | Qachon |
|---|---|
order.created | Buyurtma yaratildi, raqam ajratildi |
order.sms_received | SMS kod keldi (data.order.sms_code) |
order.cancelled | Buyurtma bekor qilindi, pul qaytdi |
order.expired | 20 daqiqada SMS kelmadi, pul qaytdi |
order.failed | Provayder xatosi, pul qaytdi |
webhook.test | Kabinetdagi «Test yuborish» tugmasi |
Payload#
Body — JSON. data.order obyekti GET /v1/orders/{id} javobi bilan bir xil.
POST /webhooks/milliysavdo HTTP/1.1
Content-Type: application/json
webhook-id: evt_3f9a2c7d1e5b8a0c4d6e2f1a
webhook-timestamp: 1792116832
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4={
"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:
- Imzolanadigan satr:
{webhook-id}.{webhook-timestamp}.{xom body}. - Kalit:
whsec_prefiksidan keyingi qismni base64-decode qilingan baytlar. - Imzo:
base64(HMAC-SHA256(kalit, satr)), headerdav1,<imzo>. - Rotatsiya paytida headerda bo'sh joy bilan ajratilgan bir nechta imzo bo'ladi — biri mos kelsa yetarli.
|hozir − webhook-timestamp| > 300soniya bo'lsa, rad eting (replay himoyasi).- Solishtirishni constant-time funksiya bilan bajaring.
Xom body
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 "", 200Express misoli#
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
2xxqaytaring. 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.testyuboradi 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.