Pular para o conteúdo
xFlow Payments

Blog

Como testar webhook localmente

Para testar webhook localmente, exponha a porta do seu servidor com um túnel HTTPS público, cadastre essa URL como endpoint usando a chave de teste e simule o pagamento com POST /api/v1/test/charges/{id}/pay. Para repetir um evento sem chamar a API, assine o corpo com o segredo e envie com curl.

Atualizado em · Equipe xFlow Payments · 5 min de leitura

Para testar webhook localmente, o seu servidor precisa de uma URL pública. A xFlow só aceita endpoints https:// e públicos: http://localhost:3000 ou um IP interno são recusados com 400 invalid_url. A solução é um túnel HTTPS, que publica uma URL na internet e encaminha cada requisição para a porta da sua máquina. Com o túnel aberto, você cadastra o endpoint com a chave de teste, simula o pagamento e vê o evento chegar no seu handler. Os detalhes de cada evento estão no guia de webhook Pix.

O que você precisa

  • Chave de teste. pk_test_ e sk_test_, criadas em Chaves de API > Criar chave, no ambiente "Teste". Ela pode ser criada antes da aprovação do cadastro.
  • Handler rodando na sua máquina, por exemplo na porta 3000.
  • Uma ferramenta de túnel HTTPS, como ngrok ou Cloudflare Tunnel. Qualquer uma que publique uma URL https:// apontando para a sua porta serve.

Passo 1: suba o handler local

O handler abaixo, em Node.js com Express, valida a assinatura e grava o corpo cru de cada entrega em arquivo. Esse arquivo vai servir para repetir o evento depois, sem depender da API.

js
import express from "express";
import { writeFileSync } from "node:fs";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const SEGREDO = process.env.XFLOW_WEBHOOK_SECRET;

