Webhooks - Pix Automático

Visão Geral

Os webhooks notificam a ITP sobre mudanças de status em consentimentos recorrentes e pagamentos Pix de forma assíncrona e em tempo real. Eles são o mecanismo primário para acompanhar o ciclo de vida de cada pagamento — essencial para orquestrar retentativas intradia e extradia de forma correta.

A detentora de conta (ASPSP) é responsável pelo disparo das notificações. A ITP deve expor um endpoint HTTPS acessível publicamente para receber e processar esses eventos.


Configuração do Endpoint Receptor

RequisitoDetalhe
ProtocoloHTTPS obrigatório (certificado válido, não auto-assinado)
MétodoPOST
Tempo de resposta≤ 5 segundos — responda 200 OK imediatamente
IdempotênciaO mesmo evento pode ser entregue mais de uma vez
DisponibilidadeAlta disponibilidade recomendada — falhas causam retries da detentora

SLA de Notificação

Conforme regras do Open Finance Brasil:

EventoSLA máximo de entrega
Mudança de status de pagamento≤ 1,5 segundo após o evento
Mudança de status de consentimento≤ 1,5 segundo após o evento

Em caso de falha no recebimento (timeout, 5xx), a detentora realiza retries automáticos.


Eventos do Consentimento Recorrente

EventoQuando ocorre
RECURRING_CONSENT_STATUS_UPDATEDO consentimento mudou de status
RECURRING_CONSENT_AWAITING_AUTHORISATIONConsentimento criado, aguardando aprovação
RECURRING_CONSENT_PARTIALLY_ACCEPTEDAprovado por parte dos titulares (alçada dupla)
RECURRING_CONSENT_AUTHORISEDConsentimento totalmente autorizado — pronto para pagamentos
RECURRING_CONSENT_REJECTEDRejeitado pelo usuário, expirado ou cancelado antes de AUTHORISED
RECURRING_CONSENT_REVOKEDRevogado após AUTHORISED
RECURRING_CONSENT_CONSUMEDExpirado após AUTHORISED (limite de data ou valor atingido)

Eventos de Pagamento Pix Recorrente

EventoQuando ocorre
RECURRING_PAYMENT_STATUS_UPDATEDUm pagamento mudou de status
RECURRING_PAYMENT_RCVDPagamento recebido pela detentora
RECURRING_PAYMENT_ACCPVerificações concluídas — pronto para liquidação
RECURRING_PAYMENT_ACPDSubmetido ao SPI — aguardando confirmação
RECURRING_PAYMENT_SCHDPagamento agendado com sucesso
RECURRING_PAYMENT_PDNGRetido temporariamente pela detentora para análise
RECURRING_PAYMENT_ACSCLiquidado com sucesso ✅
RECURRING_PAYMENT_RJCTRejeitado pela detentora ou pelo SPI ❌
RECURRING_PAYMENT_CANCCancelado pelo usuário antes da liquidação

Estrutura dos Payloads

Webhook de Pagamento

{
  "event": "RECURRING_PAYMENT_STATUS_UPDATED",
  "timestamp": "2026-06-25T10:01:30Z",
  "data": {
    "paymentInitiationId": "6a3d1e159c0f8d0011779569",
    "recurringConsentId": "urn:bricks-demo:f4b0c161-f5f6-4adb-ae63-21a2160238d0",
    "recurringPaymentId": "pmt-rec-abc123def456",
    "originalRecurringPaymentId": null,
    "endToEndId": "E13935893202606250431HnY18krNL5P",
    "previousStatus": "ACPD",
    "currentStatus": "ACSC",
    "amount": "150.00",
    "currency": "BRL",
    "date": "2026-06-25",
    "paymentReference": "R/2026-05-22/P1W",
    "updatedAt": "2026-06-25T10:01:28Z",
    "rejectionReason": null
  }
}
CampoTipoDescrição
eventstringTipo do evento
timestampstring (ISO 8601)Data/hora do disparo (UTC)
data.paymentInitiationIdstringID da payment initiation
data.recurringConsentIdstringURN do consentimento recorrente
data.recurringPaymentIdstringID do pagamento recorrente
data.originalRecurringPaymentIdstring | nullID do pagamento original (preenchido em retentativas)
data.endToEndIdstringEndToEndId da transação no SPI
data.previousStatusstringStatus anterior do pagamento
data.currentStatusstringNovo status do pagamento
data.paymentReferencestringReferência da recorrência (R/{date}/{interval})
data.rejectionReasonstring | nullCódigo do motivo de rejeição (apenas quando RJCT)

