docs
Integration Guides

PIX Cash-Out (Payment)

Overview

The PIX Cash-Out endpoint allows you to make PIX payments to any valid PIX key (CPF, CNPJ, phone, email, or random key). The amount is reserved in the account balance as soon as the payment is accepted, and the final result arrives by webhook.

For payments via PIX QR Code (scanning or copy-and-paste), use the dedicated Cash-Out via QR Code endpoint. This endpoint is exclusively for PIX key payments.

This endpoint requires a valid Bearer token. See the authentication documentation for more details.

Features

  • Instant payments 24/7
  • Support for all PIX key types (CPF, CNPJ, phone, email, random key)
  • Key format validation according to keyType
  • Automatic balance and limit verification
  • Unique identification via externalId (idempotency)
  • Customizable description for the recipient
  • Asynchronous confirmation via webhook (status PENDING → CONFIRMED or ERROR)

Endpoint

POST /api/pix/cash-out

Makes a PIX payment.

Required Headers

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"
  }'

See the PIX Key Formats section below for the exact formats accepted for each key type.

Response (201 Created)

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

Request Parameters

valuenumberobrigatorio

Payment amount in BRL (Brazilian Reais). Must have at most 2 decimal places.

Minimum: 0.01

Example: 250.50

detailsobjectobrigatorio

Destination PIX key information.

details.keystringobrigatorio

Destination PIX key. The format must match the provided keyType.

Accepted formats:

  • CPF: 12345678901 (11 digits, numbers only)
  • CNPJ: 12345678000199 (14 digits, numbers only)
  • Email: usuario@exemplo.com
  • Phone: 11999999999 (10 or 11 digits — area code + number, without the +55 country code)
  • Random key: UUID format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

See the full table in PIX Key Formats.

details.keyTypestringobrigatorio

PIX key type. The value is case-insensitive (automatically converted to uppercase).

Accepted values:

  • DOCUMENT — CPF or CNPJ (the distinction is made automatically by the digit count)
  • EMAIL — Email address
  • PHONE — Phone number (area code + number)
  • RANDOM — Random key (EVP/UUID)

Why is it required? The API does not perform a DICT lookup to infer the key type. The identification must come in the request so the system can normalize the correct format before sending it to the settlement bank (e.g., adding +55 to PHONE keys, converting DOCUMENT to CPF/CNPJ).

Example: "DOCUMENT"

details.namestring

Name of the destination PIX key holder. Optional in the API, but recommended; some settlement providers require the field.

Maximum: 100 characters

Usage: Informational field — used for statement records and recipient identification. It is not validated by the settlement bank against the PIX key registration. Sending a name different from the holder's does not cause an error in the transaction.

Example: "Ana Costa"

details.documentstring

CPF or CNPJ of the destination PIX key holder (numbers only). Validated algorithmically (check digits).

CPF: 11 digits | CNPJ: 14 digits

Optional in the API, but strongly recommended for all key types — including EMAIL, PHONE, and RANDOM. Some settlement providers require the field and reject the payment without it. This field is forwarded to your account's settlement bank. The FyPay API only validates the format: it does not compare details.document with details.key, does not query DICT, and does not block the operation when the two differ. Checking it against the actual key holder only happens with some settlement providers; with the others, a mismatching value does not prevent the payment, which is credited to the key owner. See Ownership verification by settlement provider.

Depending on your account's settlement provider, providing the document can also make the non-existent key rejection come back immediately, instead of arriving later by webhook. See Safeguards on Pix key cash-out.

Scenarios by keyType:

  • DOCUMENT: details.document must be identical to details.key (both are the CPF/CNPJ). The API does not check this equality: ensure it on your side before sending. If they differ, the payment goes to the key owner, except with settlement providers that check ownership.
  • EMAIL, PHONE, RANDOM: the document cannot be derived from the key. The integrator must ask the end user for the CPF/CNPJ and send it in this field.

UX tip: Always display a CPF/CNPJ field in the interface when the key type is not DOCUMENT.

Example: "12345678901"

externalIdstringobrigatorio

Unique external transaction identifier. Must be unique per account — if repeated, the API returns the PIX_DUPLICATE_EXTERNAL_ID error (HTTP 400, with details.existingTransactionId). Cannot start with MIRROR: (reserved prefix).

Maximum: 50 characters

Recommendation: Use a format that ensures uniqueness, such as PAY-{timestamp}-{uuid}

Example: "PAYMENT-987654-20240119"

