Webhook
kokiwa gửi yêu cầu POST đến URL https của bạn khi một thanh toán được ghi có hoặc đổi trạng thái. Quản lý URL trong trang Webhook của trang quản trị người bán.
Cần mã dùng được ngay? Ví dụ tích hợp bằng Node.js, Python, PHP, Go và Java
Định dạng yêu cầu
{
"id": "evt_3Fk9Qw2Lm7Rt5Xb1Nc8Vz4",
"type": "payment_intent.paid",
"created_at": "2026-11-02T08:41:07Z",
"livemode": true,
"data": {
"payment_intent": { "id": "pi_7Gm4Rk2Wp9Ls1Qv8Bd3Zt6", "object": "payment_intent", "status": "paid", "...": "..." },
"deposit": { "id": "dep_9Kx2Pa6Wn3Ty8Lq5Rb1Ms4", "object": "deposit", "status": "settled", "...": "..." }
}
}| Header | Mô tả |
|---|---|
Kokiwa-Signature | Chữ ký dạng t=thời_gian,v1=chữ_ký. |
Kokiwa-Event-Id | ID sự kiện, giống id trong nội dung. |
Kokiwa-Delivery-Attempt | Số lần gửi, bắt đầu từ 1. |
data.deposit là đối tượng khoản nạp. data.payment_intent là đối tượng trả về khi lấy thanh toán, không có checkout_url và livemode; livemode ở cấp cao nhất của sự kiện.
Loại sự kiện
| type | Khi nào gửi | data |
|---|---|---|
payment_intent.paid | Thanh toán đã trả đủ | payment_intent、deposit |
payment_intent.partially_paid | Thanh toán đã nhận một phần | payment_intent、deposit |
payment_intent.expired | Thanh toán hết hạn mà chưa trả | payment_intent |
payment_intent.paid_late | Tiền đến sau khi thanh toán hết hạn | payment_intent、deposit |
deposit.confirmed | Khoản nạp được xác nhận trên chuỗi, từ thanh toán hoặc địa chỉ nạp | deposit |
deposit.settled | Khoản nạp đã đến địa chỉ nhận tiền | deposit |
webhook.test | Sự kiện thử gửi từ trang quản trị | message |
Thanh toán bằng ví được kết toán ngay trong giao dịch, nên deposit.confirmed và deposit.settled đến cùng lúc. Khoản chuyển gửi deposit.confirmed trước, sau khi gom mới gửi deposit.settled. Hãy ghi có cho người dùng khi có confirmed, không cần đợi settled.
Xác minh chữ ký
Mỗi URL webhook có khóa bí mật ký riêng (whsec_…), chỉ hiển thị một lần khi tạo.
- Lấy
tvàv1từKokiwa-Signature. - Tính HMAC-SHA256 của chuỗi
{t}.{nội dung gốc}bằng khóa bí mật, mã hóa hex chữ thường và so sánh vớiv1trong thời gian không đổi. - Từ chối yêu cầu có
tlệch quá 5 phút so với hiện tại.
Hãy dùng nội dung gốc của yêu cầu. Phân tích JSON rồi tuần tự hóa lại sẽ đổi các byte và chữ ký không khớp.
Node.js
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.KOKIWA_WEBHOOK_SECRET;
app.post("/kokiwa/webhook", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Kokiwa-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(`${parts.t}.${req.body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const match = parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!fresh || !match) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
// deduplicate by event.id, process, then return 2xx quickly
res.sendStatus(200);
});Go
func verify(secret, body []byte, header string) bool {
var t, v1 string
for _, p := range strings.Split(header, ",") {
k, v, _ := strings.Cut(strings.TrimSpace(p), "=")
switch k {
case "t":
t = v
case "v1":
v1 = v
}
}
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return false
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(t + "."))
mac.Write(body)
return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(v1))
}Python
import hashlib, hmac, time
def verify(secret: bytes, body: bytes, header: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Phản hồi và thử lại
- Trả về bất kỳ 2xx nào trong 10 giây và chuyển việc chậm sang hàng đợi nền.
- Phản hồi không phải 2xx, hết thời gian chờ và lỗi kết nối được thử lại 11 lần: sau 30 giây, 2, 10, 30 phút, rồi 1, 2, 4, 8, 12, 24, 20 giờ. Việc gửi dừng sau khoảng 3 ngày.
- Một sự kiện có thể đến nhiều lần và không theo thứ tự. Hãy loại trùng theo
idsự kiện và gọiGET /v1/payment_intents/{id}để lấy trạng thái mới nhất. - Trang Webhook hiển thị kết quả mỗi lần gửi và cho phép gửi lại thủ công.
- Nếu mọi lần thử đều thất bại, dùng
GET /v1/depositsđể bù các khoản nạp bị sót.
Thông báo Telegram
Liên kết @KoKiWaPayBot trong trang Cài đặt để nhận tin nhắn cho mỗi khoản nạp. Đây chỉ là lời nhắc, không thay thế webhook.