docs
Webhooks V2

RECEIVE

Overview

The RECEIVE webhook is sent to the URLs registered for the cash_in event with the PIX BACEN format when a PIX received in your account changes state. It usually means someone paid a charge created by your application.

When it is sent

  • Charge (QR Code) payment confirmed
  • Receipt failure (status: ERROR)

Payload Structure

{
  "data": {
    "id": 12345,
    "txId": "7978c0c97ea847e78e8849634473c1f1",
    "pixKey": "7d9f0335-8dcc-4054-9bf9-0dbd61d36906",
    "status": "LIQUIDATED",
    "payment": {
      "amount": "123.45",
      "currency": "BRL"
    },
    "mismatchAmount": null,
    "refunds": [],
    "createdAt": "2024-01-15T10:30:00.000Z",
    "errorCode": null,
    "endToEndId": "E12345678202401151030abcdefghijk",
    "providerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "ticketData": {},
    "webhookType": "RECEIVE",
    "debtorAccount": {
      "ispb": "12345678",
      "name": "Carlos Oliveira",
      "bankName": "Banco Exemplo S.A.",
      "issuer": "000",
      "number": "123456",
      "document": "12345678909",
      "accountType": null
    },
    "idempotencyKey": null,
    "creditDebitType": "CREDIT",
    "creditorAccount": {
      "ispb": null,
      "name": null,
      "bankName": null,
      "issuer": null,
      "number": null,
      "document": null,
      "accountType": null
    },
    "localInstrument": "DICT",
    "transactionType": "PIX",
    "remittanceInformation": "Serviço realizado."
  },
  "type": "RECEIVE"
}

Key Fields

typestring

Always "RECEIVE".

data.idnumber

Transaction ID at FyPay. Same value as loc.id in the PUT /api/cob/:txid response.

data.txIdstring

The txid used in PUT /api/cob/:txid. Use it to find the matching order.

data.endToEndIdstring

PIX End to End ID. It is the identifier you use to request a refund at PUT /api/pix/:e2eid/devolucao/:id.

data.statusstring

Transaction status:

  • LIQUIDATED: payment confirmed
  • ERROR: processing failure
  • PENDING: still being processed
data.paymentobject

data.mismatchAmountstring

Difference between the charged amount and the paid amount (charged minus paid), when they differ. null when the payment matches the charge.

data.debtorAccountobject

Payer data.

data.creditorAccountobject

Your account. All fields come as null.

data.creditDebitTypestring

Always "CREDIT".

data.refundsarray

Always empty. Refunds arrive in their own REFUND webhooks.

data.idempotencyKeynull

Always null in RECEIVE.

data.remittanceInformationstring

Transaction description, when present.

Processing the Webhook

Node.js Example

interface ReceiveWebhook {
  type: 'RECEIVE';
  data: {
    id: number;
    txId: string | null;
    status: 'PENDING' | 'LIQUIDATED' | 'ERROR';
    payment: {
      amount: string;
      currency: 'BRL';
    };
    mismatchAmount: string | null;
    endToEndId: string | null;
    errorCode: string | null;
    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;
  }

  const order = data.txId ? await findOrderByTxId(data.txId) : null;

  if (!order) {
    await registerUnmatchedCredit(data);
    return;
  }

  await markOrderAsPaid(order.id, {
    amount: data.payment.amount,
    divergence: data.mismatchAmount,
    endToEndId: data.endToEndId,
    payer: data.debtorAccount.name,
  });
}

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

    amount = Decimal(data['payment']['amount'])
    order = find_order_by_txid(data['txId']) if data.get('txId') else None

    if order is None:
        register_unmatched_credit(data)
        return

    mark_order_as_paid(
        order_id=order.id,
        amount=amount,
        divergence=data.get('mismatchAmount'),
        e2e_id=data['endToEndId'],
        payer=data['debtorAccount'].get('name'),
    )

Correlation with the Charge

The webhook txId is the same txid you used to create the charge:

const txid = 'pedido456a1b2c3d4e5f6g7h8i9j0';
await createCob(txid, { valor: { original: '100.00' } });

await saveOrder({ orderId: 'pedido-456', txId: txid, status: 'PENDING' });

In the RECEIVE webhook, look up the order by data.txId and check data.payment.amount and data.mismatchAmount before settling it.

Error Handling

If status is ERROR, the errorCode field carries the code reported by the provider:

if (data.status === 'ERROR') {
  await notifyPaymentError({
    txId: data.txId,
    errorCode: data.errorCode,
  });
}

Idempotency

The same webhook may arrive more than once. Deduplicate by type, data.id and data.status:

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

  const isNew = await redis.set(`webhook:${key}`, '1', 'NX', 'EX', 60 * 60 * 24 * 7);
  if (!isNew) return;

  await handleReceive(webhook);
}

Best Practices

Next Steps

On this page