Avistadocs
Webhooks V2

TRANSFER

Descripción general

El webhook TRANSFER se envia cuando una transferencia PIX iniciada por su aplicación es procesada. Este evento indica el resultado (exito o fallo) de una llamada al endpoint /dict/pix.

Cuando se envia

  • Transferencia PIX procesada exitosamente (LIQUIDATED)
  • Transferencia PIX fallida (ERROR)

Estructura del Payload

{
  "type": "TRANSFER",
  "data": {
    "id": 456,
    "txId": null,
    "pixKey": "destino@email.com",
    "status": "LIQUIDATED",
    "payment": {
      "amount": "100.50",
      "currency": "BRL"
    },
    "refunds": [],
    "createdAt": "2024-01-15T10:30:00.000Z",
    "errorCode": null,
    "endToEndId": "E12345678901234567890123456789012",
    "ticketData": {},
    "webhookType": "TRANSFER",
    "debtorAccount": {
      "ispb": null,
      "name": null,
      "issuer": null,
      "number": null,
      "document": null,
      "accountType": null
    },
    "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
    "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": "Pagamento NF 12345"
  }
}

Campos Importantes

typestring

Siempre "TRANSFER" para PIX enviados.

data.idnumber

ID de la transacción. Mismo valor retornado por POST /dict/pix.

data.endToEndIdstring

End to End ID - identificador único de la transacción PIX en el Banco Central.

data.statusstring

Estado de la transferencia:

  • LIQUIDATED: Transferencia confirmada (exito)
  • ERROR: Transferencia fallida
data.paymentobject

data.idempotencyKeystring

Clave de idempotencia enviada en el header x-idempotency-key de la solicitud original.

data.creditorAccountobject

Datos de quien recibio (el destinatario).

data.creditDebitTypestring

Siempre "DEBIT" para transferencias enviadas.

data.errorCodestring

Código de error cuando status === 'ERROR'. Puede ser null en caso de exito.

data.remittanceInformationstring

Descripción de la transferencia (campo description enviado en la solicitud).

Procesamiento del Webhook

Ejemplo en Node.js

interface TransferWebhook {
  type: 'TRANSFER';
  data: {
    id: number;
    status: 'LIQUIDATED' | 'ERROR';
    payment: {
      amount: string;
      currency: string;
    };
    endToEndId: string;
    idempotencyKey: string;
    creditorAccount: {
      name: string | null;
      document: string | null;
    };
    errorCode: string | null;
  };
}

async function handleTransfer(webhook: TransferWebhook) {
  const { data } = webhook;

  // Buscar transferencia por idempotencyKey
  const transfer = await findTransferByIdempotencyKey(data.idempotencyKey);

  if (!transfer) {
    console.warn(`Transferencia no encontrada: ${data.idempotencyKey}`);
    return;
  }

  if (data.status === 'LIQUIDATED') {
    // Exito - confirmar transferencia
    await updateTransfer(transfer.id, {
      status: 'COMPLETED',
      endToEndId: data.endToEndId,
      completedAt: new Date(),
    });

    // Notificar al usuario
    await notifyTransferSuccess({
      transferId: transfer.id,
      amount: parseFloat(data.payment.amount),
      recipient: data.creditorAccount.name,
    });

  } else if (data.status === 'ERROR') {
    // Fallo - revertir
    await updateTransfer(transfer.id, {
      status: 'FAILED',
      errorCode: data.errorCode,
    });

    // Notificar al usuario
    await notifyTransferFailed({
      transferId: transfer.id,
      errorCode: data.errorCode,
    });

    // Liberar saldo bloqueado
    await releaseBlockedBalance(transfer.id);
  }
}

Ejemplo en Python

from decimal import Decimal

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

    # Buscar transferencia
    transfer = find_transfer_by_idempotency_key(data['idempotencyKey'])

    if not transfer:
        print(f"Transferencia no encontrada: {data['idempotencyKey']}")
        return

    if data['status'] == 'LIQUIDATED':
        # Exito
        update_transfer(
            transfer_id=transfer.id,
            status='COMPLETED',
            e2e_id=data['endToEndId']
        )

        notify_transfer_success(
            transfer_id=transfer.id,
            amount=Decimal(data['payment']['amount']),
            recipient=data['creditorAccount'].get('name')
        )

    elif data['status'] == 'ERROR':
        # Fallo
        update_transfer(
            transfer_id=transfer.id,
            status='FAILED',
            error_code=data['errorCode']
        )

        notify_transfer_failed(
            transfer_id=transfer.id,
            error_code=data['errorCode']
        )

        # Liberar saldo
        release_blocked_balance(transfer.id)

Correlacion con la Solicitud

Use idempotencyKey para correlacionar el webhook con su solicitud original:

// 1. Crear transferencia
const idempotencyKey = crypto.randomUUID();
const transfer = await createTransfer(idempotencyKey, {
  pixKey: 'destino@email.com',
  amount: 100.50,
});

// 2. Guardar asociacion
await saveTransfer({
  id: transfer.id,
  idempotencyKey,
  status: 'PENDING',
});

// 3. En el webhook TRANSFER
const savedTransfer = await findByIdempotencyKey(webhook.data.idempotencyKey);
// savedTransfer.id corresponde a la transferencia original

Manejo de Errores

Códigos de error comunes:

CódigoDescripciónAcción Recomendada
INSUFFICIENT_BALANCESaldo insuficienteVerificar saldo antes de transferir
INVALID_KEYClave PIX invalidaVerificar la clave con el usuario
KEY_NOT_FOUNDClave no encontrada en DICTSolicitar una clave valida
ACCOUNT_BLOCKEDCuenta bloqueadaContactar soporte
TIMEOUTTimeout de procesamientoIntentar nuevamente
if (data.status === 'ERROR') {
  switch (data.errorCode) {
    case 'INSUFFICIENT_BALANCE':
      // Notificar saldo insuficiente
      await notifyInsufficientBalance(transfer);
      break;

    case 'INVALID_KEY':
    case 'KEY_NOT_FOUND':
      // Solicitar nueva clave al usuario
      await requestNewPixKey(transfer);
      break;

    case 'TIMEOUT':
      // Puede intentar de nuevo con una nueva clave de idempotencia
      await retryTransfer(transfer);
      break;

    default:
      // Error generico
      await notifyGenericError(transfer, data.errorCode);
  }
}

Flujo de Saldo

sequenceDiagram
    participant App
    participant API
    participant Bank

    Note over App,Bank: Saldo: available=1000, blocked=0

    App->>API: POST /dict/pix (R$ 100)
    API-->>App: { type: PENDING }

    Note over App,Bank: Saldo: available=900, blocked=100

    alt Exito
        Bank->>API: Confirmacion
        API->>App: Webhook TRANSFER (LIQUIDATED)
        Note over App,Bank: Saldo: available=900, blocked=0
    else Fallo
        Bank->>API: Error
        API->>App: Webhook TRANSFER (ERROR)
        Note over App,Bank: Saldo: available=1000, blocked=0
    end

Idempotencia

Use data.id para evitar el procesamiento duplicado:

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

  const isProcessed = await redis.sismember('processed', webhookId);
  if (isProcessed) {
    return; // Ya procesado
  }

  await redis.sadd('processed', webhookId);
  await handleTransfer(webhook);
}

Mejores Prácticas

Próximos Pasos

En esta página