descriptionstring

Payment description that will appear on the recipient's statement.

Maximum: 140 characters

Default: Empty

Example: "Pagamento de fornecedor - Nota Fiscal 12345"

Response Structure

transactionIdstringsempre presente

Internal transaction ID generated by FyPay.

Example: "9876"

externalIdstringsempre presente

External ID provided in the request (same value as the input).

Example: "PAYMENT-987654-20240119"

statusstringsempre presente

Current transaction status.

Possible values:

  • PENDING: Payment being processed
  • CONFIRMED: Payment confirmed and completed
  • ERROR: Processing error

Example: "PENDING"

Note: Most PIX payments are confirmed within a few seconds

generateTimestringsempre presente

Payment creation date and time (ISO 8601 UTC).

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

Implementation Examples

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('PIX payment initiated successfully!');
    console.log(`Transaction ID: ${response.data.transactionId}`);
    console.log(`Status: ${response.data.status}`);
    console.log(`Amount: R$ ${amount.toFixed(2)}`);
    console.log(`Recipient: ${recipientName}`);

    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      const errorData = error.response?.data;
      console.error('Error making payment:', errorData);

      // Handle specific errors
      if (error.response?.status === 400) {
        if (errorData?.code === 'PIX_INSUFFICIENT_BALANCE') {
          throw new Error('Insufficient balance to make the payment');
        }
        throw new Error('Invalid data: ' + errorData?.message);
      }

      if (error.response?.status === 422 || error.response?.status === 429) {
        // Key cash-out safeguards: see /en/docs/guides/pix-cash-out-protections
        const retryAfter = error.response.headers['retry-after'];
        throw new Error(`${errorData?.code}: try again in ${retryAfter ?? '?'} s`);
      }

      throw new Error(errorData?.message || 'Error making PIX payment');
    }
    throw error;
  }
}

// Usage - Payment by CPF
sendPixPayment(
  'your_token_here',
  '12345678901',
  'DOCUMENT',
  'Ana Costa',
  '12345678901',
  250.50,
  'Pagamento de fornecedor'
);

// Usage - Payment by Email
sendPixPayment(
  'your_token_here',
  'ana.costa@email.com',
  'EMAIL',
  'Ana Costa',
  '12345678901',
  100.00,
  'Reembolso'
);

// Usage - Payment by Phone (area code + number, without the +55 country code)
sendPixPayment(
  'your_token_here',
  '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:
    """
    Send a PIX payment

    Args:
        token: Valid Bearer token
        recipient_key: Recipient's PIX key
        recipient_key_type: Key type (DOCUMENT, EMAIL, PHONE, RANDOM)
        recipient_name: Recipient's name
        recipient_document: Recipient's CPF or CNPJ
        amount: Amount in BRL
        description: Payment description (optional)

    Returns:
        Initiated payment data
    """
    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('PIX payment initiated successfully!')
        print(f"Transaction ID: {data['transactionId']}")
        print(f"Status: {data['status']}")
        print(f"Amount: R$ {amount:.2f}")
        print(f"Recipient: {recipient_name}")

        return data

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

        # Handle specific errors
        if e.response.status_code == 400:
            if error_data.get('code') == 'PIX_INSUFFICIENT_BALANCE':
                raise Exception('Insufficient balance to make the payment')
            raise Exception(f"Invalid data: {error_data.get('message')}")

        raise Exception(f"Error making payment: {error_data.get('message', str(e))}")

# Usage
token = 'your_token_here'

# Payment by 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('Insufficient balance to make the payment');
        }

        throw new Exception("Error making payment: $errorMessage");
    }

    $data = json_decode($response, true);

    echo "PIX payment initiated successfully!" . PHP_EOL;
    echo "Transaction ID: {$data['transactionId']}" . PHP_EOL;
    echo "Status: {$data['status']}" . PHP_EOL;
    echo "Amount: R$ " . number_format($amount, 2, ',', '.') . PHP_EOL;
    echo "Recipient: $recipientName" . PHP_EOL;

    return $data;
}

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

Use Cases

1. Payroll

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

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

    for (const employee of employees) {
      try {
        // Check balance before each payment
        const balance = await getBalance(this.token);

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

        // Make payment
        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
        });

        // Wait 1 second between payments
        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));
  }
}

// Usage
interface Employee {
  fullName: string;
  document: string;
  pixKey: string;
  pixKeyType: 'DOCUMENT' | 'EMAIL' | 'PHONE' | 'RANDOM';
  salary: number;
}

