docs
Guias de Integração

PIX Cash-Out (Pagamento)

Visão Geral

O endpoint PIX Cash-Out permite que você realize pagamentos PIX para qualquer chave PIX válida (CPF, CNPJ, telefone, email ou chave aleatória). O valor é reservado no saldo da conta assim que o pagamento é aceito, e o resultado final chega por webhook.

Para pagamentos via QR Code PIX (escaneamento ou copia-e-cola), utilize o endpoint dedicado Cash-Out via QR Code. Este endpoint é exclusivo para pagamentos por chave PIX.

Este endpoint requer um token Bearer válido. Verifique a documentação de autenticação para mais detalhes.

Características

  • Pagamentos instantâneos 24/7
  • Suporte a todos os tipos de chave PIX (CPF, CNPJ, telefone, email, chave aleatória)
  • Validação do formato da chave conforme o keyType
  • Verificação de saldo e limites automática
  • Identificação única por externalId (idempotência)
  • Descrição personalizável para o destinatário
  • Confirmação assíncrona via webhook (status PENDING → CONFIRMED ou ERROR)

Endpoint

POST /api/pix/cash-out

Realiza um pagamento PIX.

Headers Obrigatórios

Authorization: Bearer {token}
Content-Type: application/json

Request Body

{
  "value": 250.50,
  "details": {
    "key": "12345678901",
    "keyType": "DOCUMENT",
    "name": "Ana Costa",
    "document": "12345678901"
  },
  "externalId": "PAYMENT-987654-20240119",
  "description": "Pagamento de fornecedor"
}

Request

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": "Pagamento de fornecedor"
  }'

Veja a seção Formatos de Chave PIX abaixo para os formatos exatos aceitos para cada tipo de chave.

Response (201 Created)

{
  "transactionId": "9876",
  "externalId": "PAYMENT-987654-20240119",
  "status": "PENDING",
  "generateTime": "2024-01-19T15:45:00.000Z"
}

Parâmetros da Requisição

valuenumberobrigatorio

Valor do pagamento em reais (BRL). Deve ter no máximo 2 casas decimais.

Mínimo: 0.01

Exemplo: 250.50

detailsobjectobrigatorio

Informações da chave PIX de destino.

details.keystringobrigatorio

Chave PIX de destino. O formato deve corresponder ao keyType informado.

Formatos aceitos:

  • CPF: 12345678901 (11 dígitos, apenas números)
  • CNPJ: 12345678000199 (14 dígitos, apenas números)
  • Email: usuario@exemplo.com
  • Telefone: 11999999999 (10 ou 11 dígitos — DDD + número, sem DDI +55)
  • Chave aleatória: UUID formato xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Veja a tabela completa em Formatos de Chave PIX.

details.keyTypestringobrigatorio

Tipo da chave PIX. O valor é case-insensitive (convertido para maiúsculas automaticamente).

Valores aceitos:

  • DOCUMENT — CPF ou CNPJ (a distinção é feita automaticamente pela contagem de dígitos)
  • EMAIL — Endereço de email
  • PHONE — Número de telefone (DDD + número)
  • RANDOM — Chave aleatória (EVP/UUID)

Por que é obrigatório? A API não realiza consulta DICT para inferir o tipo da chave. A identificação deve vir na requisição para que o sistema normalize o formato correto antes de enviar ao banco liquidante (ex: adicionar +55 em chaves PHONE, converter DOCUMENT para CPF/CNPJ).

Exemplo: "DOCUMENT"

details.namestring

Nome do titular da chave PIX de destino. Opcional na API, mas recomendado; alguns liquidantes exigem o campo.

Máximo: 100 caracteres

Uso: Campo informativo — utilizado para registros de extrato e identificação do destinatário. Não é validado pelo banco liquidante contra o cadastro da chave PIX. Enviar um nome diferente do titular não causa erro na transação.

Exemplo: "Ana Costa"

details.documentstring

CPF ou CNPJ do titular da chave PIX de destino (apenas números). Validado algoritmicamente (dígitos verificadores).

CPF: 11 dígitos | CNPJ: 14 dígitos

Opcional na API, mas fortemente recomendado para todos os tipos de chave — inclusive EMAIL, PHONE e RANDOM. Alguns liquidantes exigem o campo e recusam o pagamento sem ele. Este campo é repassado ao banco liquidante da sua conta. A API FyPay valida apenas o formato: ela não compara details.document com details.key, não consulta a DICT e não bloqueia a operação quando os dois divergem. A conferência com o titular real da chave só acontece em alguns liquidantes; nos demais, um valor divergente não impede o pagamento, que é creditado ao dono da chave. Veja Verificação de titularidade por liquidante.

