Pular para o conteúdo
Painel

Atenção: Proposta provisória de contrato

Estes endpoints ainda não estão em operação e nenhum SDK foi publicado. Os exemplos usam apenas identificadores de teste e nada é enviado a lugar nenhum.

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.

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).

Endpoint: POST/v1/webhook_endpoints

Parâ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

O 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

EventoEnviado quando
charge.createdUma cobrança foi criada (pela API, por um link de pagamento ou por um período de assinatura).
charge.paidO pagamento foi confirmado. Libere o pedido aqui.
charge.failedUma tentativa com cartão foi recusada. Veja o failure_code.
charge.expiredUma cobrança Pix não foi paga antes do expires_at.
charge.refundedHouve um reembolso total ou parcial. Leia refunded_amount e status.
subscription.createdUma assinatura começou.
subscription.renewedUm novo período foi pago.
subscription.past_dueUma cobrança de renovação falhou.
subscription.pausedA assinatura foi pausada.
subscription.resumedUma assinatura pausada voltou a ficar ativa.
subscription.canceledA assinatura foi encerrada.
payout.requestedFoi solicitado um saque do seu saldo disponível.
payout.paidUm 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 exemplo 2026-11-01.
created_atstring
Quando o evento aconteceu (ISO 8601).
data.objectobject
A cobrança, assinatura ou saque.
charge.paidJSON
{  "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"    }  }}
charge.refundedJSON
{  "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.

Uma entregaHTTP
POST /webhooks/krokopay HTTP/1.1Host: example.comContent-Type: application/jsonKrokoPay-Signature: t=1793715821,v1=9f2c4e1a7b3d5f8e0a6c2b4d9e1f3a5c7b9d0e2f4a6c8b1d3e5f7a9c0b2d4e6f​{ "id": "evt_6Ff7Gg8Hh9Ii0J", "object": "event", "type": "charge.paid", ... }
  1. 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.

  2. Passo 2: Interprete o cabeçalho

    Pegue o t e todos os valores v1. Pode haver mais de um v1, por exemplo durante a troca de um segredo.

  3. 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.

  4. Passo 4: Compare em tempo constante

    Calcule o HMAC e compare com timingSafeEqual, nunca com ===.

Verificar e confirmar (só biblioteca padrão do Node)TypeScript
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

O formato do cabeçalho acima faz parte do contrato provisório e pode mudar até a versão final. Mantenha a verificação em uma única função para facilitar a atualização.

Responda 2xx, e rápido

  • Responda com qualquer status 2xx assim 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 2xx também para os tipos que você ignora, para que não sejam reenviados.
Encaminhe os eventos no seu workerTypeScript
// 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.paid pode chegar antes do charge.created. Não monte máquinas de estado que dependam da ordem de chegada.
  • Confie no estado do objeto. Decida pelo data.object.status ou, na dúvida, busque a versão mais recente com GET /v1/charges/{id}, e compare o created_at para descartar eventos antigos.
  • Torne os efeitos idempotentes. Marcar um pedido como pago duas vezes não deve mudar nada.

Testando webhooks

  1. 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.

  2. 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.

  3. 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.

Assinar e enviar um evento de teste localbash
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"
  • 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.