const payroll = new PayrollProcessor('your_token_here');
const employees: Employee[] = [
  {
    fullName: 'Pedro Santos',
    document: '12345678901',
    pixKey: '12345678901',
    pixKeyType: 'DOCUMENT',
    salary: 3500.00
  },
  // ... more employees
];

const results = await payroll.processPayroll(employees);
console.log(`Successful payments: ${results.successful.length}`);
console.log(`Failed payments: ${results.failed.length}`);

2. Marketplace - Seller Payouts

class MarketplacePayouts:
    """Processes payouts for marketplace sellers"""

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

    def process_seller_payouts(self, sales_data: list) -> dict:
        """Processes payouts based on sales"""
        results = {'successful': [], 'failed': []}

        # Group sales by seller
        seller_totals = self.group_sales_by_seller(sales_data)

        for seller_id, total_amount in seller_totals.items():
            try:
                # Fetch seller data
                seller = self.get_seller_data(seller_id)

                # Calculate amount after commission
                commission = total_amount * 0.10  # 10% commission
                payout_amount = total_amount - commission

                # Make payment
                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']
                })

                # Record payout in the database
                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:
        """Groups sales by seller"""
        totals = {}
        for sale in sales_data:
            seller_id = sale['seller_id']
            totals[seller_id] = totals.get(seller_id, 0) + sale['amount']
        return totals

3. Refund System

class RefundSystem {
  constructor(token) {
    this.token = token;
  }

  async processRefund(orderId, refundReason) {
    // Fetch order data
    const order = await this.getOrderData(orderId);

    // Validate if refund is allowed
    if (!this.canRefund(order)) {
      throw new Error('Refund not allowed for this order');
    }

    // Make payment back to the customer
    const refund = await sendPixPayment(
      this.token,
      order.customer.pixKey,
      order.customer.pixKeyType,
      order.customer.name,
      order.customer.document,
      order.amount,
      `Reembolso - Pedido ${orderId} - ${refundReason}`
    );

    // Update order status
    await this.updateOrderStatus(orderId, 'REFUNDED', refund.transactionId);

    // Notify customer
    await this.notifyCustomer(order.customer.email, refund);

    return refund;
  }

  canRefund(order) {
    // Check if the order was paid and is still within the refund window
    const daysSincePurchase = (Date.now() - new Date(order.paidAt)) / (1000 * 60 * 60 * 24);
    return order.status === 'PAID' && daysSincePurchase <= 7;
  }
}

PIX Key Formats

The table below details the exact format accepted for each key type. The details.key field is validated against the provided details.keyType — if the format does not match, the API returns a 400 error.

keyTypeExpected format in details.keyValidation regexValid examplesInvalid examples
DOCUMENTCPF: 11 numeric digits^\d{11}$ (with check digit validation)12345678901123.456.789-01 (with punctuation), 1234567890 (10 digits)
DOCUMENTCNPJ: 14 numeric digits^\d{14}$ (with check digit validation)1234567800019512.345.678/0001-95 (with punctuation)
EMAILValid email address^[^\s@]+@[^\s@]+\.[^\s@]{2,}$usuario@exemplo.comusuario@ (no domain)
PHONEArea code + number (10 or 11 digits)^\d{10,11}$11999999999 (mobile), 1133334444 (landline)5511999999999 (with country code), +5511999999999 (with +country code)
RANDOMUUID (with or without hyphens)^[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}$ or ^[a-fA-F0-9]{32}$a1b2c3d4-e5f6-4890-abcd-ef1234567890a1b2c3d4 (incomplete)

PHONE key — do not include the country code (+55). Send only area code + number (10 or 11 digits). The API adds the +55 country code automatically before sending it to the settlement bank.

DOCUMENT key — send numbers only. CPFs and CNPJs must not contain punctuation (dots, slashes, or hyphens). Besides the digit count, the API validates the CPF/CNPJ check digits.

keyType auto-detection

If you want to implement automatic keyType detection based on the key value, use the following priority logic:

  1. Contains @ → EMAIL
  2. UUID v4 format → RANDOM
  3. 14 digits + valid CNPJ (check digits) → DOCUMENT
  4. 11 digits + valid CPF (check digits) → DOCUMENT
  5. 10-11 digits + not a valid CPF → PHONE
