Webhooks - Pix Instantâneo ITP

Visão Geral

Os webhooks notificam a ITP sobre mudanças de status em consentimentos e pagamentos Pix de forma assíncrona e em tempo real, eliminando a necessidade de polling contínuo.

A detentora de conta (ASPSP) dispara as notificações para a URL registrada na ITP assim que ocorre uma mudança de estado. A ITP deve expor um endpoint HTTPS acessível publicamente para receber esses eventos.


Configuração

Requisitos do Endpoint Receptor

RequisitoDescrição
ProtocoloHTTPS obrigatório (certificado válido)
Método aceitoPOST
Tempo de resposta≤ 5 segundos (responda 200 imediatamente)
IdempotênciaO mesmo evento pode ser entregue mais de uma vez
Disponibilidade99,5% ou superior — falhas causam retries

SLA de Notificação

De acordo com as regras do Open Finance Brasil:

EventoSLA máximo
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 Disponíveis

Eventos de Pagamento

EventoDescrição
PAYMENT_STATUS_UPDATEDUm pagamento Pix mudou de status
PAYMENT_COMPLETEDPagamento liquidado com sucesso — ACSC
PAYMENT_REJECTEDPagamento rejeitado — RJCT

Eventos de Consentimento

EventoDescrição
CONSENT_STATUS_UPDATEDO consentimento mudou de status
CONSENT_AUTHORISEDUsuário autorizou o consentimento
CONSENT_CONSUMEDConsentimento utilizado (pagamento executado)
CONSENT_REJECTEDUsuário rejeitou o consentimento
CONSENT_EXPIREDConsentimento expirou sem utilização

Estrutura dos Payloads

Webhook de Pagamento

{
  "event": "PAYMENT_STATUS_UPDATED",
  "timestamp": "2026-05-31T10:01:30Z",
  "data": {
    "paymentInitiationId": "ZVjnvOXJSlgth9MVDS4HmdvyhBlHt_s1MPhMNMBhGSU",
    "paymentId": "pix-abc123def456",
    "endToEndId": "E1234567820260531000012345ABCDE",
    "previousStatus": "PDNG",
    "currentStatus": "ACSC",
    "amount": "1.15",
    "currency": "BRL",
    "date": "2026-05-31",
    "updatedAt": "2026-05-31T10:01:28Z",
    "rejectionReason": null
  }
}
CampoTipoDescrição
eventstringTipo do evento
timestampstringData/hora do disparo do webhook (ISO 8601 UTC)
data.paymentInitiationIdstringID da payment initiation
data.paymentIdstringID do pagamento Pix
data.endToEndIdstringEndToEndId da transação no SPB
data.previousStatusstringStatus anterior
data.currentStatusstringNovo status
data.amountstringValor do pagamento em BRL
data.rejectionReasonstring | nullCódigo do motivo de rejeição (apenas quando RJCT)

Webhook de Consentimento

{
  "event": "CONSENT_STATUS_UPDATED",
  "timestamp": "2026-05-31T10:05:00Z",
  "data": {
    "paymentInitiationId": "ZVjnvOXJSlgth9MVDS4HmdvyhBlHt_s1MPhMNMBhGSU",
    "consentId": "urn:celcoin:ZVjnvOXJ...",
    "previousStatus": "AWAITING_AUTHORISATION",
    "currentStatus": "AUTHORISED",
    "updatedAt": "2026-05-31T10:04:58Z",
    "brandId": "6900de69dfdf118e980e10ec"
  }
}
CampoTipoDescrição
data.paymentInitiationIdstringID da payment initiation
data.consentIdstringURN do consentimento na detentora
data.previousStatusstringStatus anterior do consentimento
data.currentStatusstringNovo status do consentimento

Status de Pagamento

StatusSiglaDescrição
PDNGPendingAguardando processamento pelo SPI
ACSPAccepted Settlement In ProcessAceito pelo SPI, em liquidação
ACSCAccepted Settlement CompletedLiquidado com sucesso ✅
RJCTRejectedRejeitado ❌

