kokiwa

Webhooks

O kokiwa envia uma requisição POST para sua URL https quando um pagamento é creditado ou muda de status. Gerencie as URLs na página Webhooks do painel do comerciante.

Formato da requisição

{
  "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", "...": "..." }
  }
}
CabeçalhoDescrição
Kokiwa-SignatureAssinatura no formato t=timestamp,v1=assinatura.
Kokiwa-Event-IdID do evento, igual ao id no corpo.
Kokiwa-Delivery-AttemptTentativa de entrega, começando em 1.

data.deposit é o objeto de depósito. data.payment_intent é o objeto retornado ao consultar um pagamento, sem checkout_url e livemode; livemode fica no nível superior do evento.

Tipos de evento

typeQuando é enviadodata
payment_intent.paidUm pagamento foi pago por completopayment_intent、deposit
payment_intent.partially_paidUm pagamento recebeu parte do valorpayment_intent、deposit
payment_intent.expiredUm pagamento expirou sem ser pagopayment_intent
payment_intent.paid_lateChegaram fundos depois que um pagamento expiroupayment_intent、deposit
deposit.confirmedUm depósito foi confirmado na blockchain, de um pagamento ou endereço de depósitodeposit
deposit.settledUm depósito chegou ao seu endereço de recebimentodeposit
webhook.testUm evento de teste enviado pelo painelmessage

Um pagamento com carteira é liquidado na própria transação, então deposit.confirmed e deposit.settled chegam juntos. Uma transferência envia primeiro deposit.confirmed e, após a varredura, deposit.settled. Credite o usuário em confirmed; não precisa esperar por settled.

Verificar a assinatura

Cada URL de webhook tem seu próprio segredo de assinatura (whsec_…), mostrado uma vez na criação.

  1. Leia t e v1 de Kokiwa-Signature.
  2. Calcule o HMAC-SHA256 da string {t}.{corpo bruto} com o segredo, codifique em hexadecimal minúsculo e compare com v1 em tempo constante.
  3. Rejeite requisições em que t difira mais de 5 minutos da hora atual.

Use o corpo bruto da requisição. Analisar o JSON e serializar de novo altera os bytes, e a assinatura não confere.

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)

Respostas e novas tentativas

  • Retorne qualquer 2xx em até 10 segundos e mova trabalho lento para uma fila em segundo plano.
  • Respostas fora de 2xx, timeouts e falhas de conexão têm 11 novas tentativas: após 30 segundos, 2, 10 e 30 minutos, depois 1, 2, 4, 8, 12, 24 e 20 horas. A entrega para após cerca de 3 dias.
  • Um evento pode chegar mais de uma vez e os eventos não têm ordem. Elimine duplicados pelo id do evento e chame GET /v1/payment_intents/{id} para o status mais recente.
  • A página Webhooks mostra o resultado de cada entrega e permite reenviar manualmente.
  • Se todas as tentativas falharem, use GET /v1/deposits para recuperar os depósitos perdidos.

Notificações no Telegram

Vincule @KoKiWaPayBot na página Configurações para receber uma mensagem a cada depósito. É só um lembrete e não substitui os webhooks.

Connect a wallet with WalletConnect