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
Requisito
Detalhe
Protocolo
HTTPS obrigatório (certificado válido, não auto-assinado)
Método
POST
Tempo de resposta
≤ 5 segundos — responda 200 OK imediatamente
Idempotência
O mesmo evento pode ser entregue mais de uma vez
Disponibilidade
Alta disponibilidade recomendada — falhas causam retries da detentora
SLA de Notificação
Conforme regras do Open Finance Brasil:
Evento
SLA 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
Evento
Quando ocorre
RECURRING_CONSENT_STATUS_UPDATED
O consentimento mudou de status
RECURRING_CONSENT_AWAITING_AUTHORISATION
Consentimento criado, aguardando aprovação
RECURRING_CONSENT_PARTIALLY_ACCEPTED
Aprovado por parte dos titulares (alçada dupla)
RECURRING_CONSENT_AUTHORISED
Consentimento totalmente autorizado — pronto para pagamentos
RECURRING_CONSENT_REJECTED
Rejeitado pelo usuário, expirado ou cancelado antes de AUTHORISED
RECURRING_CONSENT_REVOKED
Revogado após AUTHORISED
RECURRING_CONSENT_CONSUMED
Expirado após AUTHORISED (limite de data ou valor atingido)
Eventos de Pagamento Pix Recorrente
Evento
Quando ocorre
RECURRING_PAYMENT_STATUS_UPDATED
Um pagamento mudou de status
RECURRING_PAYMENT_RCVD
Pagamento recebido pela detentora
RECURRING_PAYMENT_ACCP
Verificações concluídas — pronto para liquidação
RECURRING_PAYMENT_ACPD
Submetido ao SPI — aguardando confirmação
RECURRING_PAYMENT_SCHD
Pagamento agendado com sucesso
RECURRING_PAYMENT_PDNG
Retido temporariamente pela detentora para análise
Totalmente autorizado — pagamentos podem ser executados
REJECTED
Rejeitado — por expiração, cancelamento ou recusa do usuário
REVOKED
Revogado após AUTHORISED — nenhum novo pagamento aceito
CONSUMED
Encerrado 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
Header
Descrição
Content-Type
application/json
x-celcoin-signature
Assinatura HMAC-SHA256 do payload raw
x-celcoin-event-id
ID único do evento (para deduplicação)
x-celcoin-timestamp
Timestamp 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 SCHDdeve 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.