kokiwa

Webhook

付款入帳或支付單狀態變化時,kokiwa 向你的 https 地址發送 POST 請求。在商戶後台的「回調通知」頁管理接收地址。

想直接複製可用的程式碼? Node.js、Python、PHP、Go、Java 接入範例

請求格式

{
  "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", "...": "..." }
  }
}
請求頭說明
Kokiwa-Signature簽名,格式 t=時間戳,v1=簽名。
Kokiwa-Event-Id事件 ID,與請求體的 id 相同。
Kokiwa-Delivery-Attempt第幾次投遞,從 1 開始。

data.deposit 是入帳對象。data.payment_intent 與查詢支付單返回的對象相同,只是不含 checkout_url 和 livemode;livemode 在事件最外層。

事件類型

type發送時機data
payment_intent.paid支付單已全額收到payment_intent、deposit
payment_intent.partially_paid支付單收到了一部分payment_intent、deposit
payment_intent.expired支付單過期未付款payment_intent
payment_intent.paid_late支付單過期後才收到款項payment_intent、deposit
deposit.confirmed一筆入帳已在鏈上確認,來自支付單或充值地址deposit
deposit.settled一筆入帳已到你的收款地址deposit
webhook.test你在後台發送的測試事件message

錢包付款在付款交易裡完成結算,deposit.confirmed 和 deposit.settled 同時發送。轉帳到地址先發 deposit.confirmed,歸集後才發 deposit.settled;收到 confirmed 就可以給用戶入帳,不用等 settled。

校驗簽名

每個接收地址有自己的簽名密鑰(whsec_…),只在建立時顯示一次。

  1. 從 Kokiwa-Signature 取出 t 和 v1。
  2. 用密鑰對字符串 {t}.{原始請求體} 計算 HMAC-SHA256,轉成小寫十六進制,與 v1 做常數時間比較。
  3. 拒絕 t 與當前時間相差超過 5 分鐘的請求。

必須用原始請求體計算。先解析成 JSON 再序列化,字節會變,簽名就對不上。

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)

響應與重試

  • 在 10 秒內返回任意 2xx,耗時的處理放到後台隊列。
  • 非 2xx、超時或連接失敗會重試,共 11 次,間隔為 30 秒、2 分鐘、10 分鐘、30 分鐘、1、2、4、8、12、24、20 小時,約 3 天後停止。
  • 同一事件可能送達多次,事件之間不保證順序。按事件 id 去重;需要最新狀態時調用 GET /v1/payment_intents/{id}。
  • 後台「回調通知」頁可以查看每次投遞的結果,並手動重新投遞。
  • 重試全部失敗後,用 GET /v1/deposits 補齊漏掉的入帳。

Telegram 通知

在後台「設置」頁綁定 @KoKiWaPayBot,每筆入帳都會收到消息。它只用於提醒,不能代替 Webhook。

Connect a wallet with WalletConnect