ITP - Webhooks
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
| Requisito | Descrição |
|---|---|
| Protocolo | HTTPS obrigatório (certificado válido) |
| Método aceito | POST |
| Tempo de resposta | ≤ 5 segundos (responda 200 imediatamente) |
| Idempotência | O mesmo evento pode ser entregue mais de uma vez |
| Disponibilidade | 99,5% ou superior — falhas causam retries |
SLA de Notificação
De acordo com as regras do Open Finance Brasil:
| Evento | SLA 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
| Evento | Descrição |
|---|---|
PAYMENT_STATUS_UPDATED | Um pagamento Pix mudou de status |
PAYMENT_COMPLETED | Pagamento liquidado com sucesso — ACSC |
PAYMENT_REJECTED | Pagamento rejeitado — RJCT |
Eventos de Consentimento
| Evento | Descrição |
|---|---|
CONSENT_STATUS_UPDATED | O consentimento mudou de status |
CONSENT_AUTHORISED | Usuário autorizou o consentimento |
CONSENT_CONSUMED | Consentimento utilizado (pagamento executado) |
CONSENT_REJECTED | Usuário rejeitou o consentimento |
CONSENT_EXPIRED | Consentimento 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
}
}| Campo | Tipo | Descrição |
|---|---|---|
event | string | Tipo do evento |
timestamp | string | Data/hora do disparo do webhook (ISO 8601 UTC) |
data.paymentInitiationId | string | ID da payment initiation |
data.paymentId | string | ID do pagamento Pix |
data.endToEndId | string | EndToEndId da transação no SPB |
data.previousStatus | string | Status anterior |
data.currentStatus | string | Novo status |
data.amount | string | Valor do pagamento em BRL |
data.rejectionReason | string | null | Có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"
}
}| Campo | Tipo | Descrição |
|---|---|---|
data.paymentInitiationId | string | ID da payment initiation |
data.consentId | string | URN do consentimento na detentora |
data.previousStatus | string | Status anterior do consentimento |
data.currentStatus | string | Novo status do consentimento |
Status de Pagamento
| Status | Sigla | Descrição |
|---|---|---|
PDNG | Pending | Aguardando processamento pelo SPI |
ACSP | Accepted Settlement In Process | Aceito pelo SPI, em liquidação |
ACSC | Accepted Settlement Completed | Liquidado com sucesso ✅ |
RJCT | Rejected | Rejeitado ❌ |
Status de Consentimento
| Status | Descrição |
|---|---|
AWAITING_AUTHORISATION | Aguardando autorização do usuário |
AUTHORISED | Consentimento aprovado — pronto para pagamento |
CONSUMED | Pagamento executado com sucesso — consentimento encerrado |
REJECTED | Usuário rejeitou ou timeout na jornada de autorização |
EXPIRED | Consentimento 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 200antes 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
| Header | Descrição |
|---|---|
Content-Type | application/json |
x-celcoin-signature | Assinatura HMAC-SHA256 do payload |
x-celcoin-event-id | ID único do evento (para deduplicação) |
x-celcoin-timestamp | Timestamp 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
Responda200imediatamente: 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 viaGET /payment-initiation/:idcomo 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 verifiquex-celcoin-signatureantes de processar o evento para prevenir spoofing.
Use o body bruto para validação de assinatura: A assinatura é calculada sobre o payloadraw(string), não sobre o objeto JSON parseado. Configure o middleware para preservar o body original.
RJCTcomrejectionReason: 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.
Updated about 2 hours ago