docs
集成指南

PIX Cash-Out(付款)

概述

PIX Cash-Out 端点允许您向任意有效的 PIX 密钥(CPF、CNPJ、电话、邮箱或随机密钥)发起 PIX 付款。付款一经受理,金额即在账户余额中预留,最终结果通过 webhook 送达。

对于通过 PIX 二维码付款(扫描或复制粘贴),请使用专用的 二维码 Cash-Out 端点。此端点专用于 PIX 密钥付款。

此端点需要有效的 Bearer token。详情请参阅认证文档。

功能特性

  • 全天候 24/7 即时付款
  • 支持所有 PIX 密钥类型(CPF、CNPJ、电话、邮箱、随机密钥)
  • 根据 keyType 校验密钥格式
  • 自动检查余额和限额
  • 通过 externalId 唯一标识(幂等性)
  • 可为收款方自定义描述
  • 通过 webhook 异步确认(状态 PENDING → CONFIRMED 或 ERROR)

端点

POST /api/pix/cash-out

发起一笔 PIX 付款。

必需请求头

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

请求体

{
  "value": 250.50,
  "details": {
    "key": "12345678901",
    "keyType": "DOCUMENT",
    "name": "Ana Costa",
    "document": "12345678901"
  },
  "externalId": "PAYMENT-987654-20240119",
  "description": "供应商付款"
}

请求

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": "供应商付款"
  }'

各密钥类型接受的确切格式请参阅下方的 PIX 密钥格式 部分。

响应 (201 Created)

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

请求参数

valuenumberobrigatorio

付款金额,单位为巴西雷亚尔(BRL)。最多 2 位小数。

最小值: 0.01

示例: 250.50

detailsobjectobrigatorio

目标 PIX 密钥的信息。

details.keystringobrigatorio

目标 PIX 密钥。格式必须与所提供的 keyType 相符。

接受的格式:

  • CPF: 12345678901(11 位,仅数字)
  • CNPJ: 12345678000199(14 位,仅数字)
  • 邮箱: usuario@exemplo.com
  • 电话: 11999999999(10 或 11 位 — 区号 + 号码,不含国家代码 +55)
  • 随机密钥: UUID 格式 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

完整表格请参阅 PIX 密钥格式。

details.keyTypestringobrigatorio

PIX 密钥类型。该值不区分大小写(会自动转换为大写)。

接受的值:

  • DOCUMENT — CPF 或 CNPJ(根据位数自动区分)
  • EMAIL — 邮箱地址
  • PHONE — 电话号码(区号 + 号码)
  • RANDOM — 随机密钥(EVP/UUID)

为什么是必填? API 不会通过 DICT 查询来推断密钥类型。类型必须在请求中提供,以便系统在发送给清算银行之前规范为正确的格式(例如为 PHONE 密钥添加 +55、将 DOCUMENT 转换为 CPF/CNPJ)。

示例: "DOCUMENT"

details.namestring

目标 PIX 密钥持有人的姓名。在 API 中为可选,但建议填写;部分清算机构要求提供该字段。

最大长度: 100 个字符

用途: 信息性字段 — 用于对账单记录和识别收款方。清算银行不会将其与 PIX 密钥的登记信息进行校验。提交与持有人不同的姓名不会导致交易出错。

示例: "Ana Costa"

details.documentstring

目标 PIX 密钥持有人的 CPF 或 CNPJ(仅数字)。会进行算法校验(校验位)。

CPF: 11 位 | CNPJ: 14 位

在 API 中为可选,但强烈建议所有密钥类型都提供 — 包括 EMAIL、PHONE 和 RANDOM。部分清算机构要求提供该字段,缺少时会拒绝付款。该字段会转发给您账户的清算银行。FyPay API 仅校验格式:不会将 details.document 与 details.key 进行比对,不会查询 DICT,两者不一致时也不会阻止操作。 与密钥真实持有人的比对仅在部分清算机构中进行;在其他清算机构中,不一致的值不会阻止付款,款项会入账给密钥的所有者。参见按清算机构划分的持有人核验。