Dependendo do liquidante da sua conta, informar o documento também pode fazer a recusa de chave inexistente voltar na hora, em vez de chegar depois por webhook. Veja Proteções no saque Pix por chave.

Cenários por keyType:

  • DOCUMENT: o details.document deve ser idêntico ao details.key (ambos são o CPF/CNPJ). A API não verifica essa igualdade: garanta isso do seu lado antes de enviar. Se divergirem, o pagamento vai para o dono da chave, salvo nos liquidantes que conferem titularidade.
  • EMAIL, PHONE, RANDOM: não é possível derivar o documento a partir da chave. O integrador precisa solicitar o CPF/CNPJ ao usuário final e enviar neste campo.

Dica de UX: Sempre exiba um campo de CPF/CNPJ na interface quando o tipo de chave não for DOCUMENT.

Exemplo: "12345678901"

externalIdstringobrigatorio

Identificador único externo da transação. Deve ser único por conta — se repetido, a API retornará erro PIX_DUPLICATE_EXTERNAL_ID (HTTP 400, com details.existingTransactionId). Não pode começar com MIRROR: (prefixo reservado).

Máximo: 50 caracteres

Recomendação: Use um formato que garanta unicidade, como PAY-{timestamp}-{uuid}

Exemplo: "PAYMENT-987654-20240119"

descriptionstring

Descrição do pagamento que aparecerá no extrato do destinatário.

Máximo: 140 caracteres

Padrão: Vazio

Exemplo: "Pagamento de fornecedor - Nota Fiscal 12345"

Estrutura da Resposta

transactionIdstringsempre presente

ID interno da transação gerada pela FyPay.

Exemplo: "9876"

externalIdstringsempre presente

ID externo fornecido na requisição (mesmo valor do input).

Exemplo: "PAYMENT-987654-20240119"

statusstringsempre presente

Status atual da transação.

Valores possíveis:

  • PENDING: Pagamento em processamento
  • CONFIRMED: Pagamento confirmado e finalizado
  • ERROR: Erro no processamento

Exemplo: "PENDING"

Nota: A maioria dos pagamentos PIX é confirmada em poucos segundos

generateTimestringsempre presente

Data e hora de criação do pagamento (ISO 8601 UTC).

Exemplo: "2024-01-19T15:45:00.000Z"

