kokiwa

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.

Đị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", "...": "..." }
  }
}
HeaderMô tả
Kokiwa-SignatureChữ ký dạng t=thời_gian,v1=chữ_ký.
Kokiwa-Event-IdID sự kiện, giống id trong nội dung.
Kokiwa-Delivery-AttemptSố 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

typeKhi nào gửidata
payment_intent.paidThanh toán đã trả đủpayment_intent、deposit
payment_intent.partially_paidThanh toán đã nhận một phầnpayment_intent、deposit
payment_intent.expiredThanh toán hết hạn mà chưa trảpayment_intent
payment_intent.paid_lateTiền đến sau khi thanh toán hết hạnpayment_intent、deposit
deposit.confirmedKhoản nạp được xác nhận trên chuỗi, từ thanh toán hoặc địa chỉ nạpdeposit
deposit.settledKhoản nạp đã đến địa chỉ nhận tiềndeposit
webhook.testSự 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.

  1. Lấy t và v1 từ Kokiwa-Signature.
  2. 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ới v1 trong thời gian không đổi.
  3. Từ chối yêu cầu có t lệ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 id sự kiện và gọi GET /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.

Connect a wallet with WalletConnect