视您账户的清算机构而定,提供证件号还可能使不存在密钥的拒绝结果立即返回,而不是稍后通过 webhook 到达。参见 Pix 密钥出金保护机制。

按 keyType 的场景:

  • DOCUMENT:details.document 必须与 details.key 完全相同(两者都是 CPF/CNPJ)。API 不会校验两者是否相同:请在发送前在您这边确保这一点。如果不一致,款项会付给密钥的所有者,核验持有人的清算机构除外。
  • EMAIL、PHONE、RANDOM:无法从密钥推导出证件号。集成方需要向最终用户索取 CPF/CNPJ 并填入此字段。

UX 提示: 当密钥类型不是 DOCUMENT 时,请始终在界面中显示 CPF/CNPJ 输入框。

示例: "12345678901"

externalIdstringobrigatorio

交易的外部唯一标识符。在每个账户内必须唯一 — 如果重复,API 将返回 PIX_DUPLICATE_EXTERNAL_ID 错误(HTTP 400,附带 details.existingTransactionId)。不能以 MIRROR: 开头(保留前缀)。

最大长度: 50 个字符

建议: 使用能保证唯一性的格式,例如 PAY-{timestamp}-{uuid}

示例: "PAYMENT-987654-20240119"

descriptionstring

将显示在收款方对账单中的付款描述。

最大长度: 140 个字符

默认值: 空

示例: "供应商付款 - 发票 12345"

响应结构

transactionIdstringsempre presente

FyPay 生成的交易内部 ID。

示例: "9876"

externalIdstringsempre presente

请求中提供的外部 ID(与输入值相同)。

示例: "PAYMENT-987654-20240119"

statusstringsempre presente

交易的当前状态。

可能的值:

  • PENDING:付款处理中
  • CONFIRMED:付款已确认并完成
  • ERROR:处理出错

示例: "PENDING"

注意: 大多数 PIX 付款会在几秒内确认

generateTimestringsempre presente

付款创建的日期和时间(ISO 8601 UTC)。

示例: "2024-01-19T15:45:00.000Z"

