docs
集成指南

PIX 二维码 Cash-Out

概述

PIX 二维码 Cash-Out 端点允许您根据扫描或复制(复制粘贴)的二维码发起 PIX 付款。二维码必须遵循巴西中央银行的 EMV 标准。收款方数据会自动从二维码中提取,从而简化付款流程。

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

PIX 密钥 vs 二维码:该使用哪个端点?

FyPay API 提供两个用于发送 PIX 付款的端点。请根据您的使用场景选择最合适的一个:

标准PIX 密钥 Cash-Out二维码 Cash-Out
端点POST /api/pix/cash-outPOST /api/pix/cash-out-qrcode
何时使用您知道收款方的 PIX 密钥您拥有收款方生成的二维码
收款方数据密钥和类型必填;建议提供姓名和证件号内嵌在二维码中(请求中可选)
金额验证仅余额和限额余额、限额 + 二维码金额与所填金额的比对
密钥类型CPF、CNPJ、邮箱、电话、随机密钥不适用(信息在二维码内)
确认 webhookCashOut 事件同样的 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

qrCodestringobrigatorio

PIX 二维码的内容(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 presente

FyPay 生成的交易内部 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 付款已成功发起
400INVALID_QR_CODE二维码无效或格式错误
400QR_CODE_VALUE_MISMATCH所填金额与二维码内嵌金额不符
400PIX_INSUFFICIENT_BALANCE余额不足,无法完成交易
400PIX_TRANSACTION_LIMIT_EXCEEDED、PIX_DAILY_LIMIT_EXCEEDED、PIX_NIGHT_LIMIT_EXCEEDED、PIX_LIMIT_EXCEEDED超出账户限额
400PUB_REQUEST_BODY_INVALID字段缺失或格式不符(例如 externalId 超过 36 个字符,或 qrCode 不以 000201 开头);details 返回每个字段的错误
401—Token 未提供、已过期或无效
409PIX_DUPLICATE_EXTERNAL_IDexternalId 已在另一笔交易中使用
500—处理过程中出现意外错误。重试前请先通过 externalId 查询

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

最佳实践

重要说明

  • 最低金额: R$ 0,01 | 小数位: 最多 2 位
  • externalId: 最多 36 个字符,每个账户内唯一
  • description: 最多 140 个字符
  • 二维码格式: 必须以 000201 开头,长度在 50 到 500 个字符之间
  • 动态二维码: 接受不含内嵌金额的二维码 — 由 value 字段确定付款金额
  • 带金额的静态二维码: value 中所填金额必须与二维码内嵌金额相符(容差 1 分)

后续步骤

本页目录