Allya Payments
Webhooks

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

No painel:

  1. Entre no projeto.
  2. Selecione o ambiente.
  3. Acesse Webhooks.
  4. Cadastre a URL do seu endpoint.
  5. 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

TipoQuando
payment.createdLogo após POST /v1/payments.
payment.pendingPix gerado, aguardando pagamento.
payment.paidPagamento confirmado.
payment.failedPagamento rejeitado.
payment.canceledCancelamento confirmado.
payment.expiredPix/checkout expirou.

Assinatura recorrente

TipoQuandoPayload
subscription.createdApós POST /v1/subscriptions.data.subscriptionId, status, interval, nextBillingAt, trialEndsAt.
subscription.renewedCobrança recorrente paga (renovação OK).mesmo formato.
subscription.payment_failedTentativa de cobrança falhou (cartão recusado, sem saldo).mesmo formato.
subscription.past_dueGateway marcou a assinatura como atrasada após falhas seguidas.mesmo formato.
subscription.canceledCancelamento 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 2xx em até 8 segundos. Entrega marcada como SENT.
  • 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 FAILED sem 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

On this page