Status de Consentimento

StatusDescrição
AWAITING_AUTHORISATIONAguardando autorização do usuário
AUTHORISEDConsentimento aprovado — pronto para pagamento
CONSUMEDPagamento executado com sucesso — consentimento encerrado
REJECTEDUsuário rejeitou ou timeout na jornada de autorização
EXPIREDConsentimento expirou sem utilização

Implementação do Endpoint Receptor

// Node.js / Express
app.post('/webhooks/itp-pix', 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 'PAYMENT_STATUS_UPDATED':
        if (data.currentStatus === 'ACSC') {
          await confirmPaymentSuccess(data.paymentInitiationId, data.endToEndId);
        } else if (data.currentStatus === 'RJCT') {
          await handlePaymentRejection(data.paymentInitiationId, data.rejectionReason);
        }
        break;

      case 'CONSENT_STATUS_UPDATED':
        await updateConsentStatus(data.paymentInitiationId, data.currentStatus);
        break;

      case 'CONSENT_REJECTED':
        await notifyUserConsentDenied(data.paymentInitiationId);
        break;
    }
  } catch (error) {
    // Log interno — não retornar erro HTTP (webhook já foi confirmado)
    console.error('Erro ao processar webhook:', error);
  }
});

Crítico: Sempre responda HTTP 200 antes de executar qualquer lógica de negócio. O processamento deve ser assíncrono (fila, worker) para garantir resposta dentro do timeout.


Segurança

Validação de Assinatura

Os webhooks incluem assinatura no header para garantir autenticidade:

x-celcoin-signature: sha256=abc123def456...

Valide antes de processar:

const crypto = require('crypto');

function isValidWebhookSignature(rawBody, signature, secret) {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret)
          .update(rawBody)   // usar o body bruto (string), não o objeto
          .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// No middleware Express:
app.post('/webhooks/itp-pix', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-celcoin-signature'];
  if (!isValidWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Assinatura inválida' });
  }
  // ... processar
});

Headers do Webhook

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

Idempotência e Deduplicação

Use x-celcoin-event-id para evitar processar o mesmo evento duas vezes:

const redis = require('redis');
const client = redis.createClient();

async function isEventAlreadyProcessed(eventId) {
  const result = await client.set(`event:${eventId}`, '1', {
    NX: true,     // somente se não existir
    EX: 86400     // expirar em 24h
  });
  return result === null; // null = já existia
}

app.post('/webhooks/itp-pix', async (req, res) => {
  res.status(200).json({ received: true });

  const eventId = req.headers['x-celcoin-event-id'];
  if (await isEventAlreadyProcessed(eventId)) return;

  processEvent(req.body);
});

Pontos de Atenção

⚠️

Responda 200 imediatamente: O timeout do webhook é curto. Qualquer lógica de negócio deve ser processada de forma assíncrona. Respostas lentas causam retries desnecessários.

⚠️

Não confie apenas em webhooks: Webhooks podem falhar ou sofrer atrasos. Para pagamentos críticos, combine webhooks com polling via GET /payment-initiation/:id como fallback.

⚠️

HTTPS obrigatório: Endpoints HTTP sem TLS são rejeitados. Use certificado válido (não auto-assinado) em produção.

⚠️

Valide a assinatura: Sempre verifique x-celcoin-signature antes de processar o evento para prevenir spoofing.

⚠️

Use o body bruto para validação de assinatura: A assinatura é calculada sobre o payload raw (string), não sobre o objeto JSON parseado. Configure o middleware para preservar o body original.

⚠️

RJCT com rejectionReason: Ao receber rejeição, verifique o código de motivo para determinar se é retriável. Consulte Possíveis Erros de Pagamento para a lista completa.

⚠️

SLA regulatório: O Open Finance Brasil exige que a detentora notifique em ≤ 1,5 segundo. Se seu sistema não receber a notificação em tempo razoável, implemente polling como contingência.