docs
Webhooks V2

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:

  1. Evento refund_in (CashInReversal): você devolveu um PIX recebido (por exemplo, via PUT /api/pix/:e2eid/devolucao/:id)
  2. 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 volta

Exemplo: 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

typestring

Sempre "REFUND".

data.idnumber

ID da transação original (não da devolução).

data.txIdstring

Identificador 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.statusstring

Status da devolução que gerou o webhook:

  • REFUNDED: devolução concluída
  • ERROR: falha na devolução
  • PENDING: devolução em processamento
data.paymentobject

Valor da transação original, não da devolução.

data.refundsarray

Contém um único item: a devolução que gerou este webhook. Cada devolução parcial chega no seu próprio webhook.

data.providerIdstring

ID da devolução no provedor de liquidação.

data.creditDebitTypestring

Direção do dinheiro:

  • DEBIT: saindo da sua conta (refund_in)
  • CREDIT: entrando na sua conta (refund_out)
data.endToEndIdstring

End to End ID da transação original.

data.createdAtstring

Data 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,
  });
}

Boas Práticas

Próximos Passos

Nesta página