实现示例

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 || `PIX 付款 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 付款已成功发起!');
    console.log(`交易 ID:${response.data.transactionId}`);
    console.log(`状态:${response.data.status}`);
    console.log(`金额:R$ ${amount.toFixed(2)}`);
    console.log(`收款方:${recipientName}`);

    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      const errorData = error.response?.data;
      console.error('付款出错:', errorData);

      // 处理特定错误
      if (error.response?.status === 400) {
        if (errorData?.code === 'PIX_INSUFFICIENT_BALANCE') {
          throw new Error('余额不足,无法完成付款');
        }
        throw new Error('数据无效:' + errorData?.message);
      }

      if (error.response?.status === 422 || error.response?.status === 429) {
        // 密钥出金保护机制:参见 /zh/docs/guides/pix-cash-out-protections
        const retryAfter = error.response.headers['retry-after'];
        throw new Error(`${errorData?.code}:请在 ${retryAfter ?? '?'} 秒后重试`);
      }

      throw new Error(errorData?.message || 'PIX 付款出错');
    }
    throw error;
  }
}

// 用法 - 通过 CPF 付款
sendPixPayment(
  'your_token_here',
  '12345678901',
  'DOCUMENT',
  'Ana Costa',
  '12345678901',
  250.50,
  '供应商付款'
);

// 用法 - 通过邮箱付款
sendPixPayment(
  'your_token_here',
  'ana.costa@email.com',
  'EMAIL',
  'Ana Costa',
  '12345678901',
  100.00,
  '退款'
);

// 用法 - 通过电话付款(区号 + 号码,不含国家代码 +55)
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:
    """
    发送 PIX 付款

    Args:
        token: 有效的 Bearer token
        recipient_key: 收款方的 PIX 密钥
        recipient_key_type: 密钥类型(DOCUMENT、EMAIL、PHONE、RANDOM)
        recipient_name: 收款方姓名
        recipient_document: 收款方的 CPF 或 CNPJ
        amount: 金额(雷亚尔)
        description: 付款描述(可选)

    Returns:
        已发起付款的数据
    """
    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'PIX 付款 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 付款已成功发起!')
        print(f"交易 ID:{data['transactionId']}")
        print(f"状态:{data['status']}")
        print(f"金额:R$ {amount:.2f}")
        print(f"收款方:{recipient_name}")

        return data

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

        # 处理特定错误
        if e.response.status_code == 400:
            if error_data.get('code') == 'PIX_INSUFFICIENT_BALANCE':
                raise Exception('余额不足,无法完成付款')
            raise Exception(f"数据无效:{error_data.get('message')}")

        raise Exception(f"付款出错:{error_data.get('message', str(e))}")

# 用法
token = 'your_token_here'

# 通过 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='供应商付款'
)

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 ?? "PIX 付款 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('余额不足,无法完成付款');
        }

        throw new Exception("付款出错:$errorMessage");
    }

    $data = json_decode($response, true);

    echo "PIX 付款已成功发起!" . PHP_EOL;
    echo "交易 ID:{$data['transactionId']}" . PHP_EOL;
    echo "状态:{$data['status']}" . PHP_EOL;
    echo "金额:R$ " . number_format($amount, 2, ',', '.') . PHP_EOL;
    echo "收款方:$recipientName" . PHP_EOL;

    return $data;
}

// 用法
$token = 'your_token_here';
$payment = sendPixPayment(
    $token,
    '12345678901',
    'DOCUMENT',
    'Ana Costa',
    '12345678901',
    250.50,
    '供应商付款'
);

使用场景

1. 工资发放

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

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

    for (const employee of employees) {
      try {
        // 每笔付款前检查余额
        const balance = await getBalance(this.token);

        if (balance.netBalance < employee.salary) {
          throw new Error('余额不足');
        }

        // 执行付款
        const payment = await sendPixPayment(
          this.token,
          employee.pixKey,
          employee.pixKeyType,
          employee.fullName,
          employee.document,
          employee.salary,
          `工资 ${new Date().toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })}`
        );

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

        // 每笔付款之间等待 1 秒
        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));
  }
}

// 用法
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
  },
  // ... 更多员工
];

const results = await payroll.processPayroll(employees);
console.log(`成功付款:${results.successful.length}`);
console.log(`失败付款:${results.failed.length}`);

2. 电商平台 - 向卖家分账

class MarketplacePayouts:
    """处理电商平台向卖家的分账"""

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

    def process_seller_payouts(self, sales_data: list) -> dict:
        """根据销售额处理分账"""
        results = {'successful': [], 'failed': []}

        # 按卖家汇总销售额
        seller_totals = self.group_sales_by_seller(sales_data)

        for seller_id, total_amount in seller_totals.items():
            try:
                # 获取卖家数据
                seller = self.get_seller_data(seller_id)

                # 计算扣除佣金后的金额
                commission = total_amount * 0.10  # 10% 佣金
                payout_amount = total_amount - commission

                # 执行付款
                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'销售分账 - {len(sales_data)} 笔交易'
                )

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

                # 在数据库中记录分账
                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:
        """按卖家汇总销售额"""
        totals = {}
        for sale in sales_data:
            seller_id = sale['seller_id']
            totals[seller_id] = totals.get(seller_id, 0) + sale['amount']
        return totals

3. 退款系统

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

  async processRefund(orderId, refundReason) {
    // 获取订单数据
    const order = await this.getOrderData(orderId);

    // 验证是否允许退款
    if (!this.canRefund(order)) {
      throw new Error('该订单不允许退款');
    }

    // 将款项付回给客户
    const refund = await sendPixPayment(
      this.token,
      order.customer.pixKey,
      order.customer.pixKeyType,
      order.customer.name,
      order.customer.document,
      order.amount,
      `退款 - 订单 ${orderId} - ${refundReason}`
    );

    // 更新订单状态
    await this.updateOrderStatus(orderId, 'REFUNDED', refund.transactionId);

    // 向客户发送通知
    await this.notifyCustomer(order.customer.email, refund);

    return refund;
  }

  canRefund(order) {
    // 检查订单是否已付款且仍在期限内
    const daysSincePurchase = (Date.now() - new Date(order.paidAt)) / (1000 * 60 * 60 * 24);
    return order.status === 'PAID' && daysSincePurchase <= 7;
  }
}

PIX 密钥格式

下表详细说明每种密钥类型接受的确切格式。details.key 字段会根据所提供的 details.keyType 进行校验 — 如果格式不符,API 返回 400 错误。

keyTypedetails.key 的预期格式校验正则有效示例无效示例
DOCUMENTCPF:11 位数字^\d{11}$(含校验位验证)12345678901123.456.789-01(含标点)、1234567890(10 位)
DOCUMENTCNPJ:14 位数字^\d{14}$(含校验位验证)1234567800019512.345.678/0001-95(含标点)
EMAIL有效的邮箱地址^[^\s@]+@[^\s@]+\.[^\s@]{2,}$usuario@exemplo.comusuario@(无域名)
PHONE区号 + 号码(10 或 11 位)^\d{10,11}$11999999999(手机)、1133334444(座机)5511999999999(含国家代码)、+5511999999999(含 + 国家代码)
RANDOMUUID(带或不带连字符)^[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}$ 或 ^[a-fA-F0-9]{32}$a1b2c3d4-e5f6-4890-abcd-ef1234567890a1b2c3d4(不完整)

PHONE 密钥 — 不要包含国家代码(+55)。 只发送区号 + 号码(10 或 11 位)。API 在发送给清算银行之前会自动添加国家代码 +55。

DOCUMENT 密钥 — 只发送数字。 CPF 和 CNPJ 不应包含标点(点、斜杠或连字符)。除了位数之外,API 还会校验 CPF/CNPJ 的校验位。

keyType 自动检测

如果您希望根据密钥的值实现 keyType 的自动检测,请使用以下优先级逻辑:

  1. 包含 @ → EMAIL
  2. UUID v4 格式 → RANDOM
  3. 14 位数字 + 有效 CNPJ(校验位) → DOCUMENT
  4. 11 位数字 + 有效 CPF(校验位) → DOCUMENT
  5. 10-11 位数字 + 不是有效 CPF → PHONE
function inferPixKeyType(key: string): 'DOCUMENT' | 'EMAIL' | 'PHONE' | 'RANDOM' | null {
  const trimmed = key.trim();

  // 1. UUID(随机密钥 / 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. 邮箱
  if (trimmed.includes('@')) return 'EMAIL';

  // 3. 仅数字 → DOCUMENT 或 PHONE
  const digits = trimmed.replace(/\D/g, '');

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

  // 11 位 → CPF 或 PHONE
  // 通过校验 CPF 的校验位来区分
  if (digits.length === 11) {
    return isValidCpf(digits) ? 'DOCUMENT' : 'PHONE';
  }

  // 10 位 → 座机(区号 + 8 位)
  if (digits.length === 10) return 'PHONE';

  return null; // 无法识别的格式
}

// CPF 算法校验(校验位)
function isValidCpf(cpf: string): boolean {
  if (/^(\d)\1{10}$/.test(cpf)) return false; // 拒绝 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 与电话的歧义: 11 位的值既可能是 CPF,也可能是手机号(区号 + 9 位)。上面的逻辑使用 CPF 校验位验证来区分:校验位匹配则为 CPF,否则为电话。即便如此,我们仍建议让最终用户在界面中明确选择密钥类型,以避免边界情况下的错误。

PIX 密钥验证

发送付款之前,请在本地验证 PIX 密钥的格式:

function validatePixKey(key: string, keyType: string): boolean {
  switch (keyType) {
    case 'DOCUMENT':
      // CPF:11 位或 CNPJ:14 位(仅数字)
      return /^\d{11}$/.test(key) || /^\d{14}$/.test(key);

    case 'EMAIL':
      // 带有效域名的邮箱(顶级域名至少 2 个字符)
      return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(key);

    case 'PHONE':
      // 区号 + 号码:10 位(座机)或 11 位(手机)
      // 不要包含国家代码(+55)— API 会自动添加
      return /^\d{10,11}$/.test(key);

    case 'RANDOM':
      // 带连字符的 UUID 或不带连字符的 32 位十六进制字符
      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;
  }
}

余额验证

在发起付款前检查余额,以避免 400 PIX_INSUFFICIENT_BALANCE 错误。

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

  // 检查余额是否充足
  if (balance.netBalance < amount) {
    throw new Error(
      `余额不足。可用:R$ ${balance.netBalance.toFixed(2)} | ` +
      `所需:R$ ${amount.toFixed(2)}`
    );
  }

  // 继续付款
  return await sendPixPayment(token, ...recipient, amount);
}

状态监控

要跟踪付款确认情况,请查询 GET /api/pix/transaction/\{id\}(接受数字 transactionId 或您的 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('付款已确认!');
      return true;
    }

    if (status === 'ERROR') {
      throw new Error('付款失败');
    }

    // 等待 2 秒后再次检查
    await new Promise(resolve => setTimeout(resolve, 2000));
  }

  throw new Error('超时:付款未在预期时间内确认');
}

交易状态流程

创建 PIX Cash-Out 后,交易会经历一个异步流程:

POST /api/pix/cash-out
         │
         ▼
     ┌─────────┐
     │ PENDING  │  ← API 立即响应(201)
     └────┬─────┘
          │  (在清算银行处理)
          │
    ┌─────┴──────┐
    ▼            ▼
┌──────────┐ ┌───────┐
│CONFIRMED │ │ ERROR │  ← 通过 webhook 通知
└──────────┘ └───────┘
  1. API 立即返回 status: "PENDING"(201 Created)
  2. 付款被发送到清算银行进行处理
  3. 最终结果(CONFIRMED 或 ERROR)通过 Cash-Out webhook 通知
  4. 或者,使用 轮询端点 查询状态

大多数 PIX 付款会在几秒内确认。我们建议将接收 webhook 作为主要的确认方式,轮询仅作为备用方案。

视您账户的清算机构而定,对提供商已告知不存在的密钥,或提供商封锁查询期间的出金,API 可能会立即响应(422 或 429,不创建交易,也不发送 webhook)。参见 Pix 密钥出金保护机制。

HTTP 响应状态码

状态码描述含义
201付款已发起PIX 转账已成功发起(状态 PENDING)
400数据无效缺少必填字段、格式无效、externalId 重复、余额不足或超出限额
401未授权Bearer token 未提供、已过期或无效
422密钥未找到PIX_KEY_NOT_FOUND:提供商告知该密钥不存在。当响应来自被否定密钥的记录时,响应体包含 details.cached: true 和 details.retryAfterSeconds。参见 Pix 密钥出金保护机制
429不存在的密钥过多TOO_MANY_PIX_KEYS_NOT_FOUND:因查询不存在的密钥过多,密钥出金已被暂时封锁。请等待 Retry-After 响应头指定的时间。参见 Pix 密钥出金保护机制
500内部错误处理过程中出现意外错误。重试前请先通过 externalId 查询

业务错误

除 HTTP 状态码外,API 还会在响应体中返回特定的错误码。部分错误以同步方式出现(在 API 响应中),另一些以异步方式出现(通过 webhook,当清算银行拒绝该操作时)。

同步错误(API 响应)

在 HTTP 响应中立即返回,状态码为 400(密钥出金保护机制中为 422 和 429):

错误码(code)消息原因
PIX_DUPLICATE_EXTERNAL_IDEsta transação já existe no sistema该 externalId 已在此账户中使用过(details.existingTransactionId 返回已存在的交易)
PIX_INSUFFICIENT_BALANCESaldo insuficiente para realizar esta operação可用余额小于金额 + 手续费
PIX_TRANSACTION_LIMIT_EXCEEDEDUltrapassa o limite máximo por transação金额超过配置的单笔交易限额
PIX_DAILY_LIMIT_EXCEEDEDVocê atingiu o limite diário de transações当日累计超过每日限额
PIX_NIGHT_LIMIT_EXCEEDEDO limite noturno é mais restrito夜间操作超过降低后的限额(20:00–6:00,巴西利亚时间)
PIX_LIMIT_EXCEEDEDUltrapassa o limite permitido超出账户的其他限额
验证错误按字段的消息金额小于 0.01、超过 2 位小数、密钥格式与 keyType 不符、证件号无效等
PIX_KEY_NOT_FOUNDChave Pix não encontrada状态 422。带 details.cached: true 时为保存的结果:请遵守 details.retryAfterSeconds。不带 cached 时来自本次调用中的提供商。参见 Pix 密钥出金保护机制
TOO_MANY_PIX_KEYS_NOT_FOUNDMuitas tentativas com chaves Pix inexistentes. Saque por chave bloqueado temporariamente; tente novamente em instantes.状态 429:因查询不存在的密钥过多,密钥出金已被暂时封锁。请等待 Retry-After 响应头指定的时间。参见 Pix 密钥出金保护机制

异步错误(通过 webhook)

当清算银行拒绝该操作时,交易状态变为 ERROR。errorCode 和 errorMessage 字段会在 Cash-Out webhook 的 payload 中发送。errorCode 的值因账户的清算机构而异,不应用于业务逻辑:请将其视为参考信息,并向用户展示 errorMessage。

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

部分清算机构返回的标准化错误码示例:

errorCodeerrorMessage原因可重试?
TAX_ID_MISMATCHO CPF/CNPJ informado não corresponde ao titular da conta.details.document 与 PIX 密钥所有者不符。在其他核验持有人的清算机构中,不一致会以该清算机构自己的代码和消息返回否
INVALID_TAX_IDCPF ou CNPJ inválido.提交的证件号未通过银行校验否
BLOCKED_ACCOUNTA conta de destino está bloqueada e não pode receber transferências.目标账户被司法或行政冻结否
ACCOUNT_CLOSEDA conta de destino está encerrada.目标账户已注销否
ORDER_REJECTEDA transação foi rejeitada pelo banco destinatário.目标银行拒绝了该操作否
PAYMENT_EXPIREDA transação expirou antes de ser processada.超出处理时间是
SETTLEMENT_TIMEOUTO banco destinatário não respondeu a tempo.与目标银行通信超时是

对于可重试的错误(PAYMENT_EXPIRED、SETTLEMENT_TIMEOUT),使用新的 externalId 重新发送交易是安全的。对于网络故障(无响应),请参阅下方的「安全重试」— 这些情况下请复用同一个 externalId。

发生异步错误时,交易会被自动取消(账本回滚),余额会被退回。无需采取任何额外操作 — 只需将失败原因通知最终用户即可。

请参阅 API 参考 获取响应字段的完整详情。

最佳实践

安全重试

重要说明

  • 最低金额: R$ 0,01 | 小数位: 最多 2 位
  • externalId: 最多 50 个字符,每个账户内唯一(幂等性)
  • description: 最多 140 个字符(可选)
  • details.name: 可选,最多 100 个字符 — 仅作信息用途,银行不会校验
  • details.document: 在 API 中为可选,建议所有密钥类型都提供 — API 仅校验格式;与密钥持有人的比对取决于账户的清算机构
  • PHONE 密钥: 只发送区号 + 号码(10-11 位)。API 会自动添加 +55

后续步骤

本页目录