Safira Paydocs
Webhooks V2

RECEIVE

Overview

The RECEIVE webhook is sent when a PIX is received in your account. This event indicates that someone paid a QR Code generated by your application or made a direct transfer to your PIX key.

When it is sent

  • QR Code (charge) payment confirmed
  • Direct transfer to the account's PIX key

Payload Structure

{
  "type": "RECEIVE",
  "data": {
    "id": 123,
    "txId": "7978c0c97ea847e78e8849634473c1f1",
    "pixKey": "7d9f0335-8dcc-4054-9bf9-0dbd61d36906",
    "status": "LIQUIDATED",
    "payment": {
      "amount": "100.00",
      "currency": "BRL"
    },
    "refunds": [],
    "createdAt": "2024-01-15T10:30:00.000Z",
    "errorCode": null,
    "endToEndId": "E12345678901234567890123456789012",
    "ticketData": {},
    "webhookType": "RECEIVE",
    "debtorAccount": {
      "ispb": "18236120",
      "name": "NU PAGAMENTOS S.A.",
      "issuer": "260",
      "number": "12345-6",
      "document": "123.xxx.xxx-xx",
      "accountType": null
    },
    "idempotencyKey": null,
    "creditDebitType": "CREDIT",
    "creditorAccount": {
      "ispb": null,
      "name": null,
      "issuer": null,
      "number": null,
      "document": null,
      "accountType": null
    },
    "localInstrument": "DICT",
    "transactionType": "PIX",
    "remittanceInformation": "Pagamento pedido #12345"
  }
}

Important Fields

typestring

Always "RECEIVE" for received PIX.

data.idnumber

Transaction ID. Use for idempotency.

data.txIdstring

Charge identifier (txid from the /cob endpoint). Can be null for direct transfers.

data.endToEndIdstring

End to End ID - unique identifier of the PIX transaction at the Central Bank.

data.statusstring

Transaction status:

  • LIQUIDATED: Payment confirmed (success)
  • ERROR: Processing failure
data.paymentobject

data.debtorAccountobject

Data of who paid (the payer/sender).

data.creditDebitTypestring

Always "CREDIT" for receipts.

data.refundsarray

List of refunds. Empty for transactions without refunds.

data.remittanceInformationstring

Transfer description (if provided by the payer).

Processing the Webhook

Node.js Example

interface ReceiveWebhook {
  type: 'RECEIVE';
  data: {
    id: number;
    txId: string | null;
    status: 'LIQUIDATED' | 'ERROR';
    payment: {
      amount: string;
      currency: string;
    };
    endToEndId: string;
    debtorAccount: {
      name: string | null;
      document: string | null;
    };
    remittanceInformation: string | null;
  };
}

async function handleReceive(webhook: ReceiveWebhook) {
  const { data } = webhook;

  if (data.status !== 'LIQUIDATED') {
    console.log(`PIX not confirmed: ${data.status}`);
    return;
  }

  // Convert value from string to number
  const amount = parseFloat(data.payment.amount);

  // Find order by txId (if it is a charge)
  if (data.txId) {
    const order = await findOrderByTxId(data.txId);
    if (order) {
      await markOrderAsPaid(order.id, {
        amount,
        endToEndId: data.endToEndId,
        payer: data.debtorAccount.name,
      });
      return;
    }
  }

  // Receipt without an associated charge
  await createGenericCredit({
    amount,
    endToEndId: data.endToEndId,
    payer: data.debtorAccount.name,
    description: data.remittanceInformation,
  });
}

Python Example

from decimal import Decimal

def handle_receive(webhook: dict):
    data = webhook['data']

    if data['status'] != 'LIQUIDATED':
        print(f"PIX not confirmed: {data['status']}")
        return

    # Convert value
    amount = Decimal(data['payment']['amount'])

    # Process by txId if it exists
    if data.get('txId'):
        order = find_order_by_txid(data['txId'])
        if order:
            mark_order_as_paid(
                order_id=order.id,
                amount=amount,
                e2e_id=data['endToEndId'],
                payer=data['debtorAccount'].get('name')
            )
            return

    # Generic credit
    create_generic_credit(
        amount=amount,
        e2e_id=data['endToEndId'],
        payer=data['debtorAccount'].get('name'),
        description=data.get('remittanceInformation')
    )

Correlation with Charge

If the PIX was paid via a QR Code generated by the /cob/:txid endpoint, the txId field will contain the identifier:

{
  "type": "RECEIVE",
  "data": {
    "txId": "7978c0c97ea847e78e8849634473c1f1",
    // ...
  }
}

Use this field to correlate with your internal records:

// Create charge
const charge = await createCob('my-txid-123', { valor: '100.00' });

// Save association
await saveOrder({
  orderId: 'order-456',
  txId: 'my-txid-123',
  status: 'PENDING'
});

// In the RECEIVE webhook
if (webhook.data.txId === 'my-txid-123') {
  await updateOrder('order-456', { status: 'PAID' });
}

Error Handling

If status === 'ERROR', check the errorCode field:

if (data.status === 'ERROR') {
  console.error(`PIX error: ${data.errorCode}`);

  // Notify about failure
  await notifyPaymentError({
    txId: data.txId,
    errorCode: data.errorCode,
  });
}

Idempotency

Use data.id to avoid duplicate processing:

const PROCESSED_KEY = 'processed_webhooks';

async function handleWebhook(webhook: ReceiveWebhook) {
  const webhookId = `receive:${webhook.data.id}`;

  // Check if already processed
  const isProcessed = await redis.sismember(PROCESSED_KEY, webhookId);
  if (isProcessed) {
    console.log(`Webhook ${webhookId} already processed`);
    return;
  }

  // Mark as processed BEFORE processing
  await redis.sadd(PROCESSED_KEY, webhookId);

  // Process
  await handleReceive(webhook);
}

Best Practices

Next Steps

On this page