kokiwa

Webhook

支払いの入金や支払いステータスの変化時に、kokiwa はあなたの https URL に POST リクエストを送ります。受信 URL はマーチャント管理画面の「Webhook」ページで管理します。

そのまま使えるコードが必要ですか? 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 を待つ必要はありません。

署名を検証する

受信 URL ごとに署名シークレット(whsec_…)があり、作成時に一度だけ表示されます。

  1. Kokiwa-Signature から t と v1 を取り出します。
  2. シークレットで文字列 {t}.{生のリクエスト本文} の HMAC-SHA256 を計算して小文字の 16 進数にし、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} で取得してください。
  • 「Webhook」ページで各配信の結果を確認し、手動で再配信できます。
  • 再試行がすべて失敗したら、GET /v1/deposits で漏れた入金を補ってください。

Telegram 通知

「設定」ページで @KoKiWaPayBot を連携すると、入金ごとにメッセージが届きます。通知専用で、Webhook の代わりにはなりません。

Connect a wallet with WalletConnect