docs
Guías de Integración

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/json

Cuerpo 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

valuenumberobrigatorio

Valor del pago en reales (BRL). Debe tener como máximo 2 decimales.

Mínimo: 0.01

Ejemplo: 250.50

detailsobjectobrigatorio

Información de la clave PIX de destino.

details.keystringobrigatorio

Clave 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.keyTypestringobrigatorio

Tipo 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 email
  • PHONE — 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.namestring

Nombre 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.documentstring

CPF 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: el details.document debe ser idéntico al details.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"

externalIdstringobrigatorio

Identificador 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"

descriptionstring

Descripció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 presente

ID interno de la transacción generado por FyPay.

Ejemplo: "9876"

externalIdstringsempre presente

ID externo proporcionado en la solicitud (mismo valor que la entrada).

Ejemplo: "PAYMENT-987654-20240119"

statusstringsempre presente

Estado actual de la transacción.

Valores posibles:

  • PENDING: Pago en procesamiento
  • CONFIRMED: Pago confirmado y finalizado
  • ERROR: Error en el procesamiento

Ejemplo: "PENDING"

Nota: La mayoría de los pagos PIX se confirma en pocos segundos

generateTimestringsempre presente

Fecha 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 totals

3. 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.

keyTypeFormato esperado en details.keyRegex de validaciónEjemplos válidosEjemplos inválidos
DOCUMENTCPF: 11 dígitos numéricos^\d{11}$ (con validación de dígitos verificadores)12345678901123.456.789-01 (con puntuación), 1234567890 (10 dígitos)
DOCUMENTCNPJ: 14 dígitos numéricos^\d{14}$ (con validación de dígitos verificadores)1234567800019512.345.678/0001-95 (con puntuación)
EMAILDirección de email válida^[^\s@]+@[^\s@]+\.[^\s@]{2,}$usuario@exemplo.comusuario@ (sin dominio)
PHONEDDD + 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)
RANDOMUUID (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-ef1234567890a1b2c3d4 (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:

  1. Contiene @ → EMAIL
  2. Formato UUID v4 → RANDOM
  3. 14 dígitos + CNPJ válido (dígitos verificadores) → DOCUMENT
  4. 11 dígitos + CPF válido (dígitos verificadores) → DOCUMENT
  5. 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
└──────────┘ └───────┘
  1. La API devuelve status: "PENDING" inmediatamente (201 Created)
  2. El pago se envía al banco liquidador para su procesamiento
  3. El resultado final (CONFIRMED o ERROR) se notifica vía webhook de Cash-Out
  4. 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ódigoDescripciónSignificado
201Pago IniciadoTransferencia PIX iniciada con éxito (estado PENDING)
400Datos InválidosCampos obligatorios ausentes, formato inválido, externalId repetido, saldo insuficiente o límite excedido
401No AutorizadoToken Bearer no proporcionado, expirado o inválido
422Clave no encontradaPIX_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
429Demasiadas claves inexistentesTOO_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
500Error InternoError 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)MensajeCausa
PIX_DUPLICATE_EXTERNAL_IDEsta transacción ya existe en el sistemaEl externalId ya fue utilizado para esta cuenta (details.existingTransactionId trae la transacción existente)
PIX_INSUFFICIENT_BALANCESaldo insuficiente para realizar esta operaciónEl saldo disponible es menor que el valor + tarifa
PIX_TRANSACTION_LIMIT_EXCEEDEDSupera el límite máximo por transacciónEl valor excede el límite configurado por transacción
PIX_DAILY_LIMIT_EXCEEDEDAlcanzó el límite diario de transaccionesEl acumulado del día excede el límite diario
PIX_NIGHT_LIMIT_EXCEEDEDEl límite nocturno es más restringidoLa operación nocturna excede el límite reducido (20h–6h, horario de Brasilia)
PIX_LIMIT_EXCEEDEDSupera el límite permitidoOtro límite de la cuenta fue excedido
Error de validaciónMensaje por campoValor menor que 0.01, más de 2 decimales, clave en formato que no corresponde al keyType, documento inválido etc.
PIX_KEY_NOT_FOUNDChave Pix não encontradaEstado 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_FOUNDMuitas 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:

errorCodeerrorMessageCausa¿Reintentable?
TAX_ID_MISMATCHO 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 liquidadorNo
INVALID_TAX_IDCPF ou CNPJ inválido.El documento enviado falló en la validación del bancoNo
BLOCKED_ACCOUNTA conta de destino está bloqueada e não pode receber transferências.Cuenta de destino bloqueada judicial o administrativamenteNo
ACCOUNT_CLOSEDA conta de destino está encerrada.La cuenta de destino fue cerradaNo
ORDER_REJECTEDA transação foi rejeitada pelo banco destinatário.El banco de destino rechazó la operaciónNo
PAYMENT_EXPIREDA transação expirou antes de ser processada.Se excedió el tiempo de procesamientoSí
SETTLEMENT_TIMEOUTO banco destinatário não respondeu a tempo.Timeout en la comunicación con el banco de destinoSí

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

Reintentos seguros

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 banco
  • details.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 +55 automáticamente

Próximos Pasos

En esta página