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/jsonRequest 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
valuenumberobrigatorioPayment amount in BRL (Brazilian Reais). Must have at most 2 decimal places.
Minimum: 0.01
Example: 250.50
detailsobjectobrigatorioDestination PIX key information.
details.keystringobrigatorioDestination 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+55country code) - Random key: UUID format
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
See the full table in PIX Key Formats.
details.keyTypestringobrigatorioPIX 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 addressPHONE— 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.namestringName 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.documentstringCPF 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.documentmust be identical todetails.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"
externalIdstringobrigatorioUnique 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"
descriptionstringPayment description that will appear on the recipient's statement.
Maximum: 140 characters
Default: Empty
Example: "Pagamento de fornecedor - Nota Fiscal 12345"
Response Structure
transactionIdstringsempre presenteInternal transaction ID generated by FyPay.
Example: "9876"
externalIdstringsempre presenteExternal ID provided in the request (same value as the input).
Example: "PAYMENT-987654-20240119"
statusstringsempre presenteCurrent transaction status.
Possible values:
PENDING: Payment being processedCONFIRMED: Payment confirmed and completedERROR: Processing error
Example: "PENDING"
Note: Most PIX payments are confirmed within a few seconds
generateTimestringsempre presentePayment 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 totals3. 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.
keyType | Expected format in details.key | Validation regex | Valid examples | Invalid examples |
|---|---|---|---|---|
DOCUMENT | CPF: 11 numeric digits | ^\d{11}$ (with check digit validation) | 12345678901 | 123.456.789-01 (with punctuation), 1234567890 (10 digits) |
DOCUMENT | CNPJ: 14 numeric digits | ^\d{14}$ (with check digit validation) | 12345678000195 | 12.345.678/0001-95 (with punctuation) |
EMAIL | Valid email address | ^[^\s@]+@[^\s@]+\.[^\s@]{2,}$ | usuario@exemplo.com | usuario@ (no domain) |
PHONE | Area code + number (10 or 11 digits) | ^\d{10,11}$ | 11999999999 (mobile), 1133334444 (landline) | 5511999999999 (with country code), +5511999999999 (with +country code) |
RANDOM | UUID (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-ef1234567890 | a1b2c3d4 (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:
- Contains
@→EMAIL - UUID v4 format →
RANDOM - 14 digits + valid CNPJ (check digits) →
DOCUMENT - 11 digits + valid CPF (check digits) →
DOCUMENT - 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
└──────────┘ └───────┘- The API returns
status: "PENDING"immediately (201 Created) - The payment is sent to the settlement bank for processing
- The final result (
CONFIRMEDorERROR) is notified via the Cash-Out webhook - 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
| Code | Description | Meaning |
|---|---|---|
201 | Payment Initiated | PIX transfer initiated successfully (status PENDING) |
400 | Invalid Data | Missing required fields, invalid format, repeated externalId, insufficient balance, or limit exceeded |
401 | Unauthorized | Bearer token not provided, expired, or invalid |
422 | Key Not Found | PIX_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 |
429 | Too many non-existent keys | TOO_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 |
500 | Internal Error | Unexpected 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) | Message | Cause |
|---|---|---|
PIX_DUPLICATE_EXTERNAL_ID | Esta transação já existe no sistema | The externalId was already used for this account (details.existingTransactionId carries the existing transaction) |
PIX_INSUFFICIENT_BALANCE | Saldo insuficiente para realizar esta operação | The available balance is lower than the amount + fee |
PIX_TRANSACTION_LIMIT_EXCEEDED | Ultrapassa o limite máximo por transação | The amount exceeds the configured per-transaction limit |
PIX_DAILY_LIMIT_EXCEEDED | Você atingiu o limite diário de transações | The day's accumulated total exceeds the daily limit |
PIX_NIGHT_LIMIT_EXCEEDED | O limite noturno é mais restrito | Night-time operation exceeds the reduced limit (8 PM–6 AM, Brasília time) |
PIX_LIMIT_EXCEEDED | Ultrapassa o limite permitido | Another account limit was exceeded |
| Validation error | Message per field | Amount below 0.01, more than 2 decimal places, key in a format that does not match keyType, invalid document, etc. |
PIX_KEY_NOT_FOUND | Chave Pix não encontrada | Status 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_FOUND | Muitas 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:
errorCode | errorMessage | Cause | Retryable? |
|---|---|---|---|
TAX_ID_MISMATCH | O 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 message | No |
INVALID_TAX_ID | CPF ou CNPJ inválido. | The submitted document failed the bank's validation | No |
BLOCKED_ACCOUNT | A conta de destino está bloqueada e não pode receber transferências. | Destination account blocked by court order or administratively | No |
ACCOUNT_CLOSED | A conta de destino está encerrada. | The destination account was closed | No |
ORDER_REJECTED | A transação foi rejeitada pelo banco destinatário. | The destination bank refused the operation | No |
PAYMENT_EXPIRED | A transação expirou antes de ser processada. | The processing time was exceeded | Yes |
SETTLEMENT_TIMEOUT | O banco destinatário não respondeu a tempo. | Timeout communicating with the destination bank | Yes |
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
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 bankdetails.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
+55automatically