REFUND
Visão Geral
O webhook REFUND é enviado quando uma devolução PIX muda de estado. Existem dois cenários, cada um ligado a um evento cadastrado no painel:
- Evento
refund_in(CashInReversal): você devolveu um PIX recebido (por exemplo, viaPUT /api/pix/:e2eid/devolucao/:id) - Evento
refund_out(CashOutReversal): o destinatário devolveu um PIX que você enviou
As duas situações chegam com type: "REFUND". Para receber no formato V2, cadastre as URLs desses eventos com o formato PIX BACEN.
Estrutura do Payload
{
"data": {
"id": 12345,
"txId": "7978c0c97ea847e78e8849634473c1f1",
"pixKey": "7d9f0335-8dcc-4054-9bf9-0dbd61d36906",
"status": "REFUNDED",
"payment": {
"amount": "100",
"currency": "BRL"
},
"refunds": [
{
"status": "LIQUIDATED",
"payment": {
"amount": 50,
"currency": "BRL"
},
"errorCode": null,
"eventDate": "2024-01-15T10:30:00.000Z",
"endToEndId": "D12345678202401151030abcdefghijk",
"information": "Devolução solicitada pelo recebedor"
}
],
"createdAt": "2024-01-15T09:00:00.000Z",
"errorCode": null,
"endToEndId": "E12345678202401150900abcdefghijk",
"providerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"ticketData": {},
"webhookType": "REFUND",
"debtorAccount": {
"ispb": null,
"name": null,
"bankName": null,
"issuer": null,
"number": null,
"document": null,
"accountType": null
},
"idempotencyKey": "7978c0c97ea847e78e8849634473c1f1",
"creditDebitType": "DEBIT",
"creditorAccount": {
"ispb": "12345678",
"name": "Carlos Oliveira",
"bankName": "Banco Exemplo S.A.",
"issuer": "000",
"number": "123456",
"document": "12345678909",
"accountType": null
},
"localInstrument": "DICT",
"transactionType": "PIX",
"remittanceInformation": "Devolução solicitada pelo recebedor"
},
"type": "REFUND"
}refund_in x refund_out
Você devolveu um PIX recebido.
creditDebitType = DEBIT (saindo da sua conta)
debtorAccount = Sua conta (campos null)
creditorAccount = Quem recebe de voltaExemplo: você recebeu R$ 100 e devolveu R$ 50.
O destinatário devolveu um PIX que você enviou.
creditDebitType = CREDIT (entrando na sua conta)
debtorAccount = Quem está devolvendo
creditorAccount = Sua conta (campos null)Exemplo: você enviou R$ 100 e o destinatário devolveu R$ 30.
Campos Importantes
typestringSempre "REFUND".
data.idnumberID da transação original (não da devolução).
data.txIdstringIdentificador externo da transação original: o txid da cobrança, ou o x-idempotency-key da transferência original. O mesmo valor vem em data.idempotencyKey.
data.statusstringStatus da devolução que gerou o webhook:
REFUNDED: devolução concluídaERROR: falha na devoluçãoPENDING: devolução em processamento
data.paymentobjectValor da transação original, não da devolução.
data.refundsarrayContém um único item: a devolução que gerou este webhook. Cada devolução parcial chega no seu próprio webhook.
data.providerIdstringID da devolução no provedor de liquidação.
data.creditDebitTypestringDireção do dinheiro:
DEBIT: saindo da sua conta (refund_in)CREDIT: entrando na sua conta (refund_out)
data.endToEndIdstringEnd to End ID da transação original.
data.createdAtstringData de criação da transação original.
Processando o Webhook
Exemplo Node.js
interface RefundWebhook {
type: 'REFUND';
data: {
id: number;
txId: string | null;
status: 'PENDING' | 'REFUNDED' | 'ERROR';
payment: {
amount: string;
currency: 'BRL';
};
refunds: Array<{
status: 'PENDING' | 'LIQUIDATED' | 'ERROR';
payment: {
amount: number;
currency: 'BRL';
};
errorCode: string | null;
endToEndId: string | null;
eventDate: string;
information: string | null;
}>;
endToEndId: string | null;
providerId: string | null;
creditDebitType: 'CREDIT' | 'DEBIT';
};
}
async function handleRefund(webhook: RefundWebhook) {
const { data } = webhook;
const refund = data.refunds[0];
if (data.creditDebitType === 'DEBIT') {
await handleRefundIn(data, refund);
} else {
await handleRefundOut(data, refund);
}
}
async function handleRefundIn(data: RefundWebhook['data'], refund: RefundWebhook['data']['refunds'][number]) {
const original = await findReceiptByE2eId(data.endToEndId);
if (data.status === 'REFUNDED') {
await registerRefundSent({
originalId: original.id,
amount: refund.payment.amount,
refundE2eId: refund.endToEndId,
});
}
}
async function handleRefundOut(data: RefundWebhook['data'], refund: RefundWebhook['data']['refunds'][number]) {
const original = await findTransferByIdempotencyKey(data.txId);
if (data.status === 'REFUNDED') {
await registerRefundReceived({
originalId: original.id,
amount: refund.payment.amount,
refundE2eId: refund.endToEndId,
});
}
}Exemplo Python
from decimal import Decimal
def handle_refund(webhook: dict):
data = webhook['data']
refund = data['refunds'][0]
if data['status'] != 'REFUNDED':
return
amount = Decimal(str(refund['payment']['amount']))
if data['creditDebitType'] == 'DEBIT':
original = find_receipt_by_e2e(data['endToEndId'])
register_refund_sent(original.id, amount, refund['endToEndId'])
else:
original = find_transfer_by_idempotency_key(data['txId'])
register_refund_received(original.id, amount, refund['endToEndId'])Devoluções Parciais
Uma transação pode ter várias devoluções parciais. Cada uma gera o seu próprio webhook REFUND, com o mesmo data.id e data.payment.amount (os da transação original) e um item diferente em refunds.
Para saber quanto ainda pode ser devolvido, some no seu sistema as devoluções concluídas:
const valorOriginal = Number(data.payment.amount);
const totalDevolvido = await sumCompletedRefunds(data.id);
const saldoDisponivel = valorOriginal - totalDevolvido;Atenção: amount é number em refunds
Dentro do array refunds, o campo payment.amount é number, não string.
const originalAmount = data.payment.amount;
const refundAmount = data.refunds[0].payment.amount;originalAmount é uma string (ex.: "100") e refundAmount é um número (ex.: 50).
Idempotência
Como data.id é o da transação original, ele se repete entre devoluções parciais. Deduplique pela devolução:
async function handleWebhook(webhook: RefundWebhook) {
const refund = webhook.data.refunds[0];
const refundKey = refund.endToEndId ?? webhook.data.providerId;
const key = `refund:${webhook.data.id}:${refundKey}:${webhook.data.status}`;
const isNew = await redis.set(`webhook:${key}`, '1', 'NX', 'EX', 60 * 60 * 24 * 7);
if (!isNew) return;
await handleRefund(webhook);
}Tratamento de Erros
Se data.status for ERROR, a devolução falhou. O código fica em data.errorCode e em refunds[0].errorCode:
if (data.status === 'ERROR') {
await notifyRefundFailed({
originalE2eId: data.endToEndId,
errorCode: data.errorCode,
});
}