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.
Quer código pronto para copiar? Exemplos de integração em Node.js, Python, PHP, Go e Java
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çalho | Descrição |
|---|---|
Kokiwa-Signature | Assinatura no formato t=timestamp,v1=assinatura. |
Kokiwa-Event-Id | ID do evento, igual ao id no corpo. |
Kokiwa-Delivery-Attempt | Tentativa 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
| type | Quando é enviado | data |
|---|---|---|
payment_intent.paid | Um pagamento foi pago por completo | payment_intent、deposit |
payment_intent.partially_paid | Um pagamento recebeu parte do valor | payment_intent、deposit |
payment_intent.expired | Um pagamento expirou sem ser pago | payment_intent |
payment_intent.paid_late | Chegaram fundos depois que um pagamento expirou | payment_intent、deposit |
deposit.confirmed | Um depósito foi confirmado na blockchain, de um pagamento ou endereço de depósito | deposit |
deposit.settled | Um depósito chegou ao seu endereço de recebimento | deposit |
webhook.test | Um evento de teste enviado pelo painel | message |
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.
- Leia
tev1deKokiwa-Signature. - Calcule o HMAC-SHA256 da string
{t}.{corpo bruto}com o segredo, codifique em hexadecimal minúsculo e compare comv1em tempo constante. - Rejeite requisições em que
tdifira 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
iddo evento e chameGET /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/depositspara 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.