Exemplos de Implementação

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 || `Pagamento 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('Pagamento PIX iniciado com sucesso!');
    console.log(`ID da Transação: ${response.data.transactionId}`);
    console.log(`Status: ${response.data.status}`);
    console.log(`Valor: R$ ${amount.toFixed(2)}`);
    console.log(`Destinatário: ${recipientName}`);

    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      const errorData = error.response?.data;
      console.error('Erro ao realizar pagamento:', errorData);

      // Tratar erros específicos
      if (error.response?.status === 400) {
        if (errorData?.code === 'PIX_INSUFFICIENT_BALANCE') {
          throw new Error('Saldo insuficiente para realizar o pagamento');
        }
        throw new Error('Dados inválidos: ' + errorData?.message);
      }

      if (error.response?.status === 422 || error.response?.status === 429) {
        // Proteções do saque por chave: veja /docs/guides/pix-cash-out-protections
        const retryAfter = error.response.headers['retry-after'];
        throw new Error(`${errorData?.code}: tente de novo em ${retryAfter ?? '?'} s`);
      }

      throw new Error(errorData?.message || 'Erro ao realizar pagamento PIX');
    }
    throw error;
  }
}

// Uso - Pagamento por CPF
sendPixPayment(
  'seu_token_aqui',
  '12345678901',
  'DOCUMENT',
  'Ana Costa',
  '12345678901',
  250.50,
  'Pagamento de fornecedor'
);

// Uso - Pagamento por Email
sendPixPayment(
  'seu_token_aqui',
  'ana.costa@email.com',
  'EMAIL',
  'Ana Costa',
  '12345678901',
  100.00,
  'Reembolso'
);

// Uso - Pagamento por Telefone (DDD + número, sem DDI +55)
sendPixPayment(
  'seu_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:
    """
    Envia um pagamento PIX

    Args:
        token: Token Bearer válido
        recipient_key: Chave PIX do destinatário
        recipient_key_type: Tipo da chave (DOCUMENT, EMAIL, PHONE, RANDOM)
        recipient_name: Nome do destinatário
        recipient_document: CPF ou CNPJ do destinatário
        amount: Valor em reais
        description: Descrição do pagamento (opcional)

    Returns:
        Dados do pagamento 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'Pagamento 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('Pagamento PIX iniciado com sucesso!')
        print(f"ID da Transação: {data['transactionId']}")
        print(f"Status: {data['status']}")
        print(f"Valor: R$ {amount:.2f}")
        print(f"Destinatário: {recipient_name}")

        return data

    except requests.exceptions.HTTPError as e:
        error_data = e.response.json() if e.response else {}

        # Tratar erros específicos
        if e.response.status_code == 400:
            if error_data.get('code') == 'PIX_INSUFFICIENT_BALANCE':
                raise Exception('Saldo insuficiente para realizar o pagamento')
            raise Exception(f"Dados inválidos: {error_data.get('message')}")

        raise Exception(f"Erro ao realizar pagamento: {error_data.get('message', str(e))}")

# Uso
token = 'seu_token_aqui'

# Pagamento 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='Pagamento de fornecedor'
)

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 ?? "Pagamento 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 o pagamento');
        }

        throw new Exception("Erro ao realizar pagamento: $errorMessage");
    }

    $data = json_decode($response, true);

    echo "Pagamento PIX iniciado com sucesso!" . PHP_EOL;
    echo "ID da Transação: {$data['transactionId']}" . PHP_EOL;
    echo "Status: {$data['status']}" . PHP_EOL;
    echo "Valor: R$ " . number_format($amount, 2, ',', '.') . PHP_EOL;
    echo "Destinatário: $recipientName" . PHP_EOL;

    return $data;
}

// Uso
$token = 'seu_token_aqui';
$payment = sendPixPayment(
    $token,
    '12345678901',
    'DOCUMENT',
    'Ana Costa',
    '12345678901',
    250.50,
    'Pagamento de fornecedor'
);

Casos de Uso

1. Folha de Pagamento

class PayrollProcessor {
  constructor(private token: string) {}

  async processPayroll(employees: Employee[]) {
    const results = {
      successful: [],
      failed: []
    };

    for (const employee of employees) {
      try {
        // Verificar saldo antes de cada pagamento
        const balance = await getBalance(this.token);

        if (balance.netBalance < employee.salary) {
          throw new Error('Saldo insuficiente');
        }

        // Realizar pagamento
        const payment = await sendPixPayment(
          this.token,
          employee.pixKey,
          employee.pixKeyType,
          employee.fullName,
          employee.document,
          employee.salary,
          `Salário ${new Date().toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })}`
        );

        results.successful.push({
          employee: employee.fullName,
          amount: employee.salary,
          transactionId: payment.transactionId
        });

        // Aguardar 1 segundo entre pagamentos
        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('seu_token_aqui');
const employees: Employee[] = [
  {
    fullName: 'Pedro Santos',
    document: '12345678901',
    pixKey: '12345678901',
    pixKeyType: 'DOCUMENT',
    salary: 3500.00
  },
  // ... mais funcionários
];

const results = await payroll.processPayroll(employees);
console.log(`Pagamentos bem-sucedidos: ${results.successful.length}`);
console.log(`Pagamentos com erro: ${results.failed.length}`);

2. Marketplace - Repasse para Vendedores

class MarketplacePayouts:
    """Processa repasses para vendedores de marketplace"""

    def __init__(self, token: str):
        self.token = token

    def process_seller_payouts(self, sales_data: list) -> dict:
        """Processa repasses baseados em vendas"""
        results = {'successful': [], 'failed': []}

        # Agrupar vendas por vendedor
        seller_totals = self.group_sales_by_seller(sales_data)

        for seller_id, total_amount in seller_totals.items():
            try:
                # Buscar dados do vendedor
                seller = self.get_seller_data(seller_id)

                # Calcular valor após comissão
                commission = total_amount * 0.10  # 10% de comissão
                payout_amount = total_amount - commission

                # Realizar pagamento
                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'Repasse de vendas - {len(sales_data)} transações'
                )

                results['successful'].append({
                    'seller': seller['name'],
                    'gross_amount': total_amount,
                    'commission': commission,
                    'net_amount': payout_amount,
                    'transaction_id': payment['transactionId']
                })

                # Registrar repasse no banco de dados
                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 vendas 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 dados do pedido
    const order = await this.getOrderData(orderId);

    // Validar se reembolso é permitido
    if (!this.canRefund(order)) {
      throw new Error('Reembolso não permitido para este pedido');
    }

    // Realizar pagamento de volta ao 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}`
    );

    // Atualizar status do pedido
    await this.updateOrderStatus(orderId, 'REFUNDED', refund.transactionId);

    // Enviar notificação ao cliente
    await this.notifyCustomer(order.customer.email, refund);

    return refund;
  }

  canRefund(order) {
    // Verificar se pedido foi pago e ainda está dentro do prazo
    const daysSincePurchase = (Date.now() - new Date(order.paidAt)) / (1000 * 60 * 60 * 24);
    return order.status === 'PAID' && daysSincePurchase <= 7;
  }
}