function inferPixKeyType(key: string): 'DOCUMENT' | 'EMAIL' | 'PHONE' | 'RANDOM' | null {
  const trimmed = key.trim();

  // 1. UUID (random key / 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. Digits only → DOCUMENT or PHONE
  const digits = trimmed.replace(/\D/g, '');

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

  // 11 digits → CPF or PHONE
  // The distinction is made by validating the CPF check digits
  if (digits.length === 11) {
    return isValidCpf(digits) ? 'DOCUMENT' : 'PHONE';
  }

  // 10 digits → landline (area code + 8 digits)
  if (digits.length === 10) return 'PHONE';

  return null; // Unrecognized format
}

// Algorithmic CPF validation (check digits)
function isValidCpf(cpf: string): boolean {
  if (/^(\d)\1{10}$/.test(cpf)) return false; // Rejects sequences like 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]);
}

CPF vs. Phone ambiguity: An 11-digit value can be either a CPF or a mobile number (area code + 9 digits). The logic above uses the CPF check digit validation to distinguish them: if the check digits match, it is a CPF; otherwise, it is a phone. Even so, we recommend that the end user explicitly select the key type in the interface to avoid errors in edge cases.

PIX Key Validation

Before sending a payment, validate the PIX key format locally:

function validatePixKey(key: string, keyType: string): boolean {
  switch (keyType) {
    case 'DOCUMENT':
      // CPF: 11 digits or CNPJ: 14 digits (numbers only)
      return /^\d{11}$/.test(key) || /^\d{14}$/.test(key);

    case 'EMAIL':
      // Email with valid domain (TLD of at least 2 characters)
      return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(key);

    case 'PHONE':
      // Area code + number: 10 digits (landline) or 11 digits (mobile)
      // Do NOT include the country code (+55) — the API adds it automatically
      return /^\d{10,11}$/.test(key);

    case 'RANDOM':
      // UUID with hyphens or 32 hexadecimal characters without hyphens
      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;
  }
}

Balance Verification

Check the balance before making payments to avoid the 400 PIX_INSUFFICIENT_BALANCE error.

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

  // Check if there is sufficient balance
  if (balance.netBalance < amount) {
    throw new Error(
      `Insufficient balance. Available: R$ ${balance.netBalance.toFixed(2)} | ` +
      `Required: R$ ${amount.toFixed(2)}`
    );
  }

  // Proceed with payment
  return await sendPixPayment(token, ...recipient, amount);
}

Status Monitoring

To track payment confirmation, query GET /api/pix/transaction/\{id\} (accepts the numeric transactionId or your 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('Payment confirmed!');
      return true;
    }

    if (status === 'ERROR') {
      throw new Error('Payment failed');
    }

    // Wait 2 seconds before checking again
    await new Promise(resolve => setTimeout(resolve, 2000));
  }

  throw new Error('Timeout: Payment not confirmed within expected time');
}

Transaction Status Flow

After creating a PIX Cash-Out, the transaction goes through an asynchronous flow:

POST /api/pix/cash-out
         │
         ▼
     ┌─────────┐
     │ PENDING  │  ← Immediate API response (201)
     └────┬─────┘
          │  (processing at the settlement bank)
          │
    ┌─────┴──────┐
    ▼            ▼
┌──────────┐ ┌───────┐
│CONFIRMED │ │ ERROR │  ← Notified via webhook
└──────────┘ └───────┘
  1. The API returns status: "PENDING" immediately (201 Created)
  2. The payment is sent to the settlement bank for processing
  3. The final result (CONFIRMED or ERROR) is notified via the Cash-Out webhook
  4. Alternatively, use the polling endpoint to query the status

Most PIX payments are confirmed within a few seconds. We recommend implementing webhook reception as the main confirmation method, and polling only as a fallback.

Depending on your account's settlement provider, the API may answer immediately (422 or 429, with no transaction and no webhook) to cash-outs for keys the provider already reported as non-existent or while it is blocking lookups. See Safeguards on Pix key cash-out.

HTTP Response Codes

CodeDescriptionMeaning
201Payment InitiatedPIX transfer initiated successfully (status PENDING)
400Invalid DataMissing required fields, invalid format, repeated externalId, insufficient balance, or limit exceeded
401UnauthorizedBearer token not provided, expired, or invalid
422Key Not FoundPIX_KEY_NOT_FOUND: the provider reported that the key does not exist. When the response comes from the denied-key record, the body carries details.cached: true and details.retryAfterSeconds. See Safeguards on Pix key cash-out
429Too many non-existent keysTOO_MANY_PIX_KEYS_NOT_FOUND: key cash-out is blocked after too many lookups of non-existent keys. Wait the time given in the Retry-After header. See Safeguards on Pix key cash-out
500Internal ErrorUnexpected processing error. Look it up by externalId before trying again