function assinaturaValida(raw, header) {
  const esperado = "sha256=" + createHmac("sha256", SEGREDO).update(raw).digest("hex");
  const a = Buffer.from(esperado), b = Buffer.from(header ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

app.post("/webhooks/xflow", express.raw({ type: "application/json" }), (req, res) => {
  const evento = req.get("x-webhook-event");
  const nome = String(evento).replace(/[^a-z._]/g, "");
  writeFileSync(`ultimo-${nome}.json`, req.body); // corpo cru, byte a byte
  if (!assinaturaValida(req.body, req.get("x-webhook-signature"))) {
    console.log("assinatura inválida", evento);
    return res.sendStatus(401);
  }
  const { data } = JSON.parse(req.body.toString("utf8"));
  console.log(evento, data.id, "livemode:", data.livemode);
  res.sendStatus(200);
});

app.listen(3000);

Duas regras valem desde o primeiro teste: calcule o HMAC sobre o corpo cru, antes de qualquer JSON.parse, e compare em tempo constante.

Passo 2: abra o túnel

bash
# exemplos; use a ferramenta que preferir
ngrok http 3000
# ou
cloudflared tunnel --url http://localhost:3000

A ferramenta mostra uma URL https://. Ela é o endereço público do seu handler enquanto o túnel estiver aberto. Em muitos planos gratuitos, essa URL muda a cada execução, o que afeta o passo seguinte.

Passo 3: cadastre o endpoint com a chave de teste

bash
curl -X POST https://app.xflowpayments.com/api/v1/webhooks \
  -u "pk_test_...:sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://SUA-URL-DO-TUNEL/webhooks/xflow",
    "events": ["transaction.pending", "transaction.paid", "transaction.refunded", "transaction.refund_failed"],
    "description": "teste local"
  }'

A resposta 201 traz o campo secret, o segredo de assinatura. Ele aparece uma única vez: copie para a variável XFLOW_WEBHOOK_SECRET e reinicie o handler.

Importante: a documentação não separa endpoints de teste e de produção. Por isso, o handler deve sempre conferir livemode, e o endpoint do túnel deve ser apagado quando o teste terminar.

Passo 4: gere o evento

Crie uma cobrança com a chave de teste e simule o pagamento:

bash
curl -X POST https://app.xflowpayments.com/api/v1/charges \
  -u "pk_test_...:sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"amountCents":1990,"customer":{"name":"Cliente","email":"cliente@exemplo.com","document":"00000000000"},"external_reference":"teste-local-1"}'

curl -X POST https://app.xflowpayments.com/api/v1/test/charges/{id}/pay \
  -u "pk_test_...:sk_test_..."

O evento transaction.paid chega no handler com livemode: false, payer: null e a taxa calculada com as taxas reais da sua conta. O roteiro completo do ambiente de teste está em API Pix sandbox.

Dois limites do sandbox afetam o teste de webhook. O estorno com chave de teste conclui na hora, mas a documentação não diz se ele dispara transaction.refunded: confira o resultado com GET /api/v1/charges/{id}/refund. E os saques retornam 403 test_mode_unsupported, então não há eventos withdrawal.* para receber.

Passo 5: teste o reenvio

A xFlow reenvia cada evento até 3 vezes quando a sua URL não responde 2xx. Para ver isso acontecer, troque temporariamente o 200 do handler por 500 e simule outro pagamento. A documentação não informa o intervalo entre as tentativas. O que importa é o comportamento do seu código: o mesmo evento pode chegar mais de uma vez, e a segunda entrega não pode dar baixa duas vezes. Use data.id junto com o nome do evento como chave para descartar repetições.

Repetir um evento sem chamar a API

Com o corpo gravado no passo 1, você repete a entrega quantas vezes quiser. Assine o arquivo com o segredo e envie com curl:

bash
CORPO=ultimo-transaction.paid.json
ASSINATURA="sha256=$(openssl dgst -sha256 -hmac "$XFLOW_WEBHOOK_SECRET" -hex < "$CORPO" | sed 's/^.* //')"

curl -X POST http://localhost:3000/webhooks/xflow \
  -H "Content-Type: application/json" \
  -H "x-webhook-event: transaction.paid" \
  -H "x-webhook-signature: $ASSINATURA" \
  --data-binary @"$CORPO"

Use --data-binary, e não -d: o -d remove quebras de linha do arquivo, os bytes mudam e a assinatura deixa de bater. Para conferir uma assinatura à mão, cole o segredo, o corpo e o header no validador de assinatura de webhook.

O mesmo truque cobre os eventos que o sandbox não gera, como withdrawal.completed. Monte um corpo no formato { event, data, createdAt }, assine e envie. Como a documentação não detalha o data dos eventos de saque, o handler deve confirmar o estado com GET /api/v1/transfers/{id} antes de agir.

Problemas comuns

Sintoma Causa provável O que fazer
400 invalid_url ao cadastrar URL http:// ou endereço interno Use a URL https:// do túnel
Assinatura não confere Corpo alterado antes do HMAC, curl -d ou segredo de outro endpoint Use o corpo cru, --data-binary e o segredo do endpoint certo
Eventos pararam de chegar O túnel fechou ou a URL mudou Apague o endpoint e crie outro com a URL nova
O mesmo evento chega duas vezes O handler não respondeu 2xx e houve reenvio Responda rápido e torne o processamento idempotente
400 endpoint_limit A conta chegou a 20 endpoints Apague os endpoints de testes antigos

Quando terminar

Liste os endpoints com GET /api/v1/webhooks e apague o do túnel com DELETE /api/v1/webhooks/{id}. Se for voltar a testar logo, pause com PATCH /api/v1/webhooks/{id} e o corpo { "active": false }. Um endpoint esquecido continua recebendo eventos, com dados de quem pagou, numa URL de túnel que pode deixar de ser sua. O conceito de webhook, com assinatura e reenvio, está resumido no glossário, e a referência completa fica na documentação da API.

Perguntas frequentes

Por que não posso cadastrar http://localhost como URL de webhook?

Porque a URL precisa ser https:// e pública. Um endereço interno é recusado com 400 invalid_url. Para receber eventos na sua máquina, use um túnel que publique uma URL HTTPS apontando para a porta local.

Preciso de chave de produção para testar o webhook?

Não. Com a chave de teste você cadastra o endpoint, cria a cobrança e simula o pagamento com POST /api/v1/test/charges/{id}/pay. O evento transaction.paid chega com livemode: false.

A URL do túnel mudou. Como atualizo o endpoint?

A API documenta o PATCH de webhook só para pausar ou reativar. Para trocar a URL, apague o endpoint antigo com DELETE /api/v1/webhooks/{id} e crie outro. O novo endpoint tem um segredo novo, que aparece uma única vez.

Como testo eventos de saque se o sandbox não faz saque?

Com chave de teste, os saques retornam 403 test_mode_unsupported, então o sandbox não gera eventos withdrawal.*. Monte um corpo de exemplo, assine com o segredo do endpoint e envie ao seu handler com curl.

Continue lendo