Webhooks
Como receber e processar notificações em tempo real sobre o status das suas transações.
Webhooks permitem que a Legacy notifique sua aplicação automaticamente sobre mudanças de status em cobranças e saques, sem a necessidade de consultar a API repetidamente.
Sempre que um evento relevante ocorrer — pagamento PIX confirmado, saque concluído, estorno aplicado — a Legacy enviará uma requisição POST para a URL que você informou na criação da transação.
Como configurar
Ao criar um Payin ou Payout, inclua o campo webhookUrl com a URL do seu endpoint:
{
"paymentMethod": "PIX",
"amount": 10000,
"webhookUrl": "https://sua-api.com/webhooks/legacy",
...
}A Legacy enviará todas as atualizações daquela transação específica para essa URL.
Campo opcional, mas altamente recomendadoSem
webhookUrl, você precisará consultar a API manualmente para verificar mudanças de status. O uso de webhooks é a forma mais eficiente e confiável de monitorar suas transações.
Cabeçalhos de cada evento
Toda requisição enviada pela Legacy acompanha os seguintes cabeçalhos:
| Cabeçalho | Descrição |
|---|---|
X-Webhook-Id | Identificador único do evento. Repetido em todas as retentativas do mesmo evento — use-o como chave de idempotência |
X-Webhook-Event | Tipo do evento, ex.: payin.approved, payout.completed |
X-Webhook-Signature | Assinatura HMAC do evento. Só é enviado se você ativar a assinatura (veja a seção seguinte) |
Assinatura dos eventos
Como sua webhookUrl precisa ser pública, qualquer pessoa que descubra esse endereço pode enviar uma requisição para ela fingindo ser a Legacy — inclusive um falso "status": "APPROVED". Para eliminar esse risco, você pode ativar a assinatura dos eventos: a partir daí cada requisição leva, no cabeçalho X-Webhook-Signature, uma prova criptográfica gerada com um segredo que só você e a Legacy conhecem.
A assinatura é opcional e desligada por padrãoEnquanto você não ativar, nada muda: os eventos continuam chegando exatamente como hoje, sem o cabeçalho de assinatura. Ativar é uma camada extra de segurança, recomendada para qualquer integração que libere pedido, crédito ou acesso automaticamente a partir do webhook.
Como ativar
No Dashboard da Legacy, na mesma área em que ficam suas chaves de API, abra a seção Assinatura de webhooks e clique em Ativar assinatura. Um segredo no formato whsec_..., único da sua conta, será gerado na hora — e a partir desse momento todos os eventos passam a sair assinados.
Ative primeiro e só depois passe a exigir a assinatura no seu endpoint: se o seu código rejeitar eventos sem assinatura antes de você ativar, ele descartará eventos legítimos.
Trate o segredo como uma senhaQuem tem o segredo consegue forjar eventos válidos. Guarde-o em um cofre de segredos e nunca o exponha no navegador, em código-fonte ou em repositórios.
Formato do cabeçalho
X-Webhook-Signature: t=1772000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
| Campo | Significado |
|---|---|
t | Momento em que o evento foi assinado, em segundos desde a época Unix |
v1 | HMAC-SHA256, em hexadecimal, da string {t}.{corpo} usando seu segredo como chave |
A string assinada é o timestamp, um ponto, e o corpo bruto da requisição:
1772000000.{"id":"payin_1A2B3C","status":"APPROVED",...}
Como verificar
- Leia o corpo bruto da requisição, antes de qualquer parse.
- Extraia
tev1do cabeçalhoX-Webhook-Signature. - Calcule o HMAC-SHA256 de
{t}.{corpo}usando seu segredo. - Compare com
v1usando uma função de comparação em tempo constante. - Rejeite o evento se
testiver a mais de 5 minutos do horário atual.
Use o corpo bruto, nunca o JSON re-serializadoSe você fizer
JSON.parsee depoisJSON.stringifyde novo antes de calcular o HMAC, a ordem das chaves e o espaçamento podem mudar e a assinatura não vai bater. Em frameworks que já convertem o corpo automaticamente (Express comexpress.json(), Laravel, Django), configure o acesso ao corpo original.
Node.js (Express):
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // whsec_...
const TOLERANCIA_SEGUNDOS = 300;
// `verify` guarda o corpo bruto antes do parse — é ele que foi assinado.
app.use(express.json({
verify: (req, res, buf) => { req.rawBody = buf.toString('utf8'); },
}));
function assinaturaValida(rawBody, header, secret) {
if (!header) return false;
const partes = Object.fromEntries(
header.split(',').map((p) => {
const i = p.indexOf('=');
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
})
);
const timestamp = Number(partes.t);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCIA_SEGUNDOS) return false;
const esperado = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`, 'utf8')
.digest('hex');
const a = Buffer.from(partes.v1 || '', 'utf8');
const b = Buffer.from(esperado, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/webhooks/pagamentos', (req, res) => {
if (!assinaturaValida(req.rawBody, req.get('X-Webhook-Signature'), WEBHOOK_SECRET)) {
return res.status(401).send('assinatura inválida');
}
// Evento autêntico — processe aqui.
res.sendStatus(200);
});PHP:
<?php
$secret = getenv('WEBHOOK_SECRET'); // whsec_...
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$tolerancia = 300;
$partes = [];
foreach (explode(',', $header) as $parte) {
[$chave, $valor] = array_pad(explode('=', $parte, 2), 2, null);
$partes[trim($chave)] = trim((string) $valor);
}
$timestamp = isset($partes['t']) ? (int) $partes['t'] : 0;
if (abs(time() - $timestamp) > $tolerancia) {
http_response_code(401);
exit('evento expirado');
}
$esperado = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
if (!hash_equals($esperado, $partes['v1'] ?? '')) {
http_response_code(401);
exit('assinatura inválida');
}
// Evento autêntico — processe aqui.
http_response_code(200);Python (Flask):
import hashlib
import hmac
import time
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_..."
TOLERANCIA_SEGUNDOS = 300
def assinatura_valida(raw_body: bytes, header: str, secret: str) -> bool:
if not header:
return False
partes = dict(
parte.split("=", 1) for parte in header.split(",") if "=" in parte
)
try:
timestamp = int(partes["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > TOLERANCIA_SEGUNDOS:
return False
mensagem = f"{timestamp}.".encode() + raw_body
esperado = hmac.new(secret.encode(), mensagem, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, partes.get("v1", ""))
@app.post("/webhooks/pagamentos")
def receber_webhook():
if not assinatura_valida(
request.get_data(),
request.headers.get("X-Webhook-Signature", ""),
WEBHOOK_SECRET,
):
return "assinatura inválida", 401
# Evento autêntico — processe aqui.
return "", 200Rotação do segredo
Você pode gerar um segredo novo a qualquer momento pelo Dashboard. Para não perder eventos durante a troca, o segredo anterior continua sendo aceito por 24 horas: nesse período o cabeçalho carrega duas assinaturas, uma com cada segredo.
X-Webhook-Signature: t=1772000000,v1=<assinatura com o segredo novo>,v1=<assinatura com o anterior>
Aceite o evento se qualquerv1baterPercorra todos os campos
v1do cabeçalho e considere o evento válido quando pelo menos um corresponder ao HMAC calculado. Os exemplos acima leem apenas o primeirov1; se você pretende rotacionar o segredo, itere sobre todos.
Eventos de Payin
Quando o status de um Payin mudar, você receberá um payload como este:
{
"id": "payin_1A2B3C",
"externalId": "tx_998877",
"referenceId": "pedido-001",
"status": "APPROVED",
"amount": 10000,
"paymentMethod": "PIX",
"createdAt": "2026-03-02T12:00:00.000Z"
}
O evento traz apenas os campos de identificação e statusUse
GET https://api.legacyecombrasil.com/payin/{id}quando precisar dos dados completos da transação (cliente, itens, taxas). O campocreatedAté o da criação da cobrança, não o do evento — o momento do evento vem notdo cabeçalho de assinatura.
Status possíveis do Payin
| Status | Significado |
|---|---|
PENDING | Cobrança gerada, aguardando pagamento do cliente |
APPROVED | Pagamento confirmado, saldo disponível na conta |
REFUSED | Negado pelo emissor do cartão ou pelo antifraude |
FAILED | Falha catastrófica, geralmente na adquirente |
CANCELED | Cancelado antes de ser liquidado |
REFUNDED | Estorno parcial ou total aplicado |
CHARGEBACK | Cliente contestou a cobrança junto ao banco emissor |
MED | Mecanismo Especial de Devolução acionado |
EXPIRED | Janela de pagamento do PIX ou Boleto expirou |
Eventos de Payout
Quando o status de um Payout mudar, você receberá:
{
"id": "payout_1A2B3C",
"externalId": "trf_554433",
"status": "COMPLETED",
"amount": 250000,
"referenceId": "comissao-001",
"created_at": "2026-03-02T12:30:00.000Z"
}Eventos de falha (FAILED, REFUSED, CANCELED, REFUNDED) incluem também o campo reason com o motivo informado pela instituição.
Status possíveis do Payout
| Status | Significado |
|---|---|
AWAITING_APPROVAL | Aguardando aprovação manual no Dashboard |
PENDING | Aprovado, na fila para processamento |
PROCESSING | Sendo processado pela adquirente |
APPROVED | Processado pela adquirente, aguardando confirmação final |
COMPLETED | Transferência concluída com sucesso |
FAILED | Falha no processamento, saldo estornado |
CANCELED | Cancelado antes do processamento |
REFUSED | Rejeitado pela instituição bancária |
REFUNDED | Saldo devolvido à conta de origem |
Política de resposta e reenvio
Responda com status2xxpara confirmar o recebimentoQualquer status HTTP no range
200–299é aceito como confirmação. Não é necessário nenhum corpo de resposta específico.
Seu endpoint tem 5 segundos para responder. Se ele retornar erro (status 4xx ou 5xx) ou estourar esse tempo, a Legacy reenvia o evento automaticamente, em intervalos crescentes:
| Tentativa | Intervalo |
|---|---|
| 1ª retentativa | 1 segundo após a falha |
| 2ª retentativa | 2 segundos após |
Esgotadas as tentativas, o evento é registrado como não entregue. Você pode reenviá-lo manualmente pelo Dashboard, ou consultar o estado atual da transação com GET https://api.legacyecombrasil.com/payin/{id}.
Projete seu endpoint para ser idempotenteComo um mesmo evento pode ser entregue mais de uma vez, use o cabeçalho
X-Webhook-Idpara garantir que não processará o mesmo evento duas vezes. Não use oidda transação para isso: ele é o mesmo em todos os eventos daquela transação, então eventos legítimos de status diferentes seriam descartados.
Responda rápido e processe depoisComo a janela é curta, registre o evento e responda
2xximediatamente, deixando o processamento pesado para uma fila assíncrona. Assim uma lentidão sua não vira uma entrega falha.
Updated 8 days ago