Webhook de Consentimento Recorrente

{
  "event": "RECURRING_CONSENT_STATUS_UPDATED",
  "timestamp": "2026-06-25T12:27:02Z",
  "data": {
    "paymentInitiationId": "6a3d1e159c0f8d0011779569",
    "recurringConsentId": "urn:bricks-demo:f4b0c161-f5f6-4adb-ae63-21a2160238d0",
    "previousStatus": "AWAITING_AUTHORISATION",
    "currentStatus": "AUTHORISED",
    "updatedAt": "2026-06-25T12:27:00Z",
    "startDateTime": "2026-05-22T00:00:00Z",
    "recurringConfiguration": {
      "automatic": {
        "contractId": "XE00038166201907261559y6j6",
        "interval": "SEMANAL"
      }
    }
  }
}

Status de Pagamento

StatusSiglaDescrição
RCVDReceivedRequisição recebida pela detentora — validações em andamento
ACCPAccepted Customer ProfileVerificações concluídas — pronto para submissão ao SPI
ACPDAccepted Clearing ProcessedSubmetido ao SPI — aguardando confirmação de liquidação
ACSCAccepted Settlement CompletedLiquidado com sucesso ✅
SCHDScheduledAgendado para liquidação futura
PDNGPendingRetido temporariamente para análise (não aplicável a Sweeping Accounts)
RJCTRejectedRejeitado pela detentora ou pelo SPI ❌
CANCCancelledCancelado pelo usuário antes da liquidação

Status de Consentimento Recorrente

StatusDescrição
AWAITING_AUTHORISATIONCriado — aguardando aprovação (prazo: até 60 minutos)
PARTIALLY_ACCEPTEDAprovado parcialmente (alçada dupla) — aguardando demais titulares
AUTHORISEDTotalmente autorizado — pagamentos podem ser executados
REJECTEDRejeitado — por expiração, cancelamento ou recusa do usuário
REVOKEDRevogado após AUTHORISED — nenhum novo pagamento aceito
CONSUMEDEncerrado por expiração de vigência ou atingimento de limite total

Fluxo de Retentativa via Webhook

O webhook de RJCT é o gatilho para a lógica de retentativa da ITP:

flowchart TD
    A["Webhook: RECURRING_PAYMENT_RJCT\nrecurringPaymentId + rejectionReason"] --> B{Contabiliza\ntentativa?}

    B -->|Não| C["Corrigir payload\nChamar POST /payments novamente\n(sem retry endpoint)"]
    B -->|Sim| D{Permite nova\ntentativa intradia?}

    D -->|Sim| E{Exige novo\nendToEndId?}
    D -->|Não| F{Ainda dentro\ndo prazo extradia?}

    E -->|Sim| G["Gerar novo endToEndId\nChamar POST /retry\naté as 12h do mesmo dia"]
    E -->|Não| H["Detentora gerencia\nretentativa automaticamente\n(2ª janela 18h–21h)"]

    F -->|Sim| I["Enviar POST /retry\nnova date (D+1)\naté 23h59 do dia anterior"]
    F -->|Não| J["❌ Encerrar ciclo\nNotificar recebedor"]

    style C fill:#fef9c3,stroke:#f39c12,color:#333
    style G fill:#c6e2f5,stroke:#2980b9,color:#333
    style H fill:#c6e2f5,stroke:#2980b9,color:#333
    style I fill:#c6e2f5,stroke:#2980b9,color:#333
    style J fill:#f5c6c6,stroke:#c0392b,color:#333

Implementação do Endpoint Receptor

// Node.js / Express
app.post('/webhooks/automatic-payments', async (req, res) => {
  // 1. Responder 200 imediatamente
  res.status(200).json({ received: true });

  const { event, data } = req.body;

  // 2. Processar de forma assíncrona
  try {
    switch (event) {
      case 'RECURRING_PAYMENT_STATUS_UPDATED':
        await handlePaymentStatusUpdate(data);
        break;

      case 'RECURRING_PAYMENT_RJCT':
        await handlePaymentRejection(data);
        // Verificar rejectionReason e decidir retentativa
        break;

      case 'RECURRING_PAYMENT_ACSC':
        await confirmPaymentSuccess(data);
        break;

      case 'RECURRING_CONSENT_AUTHORISED':
        await activateConsent(data.paymentInitiationId);
        break;

      case 'RECURRING_CONSENT_REVOKED':
      case 'RECURRING_CONSENT_CONSUMED':
        await deactivateConsent(data.paymentInitiationId, event);
        break;
    }
  } catch (error) {
    console.error('Erro ao processar webhook:', error);
    // Não retornar erro HTTP — o 200 já foi enviado
  }
});

