PIX Cash-Out (Pago)
Descripción General
El endpoint PIX Cash-Out le permite realizar pagos PIX a cualquier clave PIX válida (CPF, CNPJ, teléfono, email o clave aleatoria). El valor se reserva en el saldo de la cuenta en cuanto se acepta el pago, y el resultado final llega por webhook.
Para pagos vía QR Code PIX (escaneo o copiar y pegar), use el endpoint dedicado Cash-Out vía QR Code. Este endpoint es exclusivo para pagos por clave PIX.
Este endpoint requiere un token Bearer válido. Consulte la documentación de autenticación para más detalles.
Características
- Pagos instantáneos 24/7
- Soporte para todos los tipos de clave PIX (CPF, CNPJ, teléfono, email, clave aleatoria)
- Validación del formato de la clave según el
keyType - Verificación automática de saldo y límites
- Identificación única por
externalId(idempotencia) - Descripción personalizable para el destinatario
- Confirmación asíncrona vía webhook (estado PENDING → CONFIRMED o ERROR)
Endpoint
POST /api/pix/cash-out
Realiza un pago PIX.
Encabezados Requeridos
Authorization: Bearer {token}
Content-Type: application/jsonCuerpo de la Solicitud
{
"value": 250.50,
"details": {
"key": "12345678901",
"keyType": "DOCUMENT",
"name": "Ana Costa",
"document": "12345678901"
},
"externalId": "PAYMENT-987654-20240119",
"description": "Pago a proveedor"
}Solicitud
curl -X POST https://api.fypay.me/api/pix/cash-out \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"value": 250.50,
"details": {
"key": "12345678901",
"keyType": "DOCUMENT",
"name": "Ana Costa",
"document": "12345678901"
},
"externalId": "PAYMENT-987654-20240119",
"description": "Pago a proveedor"
}'Vea la sección Formatos de Clave PIX más abajo para los formatos exactos aceptados para cada tipo de clave.
Respuesta (201 Created)
{
"transactionId": "9876",
"externalId": "PAYMENT-987654-20240119",
"status": "PENDING",
"generateTime": "2024-01-19T15:45:00.000Z"
}Parámetros de la Solicitud
valuenumberobrigatorioValor del pago en reales (BRL). Debe tener como máximo 2 decimales.
Mínimo: 0.01
Ejemplo: 250.50
detailsobjectobrigatorioInformación de la clave PIX de destino.
details.keystringobrigatorioClave PIX de destino. El formato debe corresponder al keyType informado.
Formatos aceptados:
- CPF:
12345678901(11 dígitos, solo números) - CNPJ:
12345678000199(14 dígitos, solo números) - Email:
usuario@exemplo.com - Teléfono:
11999999999(10 u 11 dígitos — DDD + número, sin código de país+55) - Clave aleatoria: UUID formato
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Vea la tabla completa en Formatos de Clave PIX.
details.keyTypestringobrigatorioTipo de clave PIX. El valor no distingue mayúsculas de minúsculas (se convierte a mayúsculas automáticamente).
Valores aceptados:
DOCUMENT— CPF o CNPJ (la distinción se hace automáticamente por la cantidad de dígitos)EMAIL— Dirección de emailPHONE— Número de teléfono (DDD + número)RANDOM— Clave aleatoria (EVP/UUID)
¿Por qué es obligatorio? La API no realiza consulta DICT para inferir el tipo de clave. La identificación debe venir en la solicitud para que el sistema normalice el formato correcto antes de enviarlo al banco liquidador (ej.: agregar +55 en claves PHONE, convertir DOCUMENT a CPF/CNPJ).
Ejemplo: "DOCUMENT"
details.namestringNombre del titular de la clave PIX de destino. Opcional en la API, pero recomendado; algunos liquidadores exigen el campo.
Máximo: 100 caracteres
Uso: Campo informativo — se utiliza para registros de extracto e identificación del destinatario. El banco liquidador no lo valida contra el registro de la clave PIX. Enviar un nombre distinto al del titular no causa error en la transacción.
Ejemplo: "Ana Costa"
details.documentstringCPF o CNPJ del titular de la clave PIX de destino (solo números). Validado algorítmicamente (dígitos verificadores).
CPF: 11 dígitos | CNPJ: 14 dígitos
Opcional en la API, pero fuertemente recomendado para todos los tipos de clave — incluso EMAIL, PHONE y RANDOM. Algunos liquidadores exigen el campo y rechazan el pago sin él. Este campo se transmite al banco liquidador de su cuenta. La API FyPay valida solo el formato: no compara details.document con details.key, no consulta la DICT y no bloquea la operación cuando ambos difieren. La verificación contra el titular real de la clave solo ocurre en algunos liquidadores; en los demás, un valor divergente no impide el pago, que se acredita al dueño de la clave. Vea Verificación de titularidad por liquidante.
Dependiendo del liquidador de su cuenta, informar el documento también puede hacer que el rechazo de clave inexistente vuelva al instante, en lugar de llegar después por webhook. Vea Protecciones en el retiro Pix por clave.
Escenarios por keyType:
DOCUMENT: eldetails.documentdebe ser idéntico aldetails.key(ambos son el CPF/CNPJ). La API no verifica esa igualdad: garantícela de su lado antes de enviar. Si difieren, el pago va al dueño de la clave, salvo en los liquidadores que verifican la titularidad.EMAIL,PHONE,RANDOM: no es posible derivar el documento a partir de la clave. El integrador necesita solicitar el CPF/CNPJ al usuario final y enviarlo en este campo.
Consejo de UX: Muestre siempre un campo de CPF/CNPJ en la interfaz cuando el tipo de clave no sea DOCUMENT.
Ejemplo: "12345678901"
externalIdstringobrigatorioIdentificador externo único de la transacción. Debe ser único por cuenta — si se repite, la API devolverá el error PIX_DUPLICATE_EXTERNAL_ID (HTTP 400, con details.existingTransactionId). No puede empezar con MIRROR: (prefijo reservado).
Máximo: 50 caracteres
Recomendación: Use un formato que garantice unicidad, como PAY-{timestamp}-{uuid}
Ejemplo: "PAYMENT-987654-20240119"
descriptionstringDescripción del pago que aparecerá en el extracto del destinatario.
Máximo: 140 caracteres
Predeterminado: Vacío
Ejemplo: "Pago a proveedor - Factura 12345"
Estructura de la Respuesta
transactionIdstringsempre presenteID interno de la transacción generado por FyPay.
Ejemplo: "9876"
externalIdstringsempre presenteID externo proporcionado en la solicitud (mismo valor que la entrada).
Ejemplo: "PAYMENT-987654-20240119"
statusstringsempre presenteEstado actual de la transacción.
Valores posibles:
PENDING: Pago en procesamientoCONFIRMED: Pago confirmado y finalizadoERROR: Error en el procesamiento
Ejemplo: "PENDING"
Nota: La mayoría de los pagos PIX se confirma en pocos segundos
generateTimestringsempre presenteFecha y hora de creación del pago (ISO 8601 UTC).
Ejemplo: "2024-01-19T15:45:00.000Z"
Ejemplos de Implementación
Node.js / TypeScript
import axios from 'axios';
interface CashOutRequest {
value: number;
details: {
key: string;
keyType: 'DOCUMENT' | 'EMAIL' | 'PHONE' | 'RANDOM';
name: string;
document: string;
};
externalId: string;
description?: string;
}
interface CashOutResponse {
transactionId: string;
externalId: string;
status: 'PENDING' | 'CONFIRMED' | 'ERROR';
generateTime: string;
}
async function sendPixPayment(
token: string,
recipientKey: string,
recipientKeyType: 'DOCUMENT' | 'EMAIL' | 'PHONE' | 'RANDOM',
recipientName: string,
recipientDocument: string,
amount: number,
description?: string
): Promise<CashOutResponse> {
const payload: CashOutRequest = {
value: amount,
details: {
key: recipientKey,
keyType: recipientKeyType,
name: recipientName,
document: recipientDocument
},
externalId: `PAY-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
description: description || `Pago PIX de R$ ${amount.toFixed(2)}`
};
try {
const response = await axios.post<CashOutResponse>(
'https://api.fypay.me/api/pix/cash-out',
payload,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log('¡Pago PIX iniciado con éxito!');
console.log(`ID de la Transacción: ${response.data.transactionId}`);
console.log(`Estado: ${response.data.status}`);
console.log(`Valor: R$ ${amount.toFixed(2)}`);
console.log(`Destinatario: ${recipientName}`);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const errorData = error.response?.data;
console.error('Error al realizar el pago:', errorData);
// Tratar errores específicos
if (error.response?.status === 400) {
if (errorData?.code === 'PIX_INSUFFICIENT_BALANCE') {
throw new Error('Saldo insuficiente para realizar el pago');
}
throw new Error('Datos inválidos: ' + errorData?.message);
}
if (error.response?.status === 422 || error.response?.status === 429) {
// Protecciones del retiro por clave: vea /es/docs/guides/pix-cash-out-protections
const retryAfter = error.response.headers['retry-after'];
throw new Error(`${errorData?.code}: intente de nuevo en ${retryAfter ?? '?'} s`);
}
throw new Error(errorData?.message || 'Error al realizar el pago PIX');
}
throw error;
}
}
// Uso - Pago por CPF
sendPixPayment(
'su_token_aqui',
'12345678901',
'DOCUMENT',
'Ana Costa',
'12345678901',
250.50,
'Pago a proveedor'
);
// Uso - Pago por Email
sendPixPayment(
'su_token_aqui',
'ana.costa@email.com',
'EMAIL',
'Ana Costa',
'12345678901',
100.00,
'Reembolso'
);
// Uso - Pago por Teléfono (DDD + número, sin código de país +55)
sendPixPayment(
'su_token_aqui',
'11999999999',
'PHONE',
'Ana Costa',
'12345678901',
50.00
);Python
import requests
from datetime import datetime
from typing import Dict, Optional
import uuid
def send_pix_payment(
token: str,
recipient_key: str,
recipient_key_type: str,
recipient_name: str,
recipient_document: str,
amount: float,
description: Optional[str] = None
) -> Dict:
"""
Envía un pago PIX
Args:
token: Token Bearer válido
recipient_key: Clave PIX del destinatario
recipient_key_type: Tipo de clave (DOCUMENT, EMAIL, PHONE, RANDOM)
recipient_name: Nombre del destinatario
recipient_document: CPF o CNPJ del destinatario
amount: Valor en reales
description: Descripción del pago (opcional)
Returns:
Datos del pago iniciado
"""
url = 'https://api.fypay.me/api/pix/cash-out'
payload = {
'value': round(amount, 2),
'details': {
'key': recipient_key,
'keyType': recipient_key_type,
'name': recipient_name,
'document': recipient_document
},
'externalId': f'PAY-{int(datetime.now().timestamp())}-{uuid.uuid4().hex[:8]}',
'description': description or f'Pago PIX de R$ {amount:.2f}'
}
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
try:
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
print('¡Pago PIX iniciado con éxito!')
print(f"ID de la Transacción: {data['transactionId']}")
print(f"Estado: {data['status']}")
print(f"Valor: R$ {amount:.2f}")
print(f"Destinatario: {recipient_name}")
return data
except requests.exceptions.HTTPError as e:
error_data = e.response.json() if e.response else {}
# Tratar errores específicos
if e.response.status_code == 400:
if error_data.get('code') == 'PIX_INSUFFICIENT_BALANCE':
raise Exception('Saldo insuficiente para realizar el pago')
raise Exception(f"Datos inválidos: {error_data.get('message')}")
raise Exception(f"Error al realizar el pago: {error_data.get('message', str(e))}")
# Uso
token = 'su_token_aqui'
# Pago por CPF
payment = send_pix_payment(
token=token,
recipient_key='12345678901',
recipient_key_type='DOCUMENT',
recipient_name='Ana Costa',
recipient_document='12345678901',
amount=250.50,
description='Pago a proveedor'
)PHP
<?php
function sendPixPayment(
string $token,
string $recipientKey,
string $recipientKeyType,
string $recipientName,
string $recipientDocument,
float $amount,
?string $description = null
): array {
$url = 'https://api.fypay.me/api/pix/cash-out';
$payload = [
'value' => round($amount, 2),
'details' => [
'key' => $recipientKey,
'keyType' => $recipientKeyType,
'name' => $recipientName,
'document' => $recipientDocument
],
'externalId' => 'PAY-' . time() . '-' . bin2hex(random_bytes(4)),
'description' => $description ?? "Pago PIX de R$ " . number_format($amount, 2, ',', '.')
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 201) {
$errorData = json_decode($response, true);
$errorMessage = $errorData['message'] ?? "HTTP $httpCode";
if ($httpCode === 400 && ($errorData['code'] ?? '') === 'PIX_INSUFFICIENT_BALANCE') {
throw new Exception('Saldo insuficiente para realizar el pago');
}
throw new Exception("Error al realizar el pago: $errorMessage");
}
$data = json_decode($response, true);
echo "¡Pago PIX iniciado con éxito!" . PHP_EOL;
echo "ID de la Transacción: {$data['transactionId']}" . PHP_EOL;
echo "Estado: {$data['status']}" . PHP_EOL;
echo "Valor: R$ " . number_format($amount, 2, ',', '.') . PHP_EOL;
echo "Destinatario: $recipientName" . PHP_EOL;
return $data;
}
// Uso
$token = 'su_token_aqui';
$payment = sendPixPayment(
$token,
'12345678901',
'DOCUMENT',
'Ana Costa',
'12345678901',
250.50,
'Pago a proveedor'
);Casos de Uso
1. Nómina
class PayrollProcessor {
constructor(private token: string) {}
async processPayroll(employees: Employee[]) {
const results = {
successful: [],
failed: []
};
for (const employee of employees) {
try {
// Verificar el saldo antes de cada pago
const balance = await getBalance(this.token);
if (balance.netBalance < employee.salary) {
throw new Error('Saldo insuficiente');
}
// Realizar el pago
const payment = await sendPixPayment(
this.token,
employee.pixKey,
employee.pixKeyType,
employee.fullName,
employee.document,
employee.salary,
`Salario ${new Date().toLocaleDateString('es', { month: 'long', year: 'numeric' })}`
);
results.successful.push({
employee: employee.fullName,
amount: employee.salary,
transactionId: payment.transactionId
});
// Esperar 1 segundo entre pagos
await this.sleep(1000);
} catch (error) {
results.failed.push({
employee: employee.fullName,
error: error.message
});
}
}
return results;
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Uso
interface Employee {
fullName: string;
document: string;
pixKey: string;
pixKeyType: 'DOCUMENT' | 'EMAIL' | 'PHONE' | 'RANDOM';
salary: number;
}
const payroll = new PayrollProcessor('su_token_aqui');
const employees: Employee[] = [
{
fullName: 'Pedro Santos',
document: '12345678901',
pixKey: '12345678901',
pixKeyType: 'DOCUMENT',
salary: 3500.00
},
// ... más empleados
];
const results = await payroll.processPayroll(employees);
console.log(`Pagos exitosos: ${results.successful.length}`);
console.log(`Pagos con error: ${results.failed.length}`);2. Marketplace - Transferencia a Vendedores
class MarketplacePayouts:
"""Procesa transferencias a vendedores del marketplace"""
def __init__(self, token: str):
self.token = token
def process_seller_payouts(self, sales_data: list) -> dict:
"""Procesa transferencias basadas en ventas"""
results = {'successful': [], 'failed': []}
# Agrupar ventas por vendedor
seller_totals = self.group_sales_by_seller(sales_data)
for seller_id, total_amount in seller_totals.items():
try:
# Buscar datos del vendedor
seller = self.get_seller_data(seller_id)
# Calcular el valor después de la comisión
commission = total_amount * 0.10 # 10% de comisión
payout_amount = total_amount - commission
# Realizar el pago
payment = send_pix_payment(
token=self.token,
recipient_key=seller['pix_key'],
recipient_key_type=seller['pix_key_type'],
recipient_name=seller['name'],
recipient_document=seller['document'],
amount=payout_amount,
description=f'Transferencia de ventas - {len(sales_data)} transacciones'
)
results['successful'].append({
'seller': seller['name'],
'gross_amount': total_amount,
'commission': commission,
'net_amount': payout_amount,
'transaction_id': payment['transactionId']
})
# Registrar la transferencia en la base de datos
self.record_payout(seller_id, payment)
except Exception as e:
results['failed'].append({
'seller_id': seller_id,
'error': str(e)
})
return results
def group_sales_by_seller(self, sales_data: list) -> dict:
"""Agrupa ventas por vendedor"""
totals = {}
for sale in sales_data:
seller_id = sale['seller_id']
totals[seller_id] = totals.get(seller_id, 0) + sale['amount']
return totals3. Sistema de Reembolso
class RefundSystem {
constructor(token) {
this.token = token;
}
async processRefund(orderId, refundReason) {
// Buscar datos del pedido
const order = await this.getOrderData(orderId);
// Validar si el reembolso está permitido
if (!this.canRefund(order)) {
throw new Error('Reembolso no permitido para este pedido');
}
// Realizar el pago de vuelta al cliente
const refund = await sendPixPayment(
this.token,
order.customer.pixKey,
order.customer.pixKeyType,
order.customer.name,
order.customer.document,
order.amount,
`Reembolso - Pedido ${orderId} - ${refundReason}`
);
// Actualizar el estado del pedido
await this.updateOrderStatus(orderId, 'REFUNDED', refund.transactionId);
// Enviar notificación al cliente
await this.notifyCustomer(order.customer.email, refund);
return refund;
}
canRefund(order) {
// Verificar si el pedido fue pagado y aún está dentro del plazo
const daysSincePurchase = (Date.now() - new Date(order.paidAt)) / (1000 * 60 * 60 * 24);
return order.status === 'PAID' && daysSincePurchase <= 7;
}
}Formatos de Clave PIX
La siguiente tabla detalla el formato exacto aceptado para cada tipo de clave. El campo details.key se valida contra el details.keyType informado — si el formato no corresponde, la API devuelve error 400.
keyType | Formato esperado en details.key | Regex de validación | Ejemplos válidos | Ejemplos inválidos |
|---|---|---|---|---|
DOCUMENT | CPF: 11 dígitos numéricos | ^\d{11}$ (con validación de dígitos verificadores) | 12345678901 | 123.456.789-01 (con puntuación), 1234567890 (10 dígitos) |
DOCUMENT | CNPJ: 14 dígitos numéricos | ^\d{14}$ (con validación de dígitos verificadores) | 12345678000195 | 12.345.678/0001-95 (con puntuación) |
EMAIL | Dirección de email válida | ^[^\s@]+@[^\s@]+\.[^\s@]{2,}$ | usuario@exemplo.com | usuario@ (sin dominio) |
PHONE | DDD + número (10 u 11 dígitos) | ^\d{10,11}$ | 11999999999 (celular), 1133334444 (fijo) | 5511999999999 (con código de país), +5511999999999 (con +código de país) |
RANDOM | UUID (con o sin guiones) | ^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$ o ^[a-fA-F0-9]{32}$ | a1b2c3d4-e5f6-4890-abcd-ef1234567890 | a1b2c3d4 (incompleto) |
Clave PHONE — no incluya el código de país (+55). Envíe solo DDD + número (10 u 11 dígitos). La API agrega el código de país +55 automáticamente antes de enviarlo al banco liquidador.
Clave DOCUMENT — envíe solo números. Los CPF y CNPJ no deben contener puntuación (puntos, barras o guiones). Además de la cantidad de dígitos, la API valida los dígitos verificadores del CPF/CNPJ.
Autodetección de keyType
Si desea implementar la detección automática del keyType a partir del valor de la clave, use la siguiente lógica de prioridad:
- Contiene
@→EMAIL - Formato UUID v4 →
RANDOM - 14 dígitos + CNPJ válido (dígitos verificadores) →
DOCUMENT - 11 dígitos + CPF válido (dígitos verificadores) →
DOCUMENT - 10-11 dígitos + no es CPF válido →
PHONE
function inferPixKeyType(key: string): 'DOCUMENT' | 'EMAIL' | 'PHONE' | 'RANDOM' | null {
const trimmed = key.trim();
// 1. UUID (clave aleatoria / EVP)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (uuidRegex.test(trimmed)) return 'RANDOM';
// 2. Email
if (trimmed.includes('@')) return 'EMAIL';
// 3. Solo dígitos → DOCUMENT o PHONE
const digits = trimmed.replace(/\D/g, '');
// 14 dígitos → CNPJ
if (digits.length === 14) return 'DOCUMENT';
// 11 dígitos → CPF o PHONE
// La distinción se hace validando los dígitos verificadores del CPF
if (digits.length === 11) {
return isValidCpf(digits) ? 'DOCUMENT' : 'PHONE';
}
// 10 dígitos → teléfono fijo (DDD + 8 dígitos)
if (digits.length === 10) return 'PHONE';
return null; // Formato no reconocido
}
// Validación algorítmica de CPF (dígitos verificadores)
function isValidCpf(cpf: string): boolean {
if (/^(\d)\1{10}$/.test(cpf)) return false; // Rechaza secuencias como 11111111111
let sum = 0;
for (let i = 0; i < 9; i++) sum += parseInt(cpf[i]) * (10 - i);
let check = 11 - (sum % 11);
if (check >= 10) check = 0;
if (check !== parseInt(cpf[9])) return false;
sum = 0;
for (let i = 0; i < 10; i++) sum += parseInt(cpf[i]) * (11 - i);
check = 11 - (sum % 11);
if (check >= 10) check = 0;
return check === parseInt(cpf[10]);
}Ambigüedad CPF vs. Teléfono: Un valor de 11 dígitos puede ser tanto un CPF como un número de celular (DDD + 9 dígitos). La lógica anterior usa la validación de dígitos verificadores del CPF para distinguirlos: si los dígitos verificadores coinciden, es CPF; de lo contrario, es teléfono. Aun así, recomendamos que el usuario final seleccione explícitamente el tipo de clave en la interfaz para evitar errores en casos límite.
Validación de Clave PIX
Antes de enviar un pago, valide el formato de la clave PIX localmente:
function validatePixKey(key: string, keyType: string): boolean {
switch (keyType) {
case 'DOCUMENT':
// CPF: 11 dígitos o CNPJ: 14 dígitos (solo números)
return /^\d{11}$/.test(key) || /^\d{14}$/.test(key);
case 'EMAIL':
// Email con dominio válido (TLD mínimo de 2 caracteres)
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(key);
case 'PHONE':
// DDD + número: 10 dígitos (fijo) u 11 dígitos (celular)
// NO incluya el código de país (+55) — la API lo agrega automáticamente
return /^\d{10,11}$/.test(key);
case 'RANDOM':
// UUID con guiones o 32 caracteres hexadecimales sin guiones
return /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/.test(key)
|| /^[a-fA-F0-9]{32}$/.test(key);
default:
return false;
}
}Verificación de Saldo
Verifique el saldo antes de realizar pagos para evitar el error 400 PIX_INSUFFICIENT_BALANCE.
async function safePayment(
token: string,
amount: number,
recipient: RecipientData
) {
// Consultar el saldo
const balance = await getBalance(token);
// Verificar si hay saldo suficiente
if (balance.netBalance < amount) {
throw new Error(
`Saldo insuficiente. Disponible: R$ ${balance.netBalance.toFixed(2)} | ` +
`Necesario: R$ ${amount.toFixed(2)}`
);
}
// Continuar con el pago
return await sendPixPayment(token, ...recipient, amount);
}Monitoreo de Estado
Para seguir la confirmación del pago, consulte GET /api/pix/transaction/\{id\} (acepta el transactionId numérico o su externalId):
async function monitorPaymentStatus(transactionId, timeout = 60000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const status = await checkTransactionStatus(transactionId);
if (status === 'CONFIRMED') {
console.log('¡Pago confirmado!');
return true;
}
if (status === 'ERROR') {
throw new Error('El pago falló');
}
// Esperar 2 segundos antes de verificar de nuevo
await new Promise(resolve => setTimeout(resolve, 2000));
}
throw new Error('Timeout: Pago no confirmado en el tiempo esperado');
}Flujo de Estado de la Transacción
Después de crear un PIX Cash-Out, la transacción pasa por un flujo asíncrono:
POST /api/pix/cash-out
│
▼
┌─────────┐
│ PENDING │ ← Respuesta inmediata de la API (201)
└────┬─────┘
│ (procesamiento en el banco liquidador)
│
┌─────┴──────┐
▼ ▼
┌──────────┐ ┌───────┐
│CONFIRMED │ │ ERROR │ ← Notificado vía webhook
└──────────┘ └───────┘- La API devuelve
status: "PENDING"inmediatamente (201 Created) - El pago se envía al banco liquidador para su procesamiento
- El resultado final (
CONFIRMEDoERROR) se notifica vía webhook de Cash-Out - Alternativamente, use el endpoint de polling para consultar el estado
La mayoría de los pagos PIX se confirma en pocos segundos. Recomendamos implementar la recepción de webhooks como método principal de confirmación, y el polling solo como alternativa de respaldo.
Dependiendo del liquidador de su cuenta, la API puede responder al instante (422 o 429, sin transacción y sin webhook) a retiros a claves que el proveedor ya informó como inexistentes o mientras bloquea consultas. Vea Protecciones en el retiro Pix por clave.
Códigos de Respuesta HTTP
| Código | Descripción | Significado |
|---|---|---|
201 | Pago Iniciado | Transferencia PIX iniciada con éxito (estado PENDING) |
400 | Datos Inválidos | Campos obligatorios ausentes, formato inválido, externalId repetido, saldo insuficiente o límite excedido |
401 | No Autorizado | Token Bearer no proporcionado, expirado o inválido |
422 | Clave no encontrada | PIX_KEY_NOT_FOUND: el proveedor informó que la clave no existe. Cuando la respuesta viene del registro de clave negada, el cuerpo trae details.cached: true y details.retryAfterSeconds. Vea Protecciones en el retiro Pix por clave |
429 | Demasiadas claves inexistentes | TOO_MANY_PIX_KEYS_NOT_FOUND: el retiro por clave está bloqueado por exceso de consultas de claves inexistentes. Espere el tiempo del encabezado Retry-After. Vea Protecciones en el retiro Pix por clave |
500 | Error Interno | Error inesperado en el procesamiento. Consulte por el externalId antes de intentar de nuevo |
Errores de Negocio
Además de los códigos HTTP, la API devuelve códigos de error específicos en el cuerpo de la respuesta. Algunos errores ocurren de forma síncrona (en la respuesta de la API) y otros de forma asíncrona (vía webhook, cuando el banco liquidador rechaza la operación).
Errores síncronos (respuesta de la API)
Devueltos inmediatamente en la respuesta HTTP, con estado 400 (o 422 y 429 en las protecciones del retiro por clave):
Código de error (code) | Mensaje | Causa |
|---|---|---|
PIX_DUPLICATE_EXTERNAL_ID | Esta transacción ya existe en el sistema | El externalId ya fue utilizado para esta cuenta (details.existingTransactionId trae la transacción existente) |
PIX_INSUFFICIENT_BALANCE | Saldo insuficiente para realizar esta operación | El saldo disponible es menor que el valor + tarifa |
PIX_TRANSACTION_LIMIT_EXCEEDED | Supera el límite máximo por transacción | El valor excede el límite configurado por transacción |
PIX_DAILY_LIMIT_EXCEEDED | Alcanzó el límite diario de transacciones | El acumulado del día excede el límite diario |
PIX_NIGHT_LIMIT_EXCEEDED | El límite nocturno es más restringido | La operación nocturna excede el límite reducido (20h–6h, horario de Brasilia) |
PIX_LIMIT_EXCEEDED | Supera el límite permitido | Otro límite de la cuenta fue excedido |
| Error de validación | Mensaje por campo | Valor menor que 0.01, más de 2 decimales, clave en formato que no corresponde al keyType, documento inválido etc. |
PIX_KEY_NOT_FOUND | Chave Pix não encontrada | Estado 422. Con details.cached: true, es la respuesta guardada: respete details.retryAfterSeconds. Sin cached, vino del proveedor en esta llamada. Vea Protecciones en el retiro Pix por clave |
TOO_MANY_PIX_KEYS_NOT_FOUND | Muitas tentativas com chaves Pix inexistentes. Saque por chave bloqueado temporariamente; tente novamente em instantes. | Estado 429: el retiro por clave está bloqueado por exceso de consultas de claves inexistentes. Espere el tiempo del encabezado Retry-After. Vea Protecciones en el retiro Pix por clave |
Errores asíncronos (vía webhook)
Cuando el banco liquidador rechaza la operación, la transacción cambia al estado ERROR. Los campos errorCode y errorMessage se envían en el payload del webhook de Cash-Out. El valor de errorCode varía según el liquidador de la cuenta y no debe usarse para lógica: trátelo como informativo y muestre errorMessage al usuario.
{
"event": "CashOut",
"status": "ERROR",
"transactionId": "12345",
"externalId": "PAYMENT-987654-20240119",
"errorCode": "TAX_ID_MISMATCH",
"errorMessage": "O CPF/CNPJ informado não corresponde ao titular da conta.",
...
}Ejemplos de códigos estandarizados que algunos liquidadores devuelven:
errorCode | errorMessage | Causa | ¿Reintentable? |
|---|---|---|---|
TAX_ID_MISMATCH | O CPF/CNPJ informado não corresponde ao titular da conta. | El details.document no coincide con el dueño de la clave PIX. En otros liquidadores que verifican titularidad, la divergencia llega con el código y el mensaje del propio liquidador | No |
INVALID_TAX_ID | CPF ou CNPJ inválido. | El documento enviado falló en la validación del banco | No |
BLOCKED_ACCOUNT | A conta de destino está bloqueada e não pode receber transferências. | Cuenta de destino bloqueada judicial o administrativamente | No |
ACCOUNT_CLOSED | A conta de destino está encerrada. | La cuenta de destino fue cerrada | No |
ORDER_REJECTED | A transação foi rejeitada pelo banco destinatário. | El banco de destino rechazó la operación | No |
PAYMENT_EXPIRED | A transação expirou antes de ser processada. | Se excedió el tiempo de procesamiento | Sí |
SETTLEMENT_TIMEOUT | O banco destinatário não respondeu a tempo. | Timeout en la comunicación con el banco de destino | Sí |
Para errores reintentables (PAYMENT_EXPIRED, SETTLEMENT_TIMEOUT), es seguro reenviar la transacción con un nuevo externalId. Para fallos de red (sin respuesta), vea «Reintentos seguros» más abajo — en esos casos reutilice el mismo externalId.
Cuando ocurre un error asíncrono, la transacción se cancela automáticamente (rollback en el ledger) y el saldo se devuelve. No es necesario realizar ninguna acción adicional — solo notifique al usuario final el motivo del fallo.
Consulte la Referencia de la API para detalles completos de los campos de respuesta.
Mejores Prácticas
Notas Importantes
- Valor mínimo: R$ 0,01 | Decimales: Máximo 2
externalId: Máximo 50 caracteres, único por cuenta (idempotencia)description: Máximo 140 caracteres (opcional)details.name: Opcional, máximo 100 caracteres — informativo, no validado por el bancodetails.document: Opcional en la API, recomendado para todos los tipos de clave — la API valida solo el formato; la verificación con el titular de la clave depende del liquidador de la cuenta- Clave PHONE: Envíe solo DDD + número (10-11 dígitos). La API agrega
+55automáticamente