Visão Geral
O webhook REFUND é enviado quando uma devolução PIX é processada. Existem dois cenários:
CashInReversal : Você devolveu um PIX recebido (via /pix/:e2eid/devolucao/:id)
CashOutReversal : Alguém devolveu um PIX que você enviou
Quando é enviado
Devolução de PIX recebido confirmada (você devolvendo)
Devolução de PIX enviado recebida (alguém devolvendo para você)
Estrutura do Payload
{
"type" : "REFUND" ,
"data" : {
"id" : 123 ,
"txId" : "7978c0c97ea847e78e8849634473c1f1" ,
"pixKey" : "7d9f0335-8dcc-4054-9bf9-0dbd61d36906" ,
"status" : "REFUNDED" ,
"payment" : {
"amount" : "100.00" ,
"currency" : "BRL"
},
"refunds" : [
{
"status" : "LIQUIDATED" ,
"payment" : {
"amount" : 50.00 ,
"currency" : "BRL"
},
"errorCode" : null ,
"eventDate" : "2024-01-15T10:30:00.000Z" ,
"endToEndId" : "D12345678901234567890123456789012" ,
"information" : "Devolução solicitada pelo recebedor"
}
],
"createdAt" : "2024-01-15T09:00:00.000Z" ,
"errorCode" : null ,
"endToEndId" : "E12345678901234567890123456789012" ,
"ticketData" : {},
"webhookType" : "REFUND" ,
"debtorAccount" : {
"ispb" : null ,
"name" : null ,
"issuer" : null ,
"number" : null ,
"document" : null ,
"accountType" : null
},
"idempotencyKey" : "7978c0c97ea847e78e8849634473c1f1" ,
"creditDebitType" : "DEBIT" ,
"creditorAccount" : {
"ispb" : "18236120" ,
"name" : "NU PAGAMENTOS S.A." ,
"issuer" : "260" ,
"number" : "12345-6" ,
"document" : "123.xxx.xxx-xx" ,
"accountType" : null
},
"localInstrument" : "DICT" ,
"transactionType" : "PIX" ,
"remittanceInformation" : "Devolução parcial"
}
}
Diferença entre CashInReversal e CashOutReversal
CashInReversal
CashOutReversal
Você devolveu um PIX recebido.creditDebitType = DEBIT (saindo da sua conta)
debtorAccount = Sua conta
creditorAccount = Quem vai receber de volta
Exemplo : Você recebeu R100 , d e p o i s d e v o l v e u R 100, depois devolveu R 100 , d e p o i s d e v o l v e u R 50.Alguém devolveu um PIX que você enviou.creditDebitType = CREDIT (entrando na sua conta)
debtorAccount = Quem está devolvendo
creditorAccount = Sua conta
Exemplo : Você enviou R100 , o d e s t i n a t a ˊ r i o d e v o l v e u R 100, o destinatário devolveu R 100 , o d es t ina t a ˊ r i o d e v o l v e u R 30.
Campos Importantes
Sempre "REFUND" para devoluções.
ID da transação original (não da devolução).
Status da transação original após a devolução:
REFUNDED: Devolução processada
ERROR: Falha na devolução
Valor da transação original , não da devolução. Valor original. String com 2 casas decimais.
Lista de devoluções realizadas. Contém detalhes de cada devolução. Status da devolução: LIQUIDATED ou ERROR.
Valor da devolução. Atenção : É number, não string!
E2E ID da devolução (diferente do E2E da transação original).
Direção do dinheiro:
DEBIT: Saindo da sua conta (CashInReversal)
CREDIT: Entrando na sua conta (CashOutReversal)
E2E ID da transação original .
Processando o Webhook
Exemplo Node.js
interface RefundWebhook {
type : 'REFUND' ;
data : {
id : number ;
txId : string | null ;
status : 'REFUNDED' | 'ERROR' ;
payment : {
amount : string ;
currency : string ;
};
refunds : Array <{
status : 'LIQUIDATED' | 'ERROR' ;
payment : {
amount : number ; // number, não string!
currency : string ;
};
endToEndId : string ;
eventDate : string ;
information : string | null ;
}>;
endToEndId : string ;
creditDebitType : 'CREDIT' | 'DEBIT' ;
};
}
async function handleRefund ( webhook : RefundWebhook ) {
const { data } = webhook ;
// Identificar tipo de devolução
const isCashInReversal = data . creditDebitType === 'DEBIT' ;
if ( isCashInReversal ) {
// Você devolveu um PIX recebido
await handleCashInReversal ( data );
} else {
// Alguém devolveu um PIX que você enviou
await handleCashOutReversal ( data );
}
}
async function handleCashInReversal ( data : RefundWebhook [ 'data' ]) {
// Encontrar transação original
const original = await findTransactionByE2eId ( data . endToEndId );
// Processar cada devolução
for ( const refund of data . refunds ) {
if ( refund . status === 'LIQUIDATED' ) {
// Devolução confirmada - debitar do saldo
await processRefundOut ({
originalId: original . id ,
refundAmount: refund . payment . amount , // já é number
refundE2eId: refund . endToEndId ,
});
console . log ( `Devolvido R$ ${ refund . payment . amount } do PIX ${ original . id } ` );
}
}
}
async function handleCashOutReversal ( data : RefundWebhook [ 'data' ]) {
// Encontrar transferência original
const original = await findTransferByE2eId ( data . endToEndId );
// Processar cada devolução recebida
for ( const refund of data . refunds ) {
if ( refund . status === 'LIQUIDATED' ) {
// Devolução recebida - creditar no saldo
await processRefundIn ({
originalId: original . id ,
refundAmount: refund . payment . amount ,
refundE2eId: refund . endToEndId ,
});
console . log ( `Recebido R$ ${ refund . payment . amount } de devolução` );
}
}
}
Exemplo Python
from decimal import Decimal
def handle_refund ( webhook : dict ):
data = webhook[ 'data' ]
# Identificar tipo
is_cash_in_reversal = data[ 'creditDebitType' ] == 'DEBIT'
if is_cash_in_reversal:
handle_cash_in_reversal(data)
else :
handle_cash_out_reversal(data)
def handle_cash_in_reversal ( data : dict ):
"""Você devolveu um PIX recebido"""
original = find_transaction_by_e2e(data[ 'endToEndId' ])
for refund in data[ 'refunds' ]:
if refund[ 'status' ] == 'LIQUIDATED' :
# Já é number, converter para Decimal
amount = Decimal( str (refund[ 'payment' ][ 'amount' ]))
process_refund_out(
original_id = original.id,
refund_amount = amount,
refund_e2e = refund[ 'endToEndId' ]
)
def handle_cash_out_reversal ( data : dict ):
"""Alguém devolveu um PIX que você enviou"""
original = find_transfer_by_e2e(data[ 'endToEndId' ])
for refund in data[ 'refunds' ]:
if refund[ 'status' ] == 'LIQUIDATED' :
amount = Decimal( str (refund[ 'payment' ][ 'amount' ]))
process_refund_in(
original_id = original.id,
refund_amount = amount,
refund_e2e = refund[ 'endToEndId' ]
)
Devoluções Parciais
Uma transação pode ter múltiplas devoluções parciais. O array refunds contém todas:
{
"type" : "REFUND" ,
"data" : {
"payment" : { "amount" : "100.00" }, // Valor original: R$ 100
"refunds" : [
{
"payment" : { "amount" : 30.00 }, // 1ª devolução: R$ 30
"eventDate" : "2024-01-15T10:00:00Z"
},
{
"payment" : { "amount" : 50.00 }, // 2ª devolução: R$ 50
"eventDate" : "2024-01-15T11:00:00Z"
}
]
}
}
Cálculo do saldo de devolução:
const valorOriginal = parseFloat ( data . payment . amount ); // 100.00
const totalDevolvido = data . refunds
. filter ( r => r . status === 'LIQUIDATED' )
. reduce (( sum , r ) => sum + r . payment . amount , 0 ); // 80.00
const saldoDisponivel = valorOriginal - totalDevolvido ; // 20.00
Atenção: amount é number em refunds
Dentro do array refunds, o campo payment.amount é number , não string ! // data.payment.amount → string "100.00"
// data.refunds[0].payment.amount → number 50.00
// CORRETO
const refundAmount = data . refunds [ 0 ]. payment . amount ; // 50.00 (number)
// ERRADO - não precisa de parseFloat
const refundAmount = parseFloat ( data . refunds [ 0 ]. payment . amount );
Idempotência
Use uma combinação de data.id e refunds[].endToEndId para idempotência:
async function handleWebhook ( webhook : RefundWebhook ) {
for ( const refund of webhook . data . refunds ) {
const key = `refund: ${ webhook . data . id } : ${ refund . endToEndId } ` ;
const isProcessed = await redis . sismember ( 'processed' , key );
if ( isProcessed ) {
continue ; // Já processado
}
await redis . sadd ( 'processed' , key );
await processRefund ( webhook . data , refund );
}
}
Tratamento de Erros
Se refund.status === 'ERROR', a devolução falhou:
for ( const refund of data . refunds ) {
if ( refund . status === 'ERROR' ) {
console . error ( `Devolução falhou: ${ refund . errorCode } ` );
// Notificar sobre falha
await notifyRefundFailed ({
originalE2eId: data . endToEndId ,
refundE2eId: refund . endToEndId ,
errorCode: refund . errorCode ,
});
}
}
Boas Práticas
Identifique o tipo pela creditDebitType
Use creditDebitType para determinar se é CashInReversal (DEBIT) ou CashOutReversal (CREDIT).
Processe todas as devoluções do array
O array refunds pode conter múltiplas devoluções parciais. Itere por todas.
Cuidado com os tipos de amount
Atualize o saldo corretamente
CashInReversal: Debita do seu saldo
CashOutReversal: Credita no seu saldo
Próximos Passos