async function handlePaymentRejection(data) {
  const { recurringPaymentId, rejectionReason, date } = data;

  const errorTable = {
    'SALDO_INSUFICIENTE':            { contabiliza: true,  novaIntradia: true,  novoE2eId: false },
    'PAGAMENTO_RECUSADO_SPI':        { contabiliza: true,  novaIntradia: true,  novoE2eId: true  },
    'FALHA_INFRAESTRUTURA_SPI':      { contabiliza: true,  novaIntradia: true,  novoE2eId: true  },
    'PAGAMENTO_DIVERGENTE_CONSENTIMENTO': { contabiliza: false, novaIntradia: false, novoE2eId: false },
    'CONSENTIMENTO_INVALIDO':        { contabiliza: null,  novaIntradia: false, novoE2eId: false  },
  };

  const rule = errorTable[rejectionReason];
  if (!rule || !rule.novaIntradia) return; // sem retentativa possível

  const agora = new Date();
  const prazoIntradia = new Date(agora);
  prazoIntradia.setHours(12, 0, 0, 0);

  if (agora < prazoIntradia && rule.novoE2eId) {
    // Enviar retry com novo endToEndId
    await submitRetry(recurringPaymentId, date, gerarNovoE2eId());
  }
}

Segurança

Validação de Assinatura

x-celcoin-signature: sha256=abc123def456...
const crypto = require('crypto');

// IMPORTANTE: use o body RAW (string), não o objeto JSON parseado
app.post('/webhooks/automatic-payments',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['x-celcoin-signature'];
    const expected = 'sha256=' +
      crypto.createHmac('sha256', process.env.WEBHOOK_SECRET)
            .update(req.body)
            .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.status(401).json({ error: 'Assinatura inválida' });
    }

    res.status(200).json({ received: true });
    processEvent(JSON.parse(req.body));
  }
);

Headers do Webhook

HeaderDescrição
Content-Typeapplication/json
x-celcoin-signatureAssinatura HMAC-SHA256 do payload raw
x-celcoin-event-idID único do evento (para deduplicação)
x-celcoin-timestampTimestamp do disparo

Deduplicação

// Usando Redis para deduplicação
async function processWebhook(req, res) {
  res.status(200).json({ received: true });

  const eventId = req.headers['x-celcoin-event-id'];

  // Tentativa de inserção atômica — falha se já existir
  const inserted = await redis.set(
    `webhook:${eventId}`, '1',
    { NX: true, EX: 86400 } // expira em 24h
  );

  if (!inserted) {
    console.log(`Evento duplicado ignorado: ${eventId}`);
    return;
  }

  await processEvent(req.body);
}

Pontos de Atenção

⚠️

Webhook de RJCT é o gatilho das retentativas: A ITP deve processar o rejectionReason imediatamente ao receber o evento RJCT para decidir sobre a retentativa intradia. O prazo até as 12h é curto — implemente processamento assíncrono rápido (fila de alta prioridade).

⚠️

originalRecurringPaymentId identifica retentativas: Quando um webhook de ACSC chega com originalRecurringPaymentId preenchido, ele corresponde a uma retentativa bem-sucedida — não a um novo ciclo. Use esse campo para rastrear a genealogia dos pagamentos.

⚠️

Responda 200 antes de processar: O timeout da detentora é curto. Qualquer lógica de retentativa ou notificação deve ser feita de forma assíncrona após o 200.

⚠️

Não confie apenas em webhooks: Para retentativas, combine com polling via GET /pix/recurring-payments/:id como fallback, especialmente em horários críticos (próximo às 12h e às 21h).

⚠️

SCHD com consentimento revogado: Se chegar um evento RECURRING_PAYMENT_SCHD seguido de RECURRING_CONSENT_REVOKED no mesmo dia, o pagamento SCHD deve ser liquidado normalmente. Não cancele pagamentos agendados para o dia da revogação.

⚠️

HTTPS com certificado válido: Endpoints sem TLS ou com certificado auto-assinado são rejeitados pela detentora.

⚠️

SLA regulatório de 1,5 segundo: Se notificações não chegam em tempo razoável, implemente polling como contingência e reporte ao suporte.