kokiwa

Вебхуки

Когда платёж зачислен или меняется его статус, kokiwa отправляет POST-запрос на ваш https-адрес. Адреса управляются на странице «Вебхуки» в кабинете мерчанта.

Формат запроса

{
  "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-IdID события, совпадает с 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. Возьмите t и v1 из Kokiwa-Signature.
  2. Вычислите HMAC-SHA256 строки {t}.{исходное тело запроса} с секретом, переведите в шестнадцатеричный вид в нижнем регистре и сравните с 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)

Ответы и повторы

  • Верните любой 2xx за 10 секунд, а долгую работу передайте в фоновую очередь.
  • Ответы не 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 на странице «Настройки», чтобы получать сообщение о каждом поступлении. Это только напоминание, оно не заменяет вебхуки.

Connect a wallet with WalletConnect