PIX 二维码 Cash-Out
概述
PIX 二维码 Cash-Out 端点允许您根据扫描或复制(复制粘贴)的二维码发起 PIX 付款。二维码必须遵循巴西中央银行的 EMV 标准。收款方数据会自动从二维码中提取,从而简化付款流程。
此端点需要有效的 Bearer token。详情请参阅认证文档。
PIX 密钥 vs 二维码:该使用哪个端点?
FyPay API 提供两个用于发送 PIX 付款的端点。请根据您的使用场景选择最合适的一个:
| 标准 | PIX 密钥 Cash-Out | 二维码 Cash-Out |
|---|---|---|
| 端点 | POST /api/pix/cash-out | POST /api/pix/cash-out-qrcode |
| 何时使用 | 您知道收款方的 PIX 密钥 | 您拥有收款方生成的二维码 |
| 收款方数据 | 密钥和类型必填;建议提供姓名和证件号 | 内嵌在二维码中(请求中可选) |
| 金额验证 | 仅余额和限额 | 余额、限额 + 二维码金额与所填金额的比对 |
| 密钥类型 | CPF、CNPJ、邮箱、电话、随机密钥 | 不适用(信息在二维码内) |
| 确认 webhook | CashOut 事件 | 同样的 CashOut 事件 |
| 响应 | 相同结构 | 相同结构 |
- 使用密钥 Cash-Out:当您的应用已拥有收款方数据时(例如工资发放、程序化分账)
- 使用二维码 Cash-Out:当付款由扫描的二维码发起时(例如 POS、账单支付、复制粘贴)
两个端点返回相同的响应结构,确认后触发相同的 CashOut webhook。
功能特性
- 通过静态或动态二维码付款
- 自动验证二维码中内嵌的金额
- 发送前自动检查余额
- 通过
externalId唯一标识(幂等性) - 自动计算手续费
端点
POST /api/pix/cash-out-qrcode
根据二维码发起一笔 PIX 付款。
必需请求头
Authorization: Bearer {token}
Content-Type: application/json请求体
{
"value": 15.50,
"qrCode": "00020126580014br.gov.bcb.pix0136a1b2c3d4-e5f6-7890-abcd-ef1234567890520400005303986540515.505802BR5925DESTINATARIO LTDA6009SAO PAULO62070503***6304ABCD",
"externalId": "QRPAY-987654-20240119",
"description": "通过二维码向供应商 XYZ 付款",
"name": "Destinatario Ltda",
"document": "12345678000190"
}请求
curl -X POST https://api.fypay.me/api/pix/cash-out-qrcode \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"value": 15.50,
"qrCode": "00020126580014br.gov.bcb.pix0136a1b2c3d4-e5f6-7890-abcd-ef1234567890520400005303986540515.505802BR5925DESTINATARIO LTDA6009SAO PAULO62070503***6304ABCD",
"externalId": "QRPAY-987654-20240119",
"description": "通过二维码向供应商 XYZ 付款",
"name": "Destinatario Ltda",
"document": "12345678000190"
}'响应 (201 Created)
{
"transactionId": "456",
"externalId": "QRPAY-987654-20240119",
"status": "PENDING",
"generateTime": "2024-01-19T14:30:00.000Z"
}请求参数
valuenumberobrigatorio付款金额,单位为巴西雷亚尔(BRL)。最多 2 位小数。如果二维码包含内嵌金额,所填金额必须与之相符(容差 1 分)。
最小值: 0.01
示例: 15.50
qrCodestringobrigatorioPIX 二维码的内容(EMV 字符串)。可通过相机扫描或复制粘贴字段获取。
最小长度: 50 个字符
最大长度: 500 个字符
格式: 必须以 000201 开头(中央银行 EMV PIX 标准)
示例: "00020126580014br.gov.bcb.pix0136a1b2c3d4-e5f6-7890-abcd-ef1234567890520400005303986540515.505802BR5925DESTINATARIO LTDA6009SAO PAULO62070503***6304ABCD"
externalIdstringobrigatorio交易的外部唯一标识符(按账户)。保证幂等性 — 两次发送同一个 externalId 会返回 409 错误,code 为 PIX_DUPLICATE_EXTERNAL_ID。
最大长度: 36 个字符
建议: 使用能保证唯一性的格式
示例: "QRPAY-987654-20240119"
descriptionstring将显示在收款方对账单中的付款描述。
最大长度: 140 个字符
默认值: 空
示例: "通过二维码向供应商 XYZ 付款"
namestring收款方姓名。可选,仅作信息用途 — 省略时使用二维码中的数据。
最大长度: 100 个字符
示例: "Destinatario Ltda"
documentstring收款方的 CPF 或 CNPJ(仅数字,校验位须有效)。可选 — 省略时使用二维码中的数据。
CPF: 11 位
CNPJ: 14 位
示例: "12345678000190"
响应结构
transactionIdstringsempre presenteFyPay 生成的交易内部 ID。
示例: "456"
externalIdstringsempre presente请求中提供的外部 ID(与输入值相同)。
示例: "QRPAY-987654-20240119"
statusstringsempre presente交易的当前状态。
可能的值:
PENDING:付款处理中CONFIRMED:付款已确认并完成ERROR:处理出错
示例: "PENDING"
注意: 大多数 PIX 付款会在几秒内确认
generateTimestringsempre presente付款创建的日期和时间(ISO 8601 UTC)。
示例: "2024-01-19T14:30:00.000Z"
实现示例
Node.js / TypeScript
import axios from 'axios';
interface CashOutQrCodeRequest {
value: number;
qrCode: string;
externalId: string;
description?: string;
name?: string;
document?: string;
}
interface CashOutQrCodeResponse {
transactionId: string;
externalId: string;
status: 'PENDING' | 'CONFIRMED' | 'ERROR';
generateTime: string;
}
async function payWithQrCode(
token: string,
qrCode: string,
amount: number,
description?: string
): Promise<CashOutQrCodeResponse> {
const payload: CashOutQrCodeRequest = {
value: amount,
qrCode,
externalId: `QRPAY-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
description: description || '通过 PIX 二维码付款'
};
try {
const response = await axios.post<CashOutQrCodeResponse>(
'https://api.fypay.me/api/pix/cash-out-qrcode',
payload,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log('二维码付款已发起!');
console.log(`交易 ID:${response.data.transactionId}`);
console.log(`状态:${response.data.status}`);
console.log(`金额:R$ ${amount.toFixed(2)}`);
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 === 'INVALID_QR_CODE') {
throw new Error('二维码无效或格式错误');
}
if (errorData?.code === 'QR_CODE_VALUE_MISMATCH') {
throw new Error('所填金额与二维码中的金额不符');
}
if (errorData?.code === 'PIX_INSUFFICIENT_BALANCE') {
throw new Error('余额不足,无法完成付款');
}
throw new Error('数据无效:' + errorData?.message);
}
if (error.response?.status === 409) {
throw new Error('externalId 已在另一笔交易中使用');
}
throw new Error(errorData?.message || '二维码付款出错');
}
throw error;
}
}
// 用法 - 通过扫描的二维码付款
const qrCodeContent = '00020126580014br.gov.bcb.pix0136a1b2c3d4...6304ABCD';
payWithQrCode('your_token_here', qrCodeContent, 15.50, '供应商付款');Python
import requests
from datetime import datetime
from typing import Dict, Optional
import uuid
def pay_with_qr_code(
token: str,
qr_code: str,
amount: float,
description: Optional[str] = None
) -> Dict:
"""
通过二维码发送 PIX 付款
Args:
token: 有效的 Bearer token
qr_code: PIX 二维码内容(EMV 字符串)
amount: 金额(雷亚尔)
description: 付款描述(可选)
Returns:
已发起付款的数据
"""
url = 'https://api.fypay.me/api/pix/cash-out-qrcode'
payload = {
'value': round(amount, 2),
'qrCode': qr_code,
'externalId': f'QRPAY-{int(datetime.now().timestamp())}-{uuid.uuid4().hex[:8]}',
'description': description or '通过 PIX 二维码付款'
}
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('二维码付款已发起!')
print(f"交易 ID:{data['transactionId']}")
print(f"状态:{data['status']}")
print(f"金额:R$ {amount:.2f}")
return data
except requests.exceptions.HTTPError as e:
error_data = e.response.json() if e.response else {}
if e.response.status_code == 400:
code = error_data.get('code', '')
if code == 'INVALID_QR_CODE':
raise Exception('二维码无效或格式错误')
if code == 'QR_CODE_VALUE_MISMATCH':
raise Exception('所填金额与二维码中的金额不符')
if code == 'PIX_INSUFFICIENT_BALANCE':
raise Exception('余额不足,无法完成付款')
raise Exception(f"数据无效:{error_data.get('message')}")
if e.response.status_code == 409:
raise Exception('externalId 已在另一笔交易中使用')
raise Exception(f"付款出错:{error_data.get('message', str(e))}")
# 用法
token = 'your_token_here'
qr_code = '00020126580014br.gov.bcb.pix0136a1b2c3d4...6304ABCD'
payment = pay_with_qr_code(
token=token,
qr_code=qr_code,
amount=15.50,
description='通过二维码向供应商 XYZ 付款'
)PHP
<?php
function payWithQrCode(
string $token,
string $qrCode,
float $amount,
?string $description = null
): array {
$url = 'https://api.fypay.me/api/pix/cash-out-qrcode';
$payload = [
'value' => round($amount, 2),
'qrCode' => $qrCode,
'externalId' => 'QRPAY-' . time() . '-' . bin2hex(random_bytes(4)),
'description' => $description ?? '通过 PIX 二维码付款'
];
$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);
$errorCode = $errorData['code'] ?? '';
$errorMessage = $errorData['message'] ?? "HTTP $httpCode";
if ($httpCode === 400) {
if ($errorCode === 'INVALID_QR_CODE') {
throw new Exception('二维码无效或格式错误');
}
if ($errorCode === 'QR_CODE_VALUE_MISMATCH') {
throw new Exception('所填金额与二维码中的金额不符');
}
if ($errorCode === 'PIX_INSUFFICIENT_BALANCE') {
throw new Exception('余额不足,无法完成付款');
}
}
if ($httpCode === 409) {
throw new Exception('externalId 已在另一笔交易中使用');
}
throw new Exception("付款出错:$errorMessage");
}
$data = json_decode($response, true);
echo "二维码付款已发起!" . PHP_EOL;
echo "交易 ID:{$data['transactionId']}" . PHP_EOL;
echo "状态:{$data['status']}" . PHP_EOL;
echo "金额:R$ " . number_format($amount, 2, ',', '.') . PHP_EOL;
return $data;
}
// 用法
$token = 'your_token_here';
$qrCode = '00020126580014br.gov.bcb.pix0136a1b2c3d4...6304ABCD';
$payment = payWithQrCode($token, $qrCode, 15.50, '向供应商 XYZ 付款');使用场景
1. POS — 在收银台通过二维码付款
class PointOfSalePayment {
constructor(private token: string) {}
async payFromScannedQrCode(qrCodeContent: string, amount: number) {
// 发送前在本地验证二维码
if (!this.isValidPixQrCode(qrCodeContent)) {
throw new Error('二维码无效,请检查后重试。');
}
const payment = await payWithQrCode(
this.token,
qrCodeContent,
amount,
`POS 付款 - ${new Date().toLocaleDateString('pt-BR')}`
);
console.log(`付款已发起:${payment.transactionId}`);
return payment;
}
private isValidPixQrCode(qrCode: string): boolean {
return qrCode.length >= 50 && qrCode.startsWith('000201');
}
}2. 通过二维码向供应商付款
class SupplierPayment:
"""通过二维码处理向供应商的付款"""
def __init__(self, token: str):
self.token = token
def pay_supplier_invoice(self, qr_code: str, invoice_amount: float, invoice_id: str):
"""通过二维码支付供应商账单"""
# 先检查余额
balance = get_balance(self.token)
if balance['netBalance'] < invoice_amount:
raise Exception(f"余额不足。可用:R$ {balance['netBalance']:.2f}")
payment = pay_with_qr_code(
token=self.token,
qr_code=qr_code,
amount=invoice_amount,
description=f'支付账单 #{invoice_id}'
)
return {
'invoice_id': invoice_id,
'transaction_id': payment['transactionId'],
'status': payment['status'],
'amount': invoice_amount
}3. 周期性付款自动化
class RecurringQrCodePayment {
constructor(token) {
this.token = token;
}
async processPaymentBatch(payments) {
const results = { successful: [], failed: [] };
for (const payment of payments) {
try {
const result = await payWithQrCode(
this.token,
payment.qrCode,
payment.amount,
payment.description
);
results.successful.push({
reference: payment.reference,
transactionId: result.transactionId,
amount: payment.amount
});
// 付款之间等待
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
results.failed.push({
reference: payment.reference,
error: error.message
});
}
}
return results;
}
}二维码验证
PIX 二维码遵循巴西中央银行定义的 EMV(Europay、Mastercard、Visa) 标准。在发送给 API 之前,您可以在本地进行验证:
function isValidPixQrCode(qrCode: string): boolean {
// 检查最小和最大长度
if (qrCode.length < 50 || qrCode.length > 500) {
return false;
}
// 检查必需的 EMV 前缀
if (!qrCode.startsWith('000201')) {
return false;
}
return true;
}EMV PIX 二维码结构:
000201— Payload Format Indicator(必需)0102XX— Point of Initiation Method(11= 静态,12= 动态)- 包含收款方数据、金额、城市等信息的字段
6304XXXX— CRC16(校验和)
二维码的完整验证(EMV 解码、CRC 校验和数据提取)由 API 自动完成。本地验证仅用于过滤明显无效的二维码。
响应状态码
| 状态码 | 错误 | 描述 |
|---|---|---|
201 | — | 二维码 PIX 付款已成功发起 |
400 | INVALID_QR_CODE | 二维码无效或格式错误 |
400 | QR_CODE_VALUE_MISMATCH | 所填金额与二维码内嵌金额不符 |
400 | PIX_INSUFFICIENT_BALANCE | 余额不足,无法完成交易 |
400 | PIX_TRANSACTION_LIMIT_EXCEEDED、PIX_DAILY_LIMIT_EXCEEDED、PIX_NIGHT_LIMIT_EXCEEDED、PIX_LIMIT_EXCEEDED | 超出账户限额 |
400 | PUB_REQUEST_BODY_INVALID | 字段缺失或格式不符(例如 externalId 超过 36 个字符,或 qrCode 不以 000201 开头);details 返回每个字段的错误 |
401 | — | Token 未提供、已过期或无效 |
409 | PIX_DUPLICATE_EXTERNAL_ID | externalId 已在另一笔交易中使用 |
500 | — | 处理过程中出现意外错误。重试前请先通过 externalId 查询 |
请参阅 API 参考 获取响应字段的完整详情。
最佳实践
重要说明
- 最低金额: R$ 0,01 | 小数位: 最多 2 位
externalId: 最多 36 个字符,每个账户内唯一description: 最多 140 个字符- 二维码格式: 必须以
000201开头,长度在 50 到 500 个字符之间 - 动态二维码: 接受不含内嵌金额的二维码 — 由
value字段确定付款金额 - 带金额的静态二维码:
value中所填金额必须与二维码内嵌金额相符(容差 1 分)