Formatos de Chave PIX

A tabela abaixo detalha o formato exato aceito para cada tipo de chave. O campo details.key é validado contra o details.keyType informado — se o formato não corresponder, a API retorna erro 400.

keyTypeFormato esperado em details.keyRegex de validaçãoExemplos válidosExemplos inválidos
DOCUMENTCPF: 11 dígitos numéricos^\d{11}$ (com validação de dígitos verificadores)12345678901123.456.789-01 (com pontuação), 1234567890 (10 dígitos)
DOCUMENTCNPJ: 14 dígitos numéricos^\d{14}$ (com validação de dígitos verificadores)1234567800019512.345.678/0001-95 (com pontuação)
EMAILEndereço de email válido^[^\s@]+@[^\s@]+\.[^\s@]{2,}$usuario@exemplo.comusuario@ (sem domínio)
PHONEDDD + número (10 ou 11 dígitos)^\d{10,11}$11999999999 (celular), 1133334444 (fixo)5511999999999 (com DDI), +5511999999999 (com +DDI)
RANDOMUUID (com ou sem hífens)^[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}$ ou ^[a-fA-F0-9]{32}$a1b2c3d4-e5f6-4890-abcd-ef1234567890a1b2c3d4 (incompleto)

Chave PHONE — não inclua o DDI (+55). Envie apenas DDD + número (10 ou 11 dígitos). A API adiciona o código de país +55 automaticamente antes de enviar ao banco liquidante.

Chave DOCUMENT — envie apenas números. CPFs e CNPJs não devem conter pontuação (pontos, barras ou hífens). Além da contagem de dígitos, a API valida os dígitos verificadores do CPF/CNPJ.

Auto-detecção de keyType

Se você deseja implementar detecção automática do keyType com base no valor da chave, use a seguinte lógica de prioridade:

  1. Contém @ → EMAIL
  2. Formato UUID v4 → RANDOM
  3. 14 dígitos + CNPJ válido (check digits) → DOCUMENT
  4. 11 dígitos + CPF válido (check digits) → DOCUMENT
  5. 10-11 dígitos + não é CPF válido → PHONE
function inferPixKeyType(key: string): 'DOCUMENT' | 'EMAIL' | 'PHONE' | 'RANDOM' | null {
  const trimmed = key.trim();

  // 1. UUID (chave aleatória / 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. Apenas dígitos → DOCUMENT ou PHONE
  const digits = trimmed.replace(/\D/g, '');

  // 14 dígitos → CNPJ
  if (digits.length === 14) return 'DOCUMENT';

  // 11 dígitos → CPF ou PHONE
  // A distinção é feita validando os dígitos verificadores do CPF
  if (digits.length === 11) {
    return isValidCpf(digits) ? 'DOCUMENT' : 'PHONE';
  }

  // 10 dígitos → telefone fixo (DDD + 8 dígitos)
  if (digits.length === 10) return 'PHONE';

  return null; // Formato não reconhecido
}

// Validação algorítmica de CPF (dígitos verificadores)
function isValidCpf(cpf: string): boolean {
  if (/^(\d)\1{10}$/.test(cpf)) return false; // Rejeita sequências 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]);
}

Ambiguidade CPF vs. Telefone: Um valor de 11 dígitos pode ser tanto um CPF quanto um número de celular (DDD + 9 dígitos). A lógica acima usa a validação de dígitos verificadores do CPF para distinguir: se os check digits batem, é CPF; caso contrário, é telefone. Mesmo assim, recomendamos que o usuário final selecione explicitamente o tipo da chave na interface para evitar erros em casos limítrofes.

Validação de Chave PIX

Antes de enviar um pagamento, valide o formato da chave PIX localmente:

