Confiabilidade
Webhooks
A KrokoPay envia um POST HTTPS para o seu servidor quando algo acontece com uma cobrança, assinatura ou saque. Verifique a assinatura, responda 2xx rápido e processe cada evento uma única vez.
Nesta página
Webhooks são a forma de o seu sistema saber de resultados que acontecem fora da sua requisição: um Pix pago minutos depois, um cartão recusado, uma renovação de madrugada. Em vez de consultar a API o tempo todo, cadastre um endpoint e a KrokoPay entrega cada evento nele, em JSON.
Cadastre um endpoint
Adicione endpoints em Desenvolvedores → Webhooks ou pela API. Cada endpoint tem uma URL HTTPS pública, a lista de tipos de evento que deseja receber e o próprio segredo de assinatura (whsec_test_… no ambiente de teste).
/v1/webhook_endpointsParâmetros do corpo
urlstringObrigatório- URL HTTPS que recebe os eventos.
eventsstring[]Obrigatório- Tipos de evento a entregar. Veja o catálogo.
descriptionstringOpcional- Uma anotação para o seu time, por exemplo qual serviço é dono do endpoint.
activebooleanOpcional- O padrão é
true. Endpoints inativos não recebem nada.
curl -X POST https://api.krokopay.app/v1/webhook_endpoints \ -H "Authorization: Bearer kp_test_sk_4f9a2c7e1b8d4a6f9c3e5d7b" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/krokopay", "description": "Orders service", "events": [ "charge.paid", "charge.failed", "charge.expired", "charge.refunded" ] }'Atualize um endpoint com POST /v1/webhook_endpoints/{id} e remova com DELETE /v1/webhook_endpoints/{id}.
Atenção: Guarde o segredo no servidor
secret é devolvido na criação do endpoint e fica visível no painel. Guarde-o em uma variável de ambiente: quem tiver esse valor consegue forjar eventos para o endpoint.Catálogo de eventos
| Evento | Enviado quando |
|---|---|
charge.created | Uma cobrança foi criada (pela API, por um link de pagamento ou por um período de assinatura). |
charge.paid | O pagamento foi confirmado. Libere o pedido aqui. |
charge.failed | Uma tentativa com cartão foi recusada. Veja o failure_code. |
charge.expired | Uma cobrança Pix não foi paga antes do expires_at. |
charge.refunded | Houve um reembolso total ou parcial. Leia refunded_amount e status. |
subscription.created | Uma assinatura começou. |
subscription.renewed | Um novo período foi pago. |
subscription.past_due | Uma cobrança de renovação falhou. |
subscription.paused | A assinatura foi pausada. |
subscription.resumed | Uma assinatura pausada voltou a ficar ativa. |
subscription.canceled | A assinatura foi encerrada. |
payout.requested | Foi solicitado um saque do seu saldo disponível. |
payout.paid | Um saque chegou à conta de destino. |
Inscreva-se só no que você trata. Para a maioria das lojas, charge.paid, charge.failed, charge.expired e charge.refunded bastam; acrescente os eventos subscription.* se você vende assinaturas.
Envelope do evento
Todos os eventos têm o mesmo envelope. O data.object é o objeto completo, como a API o devolve no momento do evento, então raramente é preciso fazer outra requisição.
Envelope
idstring- Id do evento (
evt_…). É o mesmo em todas as tentativas de entrega: use-o para eliminar duplicados. objectstring- Sempre
event. typestring- Um dos tipos do catálogo.
api_versionstring- Versão da API usada para montar o
data.object, por exemplo2026-11-01. created_atstring- Quando o evento aconteceu (ISO 8601).
data.objectobject- A cobrança, assinatura ou saque.
{ "id": "evt_6Ff7Gg8Hh9Ii0J", "object": "event", "type": "charge.paid", "api_version": "2026-11-01", "created_at": "2026-11-03T14:23:41.000Z", "data": { "object": { "id": "chg_4Fp9KzW1xQm7Rt", "object": "charge", "amount": 49700, "currency": "BRL", "status": "paid", "method": "pix", "card": null, "description": "Curso Identidade Visual do Zero", "customer_id": "cus_8Hq2LmZr4TxN1a", "product_id": "prod_3Vb7QpK9sWd2Ee", "price_id": "price_5Kd1NcR8vYq3Hm", "link_id": "plink_2Wn6HsT9cVb4Lq", "subscription_id": null, "fee": null, "net": null, "refunded_amount": 0, "failure_code": null, "created_at": "2026-11-03T14:22:05.000Z", "paid_at": "2026-11-03T14:23:41.000Z", "expires_at": "2026-11-03T14:37:05.000Z", "available_at": null, "checkout_url": "https://krokopay.app/pay/chg_4Fp9KzW1xQm7Rt" } }}{ "id": "evt_4Zz5Aa6Bb7Cc8D", "object": "event", "type": "charge.refunded", "api_version": "2026-11-01", "created_at": "2026-11-05T09:10:00.000Z", "data": { "object": { "id": "chg_4Fp9KzW1xQm7Rt", "object": "charge", "amount": 49700, "currency": "BRL", "status": "partially_refunded", "method": "pix", "card": null, "description": "Curso Identidade Visual do Zero", "customer_id": "cus_8Hq2LmZr4TxN1a", "product_id": "prod_3Vb7QpK9sWd2Ee", "price_id": "price_5Kd1NcR8vYq3Hm", "link_id": "plink_2Wn6HsT9cVb4Lq", "subscription_id": null, "fee": null, "net": null, "refunded_amount": 9900, "failure_code": null, "created_at": "2026-11-03T14:22:05.000Z", "paid_at": "2026-11-03T14:23:41.000Z", "expires_at": "2026-11-03T14:37:05.000Z", "available_at": null, "checkout_url": "https://krokopay.app/pay/chg_4Fp9KzW1xQm7Rt" } }}Verifique as assinaturas
Cada entrega traz o cabeçalho KrokoPay-Signature. O formato proposto é t=<segundos unix>,v1=<hex>, em que v1 é um HMAC-SHA256 de <t>.<corpo bruto> com o segredo do endpoint como chave. Recalcule e compare antes de confiar no conteúdo.
POST /webhooks/krokopay HTTP/1.1Host: example.comContent-Type: application/jsonKrokoPay-Signature: t=1793715821,v1=9f2c4e1a7b3d5f8e0a6c2b4d9e1f3a5c7b9d0e2f4a6c8b1d3e5f7a9c0b2d4e6f{ "id": "evt_6Ff7Gg8Hh9Ii0J", "object": "event", "type": "charge.paid", ... }Passo 1: Leia o corpo bruto
Use exatamente os bytes recebidos. Interpretar e serializar o JSON de novo altera o conteúdo e invalida a assinatura.
Passo 2: Interprete o cabeçalho
Pegue o
te todos os valoresv1. Pode haver mais de umv1, por exemplo durante a troca de um segredo.Passo 3: Confira o horário
Rejeite assinaturas antigas demais, para bloquear requisições reaproveitadas. A janela de tolerância ainda será definida; até lá, escolha uma para a sua integração.
Passo 4: Compare em tempo constante
Calcule o HMAC e compare com
timingSafeEqual, nunca com===.
import { createHmac, timingSafeEqual } from "node:crypto";// The endpoint's signing secret (whsec_test_…), kept in your environment.const WEBHOOK_SECRET = process.env.KROKOPAY_WEBHOOK_SECRET ?? "";// How old a signature may be. The proposal does not fix this value yet:// choose one for your integration and revisit it when it is defined.const TOLERANCE_SECONDS = 300;/** Parses "t=<unix seconds>,v1=<hex>" into its parts. */function parseSignatureHeader(header: string): { timestamp: number; signatures: string[] } { let timestamp = Number.NaN; const signatures: string[] = []; for (const part of header.split(",")) { const [key, value] = part.trim().split("=", 2); if (key === "t") timestamp = Number(value); if (key === "v1" && value) signatures.push(value); } return { timestamp, signatures };}export function verifySignature(rawBody: string, header: string | null, secret: string): boolean { if (!header || !secret) return false; const { timestamp, signatures } = parseSignatureHeader(header); if (!Number.isFinite(timestamp) || signatures.length === 0) return false; if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false; const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest(); return signatures.some((signature) => { const received = Buffer.from(signature, "hex"); return received.length === expected.length && timingSafeEqual(received, expected); });}// Your own persistence and job queue.declare function alreadyProcessed(eventId: string): Promise<boolean>;declare function enqueue(event: KrokoPayEvent): Promise<void>;interface KrokoPayEvent { id: string; type: string; created_at: string; data: { object: { id: string; object: string } };}// A fetch-style handler: Request in, Response out.export async function POST(request: Request): Promise<Response> { // Verify the raw body exactly as received, before parsing it. const rawBody = await request.text(); const header = request.headers.get("KrokoPay-Signature"); if (!verifySignature(rawBody, header, WEBHOOK_SECRET)) { return new Response("Invalid signature", { status: 400 }); } const event = JSON.parse(rawBody) as KrokoPayEvent; // Deliveries can repeat: acknowledge duplicates without redoing the work. if (await alreadyProcessed(event.id)) return new Response(null, { status: 200 }); // Hand off slow work (emails, fulfilment) and answer right away. await enqueue(event); return new Response(null, { status: 200 });}Informação: Formato proposto
Responda 2xx, e rápido
- Responda com qualquer status
2xxassim que a assinatura for verificada e o evento estiver salvo. O corpo da resposta é ignorado. - Deixe o trabalho demorado (e-mails, liberação do pedido, chamadas a outros serviços) para uma tarefa em segundo plano, depois de responder.
- Qualquer outro status, um timeout ou um erro de conexão conta como entrega com falha. O tempo limite de resposta ainda será definido.
- Responda
2xxtambém para os tipos que você ignora, para que não sejam reenviados.
// Runs in your worker, after the webhook was acknowledged.// Re-read the object from the API when you need its latest state.async function handle(event: KrokoPayEvent): Promise<void> { switch (event.type) { case "charge.paid": await markOrderPaid(event.data.object.id); break; case "charge.failed": case "charge.expired": await releaseReservedStock(event.data.object.id); break; case "charge.refunded": await recordRefund(event.data.object.id); break; case "subscription.past_due": await notifyBillingProblem(event.data.object.id); break; default: // Ignore types you did not subscribe to or do not handle yet. break; }}Reenvios
Uma entrega com falha é reenviada com o mesmo id de evento e um número de tentativa crescente, que você acompanha por endpoint no painel. O calendário de reenvios (intervalos e número máximo de tentativas) ainda será definido. Você também pode reenviar uma entrega manualmente pelo painel.
Ordem e duplicados
- Duplicados acontecem. Um reenvio depois de um timeout pode entregar um evento que você já processou. Guarde os ids processados e ignore as repetições.
- A ordem não é garantida. O
charge.paidpode chegar antes docharge.created. Não monte máquinas de estado que dependam da ordem de chegada. - Confie no estado do objeto. Decida pelo
data.object.statusou, na dúvida, busque a versão mais recente com GET /v1/charges/{id}, e compare ocreated_atpara descartar eventos antigos. - Torne os efeitos idempotentes. Marcar um pedido como pago duas vezes não deve mudar nada.
Testando webhooks
Passo 1: Envie um evento de teste
Em Desenvolvedores → Webhooks, escolha um endpoint e um tipo de evento e envie. A entrega aparece na lista com o status e o número da tentativa.
Passo 2: Dispare fluxos reais
Pague uma cobrança de teste no checkout com os cartões de teste: cada resultado emite o próprio evento.
Passo 3: Teste localmente
Assine um payload você mesmo com o segredo de teste e envie para o seu servidor local, como abaixo. Nenhuma requisição sai da sua máquina.
secret="whsec_test_2e4c6a8b0d1f3a5c7e9b"payload='{"id":"evt_test_local","object":"event","type":"charge.paid","api_version":"2026-11-01","created_at":"2026-11-03T14:23:41.000Z","data":{"object":{"id":"chg_test_local","object":"charge"}}}'t=$(date +%s)sig=$(printf '%s.%s' "$t" "$payload" | openssl dgst -sha256 -hmac "$secret" -hex | sed 's/^.* //')curl -X POST http://localhost:3000/webhooks/krokopay \ -H "Content-Type: application/json" \ -H "KrokoPay-Signature: t=$t,v1=$sig" \ -d "$payload"Veja também
- Idempotência: a mesma segurança para as suas requisições à KrokoPay.
- Checkout: quais eventos um pagamento emite.
- Assinaturas: eventos de cada transição da assinatura.