Business Errors

Besides the HTTP codes, the API returns specific error codes in the response body. Some errors occur synchronously (in the API response) and others asynchronously (via webhook, when the settlement bank rejects the operation).

Synchronous errors (API response)

Returned immediately in the HTTP response, with status 400 (or 422 and 429 for the key cash-out safeguards):

Error code (code)MessageCause
PIX_DUPLICATE_EXTERNAL_IDEsta transação já existe no sistemaThe externalId was already used for this account (details.existingTransactionId carries the existing transaction)
PIX_INSUFFICIENT_BALANCESaldo insuficiente para realizar esta operaçãoThe available balance is lower than the amount + fee
PIX_TRANSACTION_LIMIT_EXCEEDEDUltrapassa o limite máximo por transaçãoThe amount exceeds the configured per-transaction limit
PIX_DAILY_LIMIT_EXCEEDEDVocê atingiu o limite diário de transaçõesThe day's accumulated total exceeds the daily limit
PIX_NIGHT_LIMIT_EXCEEDEDO limite noturno é mais restritoNight-time operation exceeds the reduced limit (8 PM–6 AM, Brasília time)
PIX_LIMIT_EXCEEDEDUltrapassa o limite permitidoAnother account limit was exceeded
Validation errorMessage per fieldAmount below 0.01, more than 2 decimal places, key in a format that does not match keyType, invalid document, etc.
PIX_KEY_NOT_FOUNDChave Pix não encontradaStatus 422. With details.cached: true, it is the stored answer: respect details.retryAfterSeconds. Without cached, it came from the provider in this call. See Safeguards on Pix key cash-out
TOO_MANY_PIX_KEYS_NOT_FOUNDMuitas tentativas com chaves Pix inexistentes. Saque por chave bloqueado temporariamente; tente novamente em instantes.Status 429: key cash-out is blocked after too many lookups of non-existent keys. Wait the time given in the Retry-After header. See Safeguards on Pix key cash-out

Asynchronous errors (via webhook)

When the settlement bank rejects the operation, the transaction changes to ERROR status. The errorCode and errorMessage fields are sent in the Cash-Out webhook payload. The errorCode value varies according to the account's settlement provider and should not be used for logic: treat it as informational and display errorMessage to the user.

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

Examples of standardized codes that some settlement providers return:

errorCodeerrorMessageCauseRetryable?
TAX_ID_MISMATCHO CPF/CNPJ informado não corresponde ao titular da conta.details.document does not match the PIX key owner. With other settlement providers that check ownership, the mismatch arrives with the provider's own code and messageNo
INVALID_TAX_IDCPF ou CNPJ inválido.The submitted document failed the bank's validationNo
BLOCKED_ACCOUNTA conta de destino está bloqueada e não pode receber transferências.Destination account blocked by court order or administrativelyNo
ACCOUNT_CLOSEDA conta de destino está encerrada.The destination account was closedNo
ORDER_REJECTEDA transação foi rejeitada pelo banco destinatário.The destination bank refused the operationNo
PAYMENT_EXPIREDA transação expirou antes de ser processada.The processing time was exceededYes
SETTLEMENT_TIMEOUTO banco destinatário não respondeu a tempo.Timeout communicating with the destination bankYes

For retryable errors (PAYMENT_EXPIRED, SETTLEMENT_TIMEOUT), it is safe to resend the transaction with a new externalId. For network failures (no response), see "Safe retries" below — in those cases reuse the same externalId.

When an asynchronous error occurs, the transaction is automatically canceled (ledger rollback) and the balance is reversed. No additional action is needed — just notify the end user about the reason for the failure.

See the API Reference for full response field details.

Best Practices

Safe retries

Important Notes

  • Minimum amount: R$ 0.01 | Decimal places: Maximum 2
  • externalId: Maximum 50 characters, unique per account (idempotency)
  • description: Maximum 140 characters (optional)
  • details.name: Optional, maximum 100 characters — informational, not validated by the bank
  • details.document: Optional in the API, recommended for all key types — the API only validates the format; checking it against the key holder depends on the account's settlement provider
  • PHONE key: Send only area code + number (10-11 digits). The API adds +55 automatically

Next Steps

On this page