Webhooks
Como receber eventos padronizados enviados pela Allya Payments. Payload, assinatura, validação e eventos.
A Allya recebe webhooks dos gateways (inbound), normaliza o evento e envia um webhook padronizado para os endpoints cadastrados no seu projeto (outbound).
Esta página foca no outbound (Allya → sua aplicação): como cadastrar endpoint, validar a assinatura, e o catálogo de eventos.
Para a parte inbound (gateway → Allya), incluindo formato de validação por gateway, veja Gateways.
Sumário
- Configurar endpoint: cadastrar URL HTTPS pública e copiar o secret.
- Headers enviados:
X-Allya-Event,X-Allya-Signature, etc. - Payload: formato JSON e exemplo.
- Validar assinatura: HMAC-SHA256 com proteção contra replay.
- Eventos disponíveis: payment.* e subscription.*.
- Retentativas: comportamento atual e limitações.
Configurar endpoint
No painel:
- Entre no projeto.
- Selecione o ambiente.
- Acesse Webhooks.
- Cadastre a URL do seu endpoint.
- Copie o secret exibido na criação.
O secret é usado para validar a assinatura dos eventos enviados pela Allya.
Por segurança, a URL cadastrada precisa ser HTTPS pública. URLs locais, privadas, link-local, metadata services ou nomes internos sem DNS público são bloqueados antes da criação e também antes de cada envio.
Para testar a integração na sua máquina antes de publicar, veja Testar webhooks localmente: exponha sua aplicação atrás de um túnel HTTPS (ngrok ou Cloudflare Tunnel) e cadastre a URL pública no painel.
Headers enviados
X-Allya-Event: payment.paid
X-Allya-Delivery-Id: delivery_id
X-Allya-Timestamp: 1710000000
X-Allya-Signature: sha256=...Payload
{
"id": "evt_9d2c7b1f4a6e4b58a0d3c2e1f9b8a765",
"type": "payment.paid",
"createdAt": "2026-05-21T12:00:00.000Z",
"data": {
"paymentId": "pay_7f3e0c9a12b44d63a9f612c42a8d2b90",
"externalId": "pedido_123",
"status": "paid",
"method": "pix",
"provider": "abacate_pay",
"amount": 4990,
"currency": "BRL",
"gatewayRef": "gateway_payment_id"
}
}Exemplo completo (curl)
Útil para reproduzir o webhook em Postman, Insomnia ou mocks locais. Copie e ajuste https://api.suaempresa.com/webhooks/allya (endpoint cadastrado no seu sistema) para a sua URL.
curl -X POST https://api.suaempresa.com/webhooks/allya \
-H "Content-Type: application/json" \
-H "User-Agent: Allya-Payments-Webhook/1.0" \
-H "X-Allya-Event: payment.paid" \
-H "X-Allya-Delivery-Id: wd_0e4a8b3c9d1f4a27b6c5d8e2f0a19374" \
-H "X-Allya-Timestamp: 1710000000" \
-H "X-Allya-Signature: sha256=abcdef1234567890..." \
-d '{
"id": "evt_9d2c7b1f4a6e4b58a0d3c2e1f9b8a765",
"type": "payment.paid",
"createdAt": "2026-05-21T12:00:00.000Z",
"data": {
"paymentId": "pay_7f3e0c9a12b44d63a9f612c42a8d2b90",
"externalId": "pedido_123",
"status": "paid",
"method": "pix",
"provider": "abacate_pay",
"amount": 4990,
"currency": "BRL",
"gatewayRef": "gateway_payment_id"
}
}'Para gerar a assinatura de teste:
TIMESTAMP=1710000000
BODY='{"id":"evt_9d2c7b1f4a6e4b58a0d3c2e1f9b8a765","type":"payment.paid","createdAt":"2026-05-21T12:00:00.000Z","data":{"paymentId":"pay_7f3e0c9a12b44d63a9f612c42a8d2b90","externalId":"pedido_123","status":"paid","method":"pix","provider":"abacate_pay","amount":4990,"currency":"BRL","gatewayRef":"gateway_payment_id"}}'
SECRET=wh_sec_xxx
SIGNATURE=$(
printf "%s.%s" "$TIMESTAMP" "$BODY" \
| openssl dgst -sha256 -hmac "$SECRET" -binary \
| od -An -tx1 \
| tr -d ' \n'
)
printf "sha256=%s\n" "$SIGNATURE"Validar assinatura
A assinatura é HMAC-SHA256 do texto:
{timestamp}.{body}Além de checar o HMAC, rejeite requisições cujo X-Allya-Timestamp esteja muito velho (recomendado: 5 minutos). Sem essa checagem, um atacante que tenha capturado uma entrega antiga pode reenviá-la indefinidamente: o HMAC continuaria válido porque o secret não muda.
Exemplo em TypeScript usando Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
const MAX_TIMESTAMP_SKEW_SECONDS = 300;
function verifyAllyaWebhook(args: {
rawBody: string;
signatureHeader: string | null;
timestampHeader: string | null;
secret: string;
}): { valid: true } | { valid: false; reason: string } {
if (!args.signatureHeader || !args.timestampHeader) {
return { valid: false, reason: "missing_headers" };
}
// 1. Anti-replay: rejeita timestamps fora da janela aceitável.
const ts = Number.parseInt(args.timestampHeader, 10);
if (!Number.isFinite(ts)) {
return { valid: false, reason: "invalid_timestamp" };
}
const skew = Math.abs(Math.floor(Date.now() / 1000) - ts);
if (skew > MAX_TIMESTAMP_SKEW_SECONDS) {
return { valid: false, reason: "timestamp_out_of_range" };
}
// 2. HMAC: compara em tempo constante.
const expected = createHmac("sha256", args.secret)
.update(`${args.timestampHeader}.${args.rawBody}`)
.digest("hex");
const received = args.signatureHeader.replace(/^sha256=/, "");
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return { valid: false, reason: "invalid_signature" };
}
return { valid: true };
}Exemplo de rota Next.js
import { headers } from "next/headers";
export async function POST(req: Request) {
const rawBody = await req.text();
const headerStore = await headers();
const result = verifyAllyaWebhook({
rawBody,
signatureHeader: headerStore.get("x-allya-signature"),
timestampHeader: headerStore.get("x-allya-timestamp"),
secret: process.env.ALLYA_WEBHOOK_SECRET!,
});
if (!result.valid) {
return Response.json({ error: result.reason }, { status: 401 });
}
const event = JSON.parse(rawBody);
if (event.type === "payment.paid") {
// Marque o pedido como pago no seu sistema.
}
return Response.json({ ok: true });
}Eventos disponíveis
Pagamento avulso
| Tipo | Quando |
|---|---|
payment.created | Logo após POST /v1/payments. |
payment.pending | Pix gerado, aguardando pagamento. |
payment.paid | Pagamento confirmado. |
payment.failed | Pagamento rejeitado. |
payment.canceled | Cancelamento confirmado. |
payment.expired | Pix/checkout expirou. |
Assinatura recorrente
| Tipo | Quando | Payload |
|---|---|---|
subscription.created | Após POST /v1/subscriptions. | data.subscriptionId, status, interval, nextBillingAt, trialEndsAt. |
subscription.renewed | Cobrança recorrente paga (renovação OK). | mesmo formato. |
subscription.payment_failed | Tentativa de cobrança falhou (cartão recusado, sem saldo). | mesmo formato. |
subscription.past_due | Gateway marcou a assinatura como atrasada após falhas seguidas. | mesmo formato. |
subscription.canceled | Cancelamento efetivado (imediato ou ao fim do período). | canceledAt preenchido. |
O payload de evento de assinatura usa data.subscriptionId em vez de data.paymentId. Demais headers (X-Allya-Signature, etc.) e o esquema HMAC-SHA256 são idênticos.
No Asaas, cobranças de assinatura chegam como eventos PAYMENT_* com payment.subscription. Se a cobrança ainda não existir como Payment local, a Allya cria esse registro, vincula à assinatura e então emite os eventos padronizados de pagamento e assinatura.
No Pagar.me, POST /v1/subscriptions cria primeiro um checkout pl_.... O evento público subscription.created é enviado quando o gateway manda subscription.created com o sub_... real. Eventos charge.paid e charge.payment_failed vinculados a uma assinatura criam ou atualizam o Payment local do ciclo e também disparam subscription.renewed ou subscription.payment_failed.
{
"id": "evt_31c4a9b7e6d54f2c8a0b1e3d5f709246",
"type": "subscription.renewed",
"createdAt": "2026-06-25T12:00:00.000Z",
"data": {
"subscriptionId": "sub_4a1c0e7b9d224f5fb8c6102d7e3a9014",
"externalId": "plano-pro-12345",
"status": "active",
"method": "card",
"provider": "asaas",
"amount": 4990,
"currency": "BRL",
"interval": "monthly",
"gatewayRef": "gateway_subscription_id",
"nextBillingAt": "2026-07-25T00:00:00.000Z",
"trialEndsAt": null,
"cancelAtPeriodEnd": false,
"canceledAt": null
}
}subscription.payment_failed
Disparado quando o gateway tenta cobrar e o cartão é recusado (sem saldo, expirado, antifraude, etc.). A assinatura ainda pode ser retentada pelo gateway antes de virar past_due.
{
"id": "evt_6b8e1c4d9f0a4b27a5d3e2c17890f6ab",
"type": "subscription.payment_failed",
"createdAt": "2026-06-25T12:00:00.000Z",
"data": {
"subscriptionId": "sub_4a1c0e7b9d224f5fb8c6102d7e3a9014",
"externalId": "plano-pro-12345",
"status": "past_due",
"method": "card",
"provider": "pagarme",
"amount": 4990,
"currency": "BRL",
"interval": "monthly",
"gatewayRef": "gateway_subscription_id",
"nextBillingAt": "2026-06-28T00:00:00.000Z",
"trialEndsAt": null,
"cancelAtPeriodEnd": false,
"canceledAt": null
}
}subscription.past_due
Disparado quando o gateway desiste de retentar cobranças seguidas e marca a assinatura como atrasada. Costuma vir depois de uma ou mais ocorrências de subscription.payment_failed. Em alguns gateways, ele entra direto sem evento payment_failed intermediário: depende do dunning configurado.
{
"id": "evt_c0d7f2a9b4e64c138a5d9e1f0726b3c8",
"type": "subscription.past_due",
"createdAt": "2026-07-02T12:00:00.000Z",
"data": {
"subscriptionId": "sub_4a1c0e7b9d224f5fb8c6102d7e3a9014",
"externalId": "plano-pro-12345",
"status": "past_due",
"method": "card",
"provider": "pagarme",
"amount": 4990,
"currency": "BRL",
"interval": "monthly",
"gatewayRef": "gateway_subscription_id",
"nextBillingAt": null,
"trialEndsAt": null,
"cancelAtPeriodEnd": false,
"canceledAt": null
}
}Retentativas
Na versão atual, a entrega é síncrona e registra sucesso ou falha:
- Sucesso: endpoint respondeu
2xxem até 8 segundos. Entrega marcada comoSENT. - Falha de rede ou 5xx: entrega marcada como
FAILED: sem retry automático. - 4xx no endpoint do cliente: a Allya assume erro permanente do receptor (URL errada, body rejeitado) e marca
FAILEDsem retry.
Reenvio manual: no painel, em Webhooks → Histórico de entregas, cada entrega FAILED tem botão Reenviar. O reenvio usa o payload original e mesma assinatura (X-Allya-Signature continua válida porque o timestamp também é o original).
Retentativas automáticas com backoff e dead-letter queue ainda não estão disponíveis. Se seu endpoint estiver instável, monitore o painel: webhooks falhos não chegam a você sem ação manual.
Status processing não dispara webhook outbound. Esse status é intermediário (gateway ainda não confirmou) e seria ruído para o consumidor. Para sair de processing, aguarde o próximo webhook do gateway ou chame POST /v1/payments/:id/sync.
Veja também
- Status de pagamento: mapeamento completo, incluindo o que dispara cada evento.
- Status de assinatura: estados normalizados das assinaturas.
- Testar webhooks localmente: túnel HTTPS para receber em desenvolvimento.
- Troubleshooting: quando o handler rejeita a assinatura.