function validatePixKey(key: string, keyType: string): boolean {
  switch (keyType) {
    case 'DOCUMENT':
      // CPF: 11 dígitos ou CNPJ: 14 dígitos (apenas números)
      return /^\d{11}$/.test(key) || /^\d{14}$/.test(key);

    case 'EMAIL':
      // Email com domínio válido (TLD mínimo 2 caracteres)
      return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(key);

    case 'PHONE':
      // DDD + número: 10 dígitos (fixo) ou 11 dígitos (celular)
      // NÃO inclua DDI (+55) — a API adiciona automaticamente
      return /^\d{10,11}$/.test(key);

    case 'RANDOM':
      // UUID com hífens ou 32 caracteres hexadecimais sem hífens
      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;
  }
}

Verificação de Saldo

Verifique o saldo antes de realizar pagamentos para evitar o erro 400 PIX_INSUFFICIENT_BALANCE.

async function safePayment(
  token: string,
  amount: number,
  recipient: RecipientData
) {
  // Consultar saldo
  const balance = await getBalance(token);

  // Verificar se há saldo suficiente
  if (balance.netBalance < amount) {
    throw new Error(
      `Saldo insuficiente. Disponível: R$ ${balance.netBalance.toFixed(2)} | ` +
      `Necessário: R$ ${amount.toFixed(2)}`
    );
  }

  // Prosseguir com pagamento
  return await sendPixPayment(token, ...recipient, amount);
}

Monitoramento de Status

Para acompanhar a confirmação do pagamento, consulte GET /api/pix/transaction/\{id\} (aceita o transactionId numérico ou o seu 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('Pagamento confirmado!');
      return true;
    }

    if (status === 'ERROR') {
      throw new Error('Pagamento falhou');
    }

    // Aguardar 2 segundos antes de verificar novamente
    await new Promise(resolve => setTimeout(resolve, 2000));
  }

  throw new Error('Timeout: Pagamento não confirmado no tempo esperado');
}

Fluxo de Status da Transação

Após criar um PIX Cash-Out, a transação passa por um fluxo assíncrono:

POST /api/pix/cash-out
         │
         ▼
     ┌─────────┐
     │ PENDING  │  ← Resposta imediata da API (201)
     └────┬─────┘
          │  (processamento no banco liquidante)
          │
    ┌─────┴──────┐
    ▼            ▼
┌──────────┐ ┌───────┐
│CONFIRMED │ │ ERROR │  ← Notificado via webhook
└──────────┘ └───────┘
  1. A API retorna status: "PENDING" imediatamente (201 Created)
  2. O pagamento é enviado ao banco liquidante para processamento
  3. O resultado final (CONFIRMED ou ERROR) é notificado via webhook de Cash-Out
  4. Alternativamente, use o endpoint de polling para consultar o status

A maioria dos pagamentos PIX é confirmada em poucos segundos. Recomendamos implementar o recebimento de webhooks como método principal de confirmação, e polling apenas como fallback.

Dependendo do liquidante da sua conta, a API pode responder na hora (422 ou 429, sem transação e sem webhook) a saques para chaves que o provedor já informou não existirem ou enquanto ele bloqueia consultas. Veja Proteções no saque Pix por chave.

Códigos de Resposta HTTP

CódigoDescriçãoSignificado
201Pagamento IniciadoTransferência PIX iniciada com sucesso (status PENDING)
400Dados InválidosCampos obrigatórios ausentes, formato inválido, externalId repetido, saldo insuficiente ou limite excedido
401Não AutorizadoToken Bearer não fornecido, expirado ou inválido
422Chave não encontradaPIX_KEY_NOT_FOUND: o provedor informou que a chave não existe. Quando a resposta vem do registro de chave negada, o corpo traz details.cached: true e details.retryAfterSeconds. Veja Proteções no saque Pix por chave
429Muitas chaves inexistentesTOO_MANY_PIX_KEYS_NOT_FOUND: o saque por chave está bloqueado por excesso de consultas de chaves inexistentes. Aguarde o tempo do cabeçalho Retry-After. Veja Proteções no saque Pix por chave
500Erro InternoErro inesperado no processamento. Consulte pelo externalId antes de tentar de novo

Erros de Negócio

Além dos códigos HTTP, a API retorna códigos de erro específicos no corpo da resposta. Alguns erros ocorrem de forma síncrona (na resposta da API) e outros de forma assíncrona (via webhook, quando o banco liquidante rejeita a operação).

Erros síncronos (resposta da API)

Retornados imediatamente na resposta HTTP, com status 400 (ou 422 e 429 nas proteções de saque por chave):

Código do erro (code)MensagemCausa
PIX_DUPLICATE_EXTERNAL_IDEsta transação já existe no sistemaO externalId já foi utilizado para esta conta (details.existingTransactionId traz a transação existente)
PIX_INSUFFICIENT_BALANCESaldo insuficiente para realizar esta operaçãoO saldo disponível é menor que o valor + taxa
PIX_TRANSACTION_LIMIT_EXCEEDEDUltrapassa o limite máximo por transaçãoO valor excede o limite configurado por transação
PIX_DAILY_LIMIT_EXCEEDEDVocê atingiu o limite diário de transaçõesO acumulado do dia excede o limite diário
PIX_NIGHT_LIMIT_EXCEEDEDO limite noturno é mais restritoOperação noturna excede o limite reduzido (20h–6h, horário de Brasília)
PIX_LIMIT_EXCEEDEDUltrapassa o limite permitidoOutro limite da conta foi excedido
Erro de validaçãoMensagem por campoValor menor que 0.01, mais de 2 casas decimais, chave em formato que não corresponde ao keyType, documento inválido etc.
PIX_KEY_NOT_FOUNDChave Pix não encontradaStatus 422. Com details.cached: true, é a resposta guardada: respeite details.retryAfterSeconds. Sem cached, veio do provedor nesta chamada. Veja Proteções no saque Pix por chave
TOO_MANY_PIX_KEYS_NOT_FOUNDMuitas tentativas com chaves Pix inexistentes. Saque por chave bloqueado temporariamente; tente novamente em instantes.Status 429: o saque por chave está bloqueado por excesso de consultas de chaves inexistentes. Aguarde o tempo do cabeçalho Retry-After. Veja Proteções no saque Pix por chave

Erros assíncronos (via webhook)

Quando o banco liquidante rejeita a operação, a transação muda para status ERROR. Os campos errorCode e errorMessage são enviados no payload do webhook de Cash-Out. O valor de errorCode varia conforme o liquidante da conta e não deve ser usado para lógica: trate-o como informativo e exiba errorMessage ao usuário.

{
  "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.",
  ...
}

Exemplos de códigos padronizados que alguns liquidantes devolvem:

errorCodeerrorMessageCausaRetentável?
TAX_ID_MISMATCHO CPF/CNPJ informado não corresponde ao titular da conta.O details.document não confere com o dono da chave PIX. Em outros liquidantes que conferem titularidade, a divergência chega com o código e a mensagem do próprio liquidanteNão
INVALID_TAX_IDCPF ou CNPJ inválido.O documento enviado falhou na validação do bancoNão
BLOCKED_ACCOUNTA conta de destino está bloqueada e não pode receber transferências.Conta de destino bloqueada judicialmente ou administrativamenteNão
ACCOUNT_CLOSEDA conta de destino está encerrada.A conta de destino foi encerradaNão
ORDER_REJECTEDA transação foi rejeitada pelo banco destinatário.O banco de destino recusou a operaçãoNão
PAYMENT_EXPIREDA transação expirou antes de ser processada.O tempo de processamento foi excedidoSim
SETTLEMENT_TIMEOUTO banco destinatário não respondeu a tempo.Timeout na comunicação com o banco de destinoSim

Para erros retentáveis (PAYMENT_EXPIRED, SETTLEMENT_TIMEOUT), é seguro reenviar a transação com um novo externalId. Para falhas de rede (sem resposta), veja «Retentativas seguras» abaixo — nesses casos reutilize o mesmo externalId.

Quando um erro assíncrono ocorre, a transação é automaticamente cancelada (rollback no ledger) e o saldo é estornado. Não é necessário tomar nenhuma ação adicional — apenas notifique o usuário final sobre o motivo da falha.

Consulte a Referência da API para detalhes completos dos campos de resposta.

Boas Práticas

Retentativas seguras

Observações Importantes

  • Valor mínimo: R$ 0,01 | Casas decimais: Máximo 2
  • externalId: Máximo 50 caracteres, único por conta (idempotência)
  • description: Máximo 140 caracteres (opcional)
  • details.name: Opcional, máximo 100 caracteres — informativo, não validado pelo banco
  • details.document: Opcional na API, recomendado para todos os tipos de chave — a API valida só o formato; a conferência com o titular da chave depende do liquidante da conta
  • Chave PHONE: Envie apenas DDD + número (10-11 dígitos). A API adiciona +55 automaticamente

Próximos Passos

Nesta página