> ## Documentation Index
> Fetch the complete documentation index at: https://docs.billbuddyhq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 上传 TXT 批量交易文件

> 通过 multipart/form-data 上传固定宽度 TXT 文件，异步处理银行账户或支付 Token 交易。使用 OAuth2 Bearer Token，无需请求签名，也无需提交客户标识。不要手工设置 Content-Type 的 boundary，由上传工具自动生成。

### 文件要求

文件必须非空且不超过 10 MiB（10,485,760 字节）。文件名连同扩展名不超过 50 字节，主文件名只允许字母、数字和下划线；扩展名必须为 .txt（大小写不敏感）。内容仅允许可打印 ASCII 字符（0x20–0x7E），不允许 BOM、制表符、中文或其他多字节字符。每条记录恰好 120 字节，不含换行；所有记录（包括最后一条）以 CRLF（\r\n）结尾。不得删除行尾填充空格。

顺序必须为一个 H 文件头、至少一条 D 明细、一个 E 文件尾，E 后不得追加空行或其他内容。D 逻辑行号从 000001 连续递增，E 的笔数和本金合计必须与全部 D 记录一致。下表所有位置均从 1 开始，未使用区域按表填空格。

### 文件头 H

| 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |
| --- | --- | --- | --- |
| 1 | 1 | H | 固定记录类型。 |
| 2–17 | 16 | BILLBUDDYPAYMENT | 固定标识。 |
| 18 | 1 | 空格 | 固定填充。 |
| 19–23 | 5 | 账单方代码 | 五位字母或数字，如 ABC12；实际接入值请向 BillBuddy 确认。 |
| 24–30 | 7 | 空格 | 固定填充。 |
| 31–40 | 10 | file_identifier | 恰好十位字母、数字或下划线，如 PAY2030001。 |
| 41 | 1 | 空格 | 固定填充。 |
| 42–47 | 6 | 文件生成日期 | 有效 ddmmyy，年份 00–99 表示 2000–2099；如 010130 表示 2030-01-01。 |
| 48 | 1 | 预留位 | 空格或 T，t 也可接受；建议填空格。此位不是测试开关，不会切换为模拟支付。 |
| 49–120 | 72 | 空格 | 预留区域，填空格。 |

### 明细 D：公共字段

| 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |
| --- | --- | --- | --- |
| 1 | 1 | D | 固定记录类型。 |
| 2–17 | 16 | 业务参考号 | 非空，最长 16 字符，左对齐、右补空格。 |
| 18 | 1 | 空格 | 固定填充。 |
| 19–28 | 10 | 本金金额 | 十位数字，左补零，单位为分。有效范围 1–999999（AUD 0.01–9999.99），如 0000002550 表示 AUD 25.50。 |
| 29–30 | 2 | 空格 | 固定填充。 |
| 31–32 | 2 | 支付类型 | BC：银行账户；CT：支付 Token。一个文件可混合两种类型。 |
| 115–120 | 6 | 逻辑行号 | 六位数字，从 000001 开始连续递增，仅计算 D 记录。 |

### 明细 D：BC 专用字段

| 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |
| --- | --- | --- | --- |
| 33 | 1 | 空格 | 固定填充。 |
| 34–40 | 7 | BSB | 格式为三个数字、连字符、三个数字，如 123-456。 |
| 41 | 1 | 空格 | 固定填充。 |
| 42–50 | 9 | 银行账号 | 非空，数字账号最长 9 位，右补空格；前导零保留。账号区域内的空格及连字符会被忽略，建议直接传数字。 |
| 51–66 | 16 | 空格 | 必须全部为空格。 |
| 67–98 | 32 | 账户名 | 非空，最长 32 字符，左对齐、右补空格。 |
| 99 | 1 | 空格 | 固定填充。 |
| 100–103 | 4 | 0000 | 固定四个零。 |
| 104–114 | 11 | 空格 | 预留区域，填空格。 |

### 明细 D：CT 专用字段

| 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |
| --- | --- | --- | --- |
| 33–82 | 50 | 空格 | 必须全部为空格。 |
| 83–114 | 32 | 支付 Token | 恰好 32 字符的有效支付 Token，不得为空或带右侧填充空格；使用获准测试环境提供的 Token。 |

### 文件尾 E

| 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |
| --- | --- | --- | --- |
| 1 | 1 | E | 固定记录类型。 |
| 2–18 | 17 | 空格 | 必须全部为空格。 |
| 19–28 | 10 | 本金总额 | 十位数字、左补零，单位为分，必须等于所有 D 记录本金之和，不含附加费。 |
| 29–99 | 71 | 空格 | 必须全部为空格。 |
| 100–111 | 12 | 000000000000 | 固定十二个零。 |
| 112–114 | 3 | 空格 | 必须全部为空格。 |
| 115–120 | 6 | 明细总笔数 | 六位数字、左补零，必须等于 D 记录总数，至少为 000001。 |

### 受理、查询与重复提交

code=0、status=received 仅表示文件已接收。文件结构在上传时校验，明细字段及支付 Token 随后逐行校验；部分行失败时，其余有效行仍可能执行。金额币种为 AUD；文件金额、amount_cents 和 declared_total_amount_cents 使用分，不含另行计算的附加费。

file_identifier 在同一商户的所有业务范围和上传入口之间必须唯一。相同标识重复上传相同内容或不同内容均返回 code=1。文件格式失败后，该标识也可能已被占用。请保存 batch_id 和 file_identifier，通过批次结果接口核对处理状态；不要因超时、重复上传错误或结果暂不可用而换一个标识重新提交同一批扣款。

后续查询请沿用上传请求中的 merchant_id 及同一商户的 OAuth 凭据，并使用返回的 batch_id 或文件头中的 file_identifier 定位批次。响应字段含义见下方定义。

### 示例文件

[下载 payments.txt](/examples/payments.txt)。文件包含一条 AUD 25.50 银行账户记录和一条 AUD 48.00 Token 记录，共两笔 AUD 73.50；保留了固定宽度空格及 CRLF。账单方代码、文件标识、账户、Token、参考号和日期均为虚构示例，调用前替换为获准测试数据，且重新计算尾部笔数和金额合计。



## OpenAPI

````yaml /openapi/billbuddy-v2-openapi.yaml post /api/v2/trans/bulk-upload
openapi: 3.0.3
info:
  title: BillBuddy API 接入文档
  version: 2.0.0
  description: >
    BillBuddy API
    接入文档，本文收录第三方服务端主动调用的认证、交易和订阅接口。请求与响应示例均使用完整但虚构的业务数据及离线生成的演示令牌，不能用于实际认证或交易。


    ## 接入步骤

    1. 向 BillBuddy 获取 Client ID、Client Secret、商户标识及业务标识。如启用 IP 白名单，请使用已获准的固定出口
    IP 调用。

    2. 通过 POST /oauth/token 获取 access_token。业务请求携带 Authorization: Bearer
    {access_token} 和 Accept: application/json。JSON 请求使用 Content-Type:
    application/json；获取令牌使用表单。

    3. 按需调用支付或订阅接口。客户通过 activation_url 打开托管激活页；Client Secret 仅可保存在商户服务端。


    ## 标识及金额

    商户身份由 OAuth 凭据确定，无需在请求中传 business_id。需要指定业务的接口使用 merchant_id；单笔交易查询和批次结果查询的
    merchant_id 可选，省略或为空时在当前商户全部业务范围内查询；交易列表使用可选的
    Mid（区分大小写），省略时查询当前商户的全部业务。业务标识必须属于凭据所属商户。响应中的标识请按各接口字段说明读取。

    除 TXT 批量上传文件及其 amount_cents/declared_total_amount_cents
    使用分外，金额使用主货币单位，最多两位小数，例如 10.50 表示 AUD 10.50。银行账号、卡号、月份、年份和邮编建议使用字符串，以保留前导零。


    ## 成功和失败

    业务 JSON 成功 code=0，常规失败 code=1。交易及订阅业务接口参数校验失败通常为 code=400、data={}，HTTP 状态仍为
    200。V2 业务接口认证失败为 code=1，plan/list 标识业务校验失败为 code=1。HTTP 429 表示请求超过频率限制。OAuth
    端点采用标准 OAuth 响应（HTTP 200/400/401/429）。

    code=0 表示接口处理成功，扣款结果须另行判断：批量 accepted 仅表示请求已受理，将异步处理；银行 Pending、预约 Pending 和
    3DS 待验证均不代表支付成功。交易、DDR 订阅和扣费计划各自使用不同的 status 枚举，请参阅对应字段说明。


    ## 日期及频率

    请求日期建议使用 YYYY-MM-DD。今天及明天均按服务业务时区 Australia/Sydney 计算（适用夏令时）；预约取消时间以
    Australia/Sydney 为准（适用夏令时），仅允许在处理日 08:15 之前取消，到达该时刻即不可取消。支付 complete_date
    与收银台 expires_at 使用 DD/MM/YYYY HH:mm:ss，其他时间格式见字段说明。

    RRULE 标准示例：Daily FREQ=DAILY;INTERVAL=1；Weekly
    FREQ=WEEKLY;INTERVAL=1;BYDAY=MO；Fortnightly
    FREQ=WEEKLY;INTERVAL=2;BYDAY=MO；Monthly
    FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=15；Quarterly
    FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=15；Half Yearly
    FREQ=MONTHLY;INTERVAL=6;BYMONTHDAY=15；Yearly
    FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=15。星期缩写为
    MO/TU/WE/TH/FR/SA/SU。Weekly/Monthly 省略具体扣费日时按起算日确定；单一 BYDAY=SA 或 SU
    按周一处理，未指定具体日期且起算日在周末时也按周一处理。单值 BYMONTHDAY=29/30/31 在短月按月末处理；BYMONTHDAY=-1
    表示月底。所有规则须符合有效 RRULE 格式。

    非标准频率（如每 10 天）须同时传 process_type=12、custom_cycle_type=1、custom_cycle=10，且
    day_of_debit=FREQ=DAILY;INTERVAL=10。单位 1/2/3/4 对应
    DAILY/WEEKLY/MONTHLY/YEARLY，以天为单位时间隔最小为 5，其余单位最小为 1，INTERVAL 须等于
    custom_cycle。季度、半年及年度还须提供 day_of_debit_dtstart；完整日期须严格晚于同段
    start_date，并在其后一个计费周期内。createSchedule 的实际首次扣费日期由 DDR 起算日、扣费频率及最早明天的限制共同确定。


    ## 使用示例

    参数类型、可空、长度、枚举及数值范围见 Schema；条件必填、日期及业务限制见字段 description。请结合两者使用。curl 示例中的
    BASE_URL、ACCESS_TOKEN、CLIENT_ID 和 CLIENT_SECRET 需由调用方设置；实际测试或生产域名由 BillBuddy
    提供。示例中的客户、订单、卡号、账户及日期须替换为测试环境允许使用且满足日期限制的数据。


    ## 托管订阅接入

    调用 POST /api/v2/subscription/create 创建订阅，取得 activation_url 后原样用于浏览器跳转或
    iframe 的 src；也可将 activation_html 用作 iframe 的 srcdoc。客户在 BillBuddy
    提供的页面填写支付信息并完成授权和激活。请使用返回的完整链接，无需自行拼接地址或解析链接中的凭证。Client Secret 和
    access_token 仅保存在商户服务端，不得放入 iframe 地址或页面内容。


    周期订阅及 By agreement 须在 start_date 当天之前激活；纯一次性订阅须在创建后 30
    天内提交激活。页面可以打开不保证仍可提交，以激活接口结果为准。


    ## 示例数据约定

    示例中的商户、客户、订单、支付 Token、会话链接和 OAuth 凭据均为虚构，不能用于真实调用。银行卡使用标准测试卡号；银行账户为虚构账户；邮箱使用
    example.com 保留域名，电话号码使用澳大利亚保留的演示号码。请求中的 BASE_URL、ACCESS_TOKEN、CLIENT_ID 和
    CLIENT_SECRET 仍需替换为获准测试环境的实际值。示例中必要的空字符串、null、空数组、0 和 false
    表示实际接口状态，不能统一替换为非空值。

    支付响应的 sign 按各支付场景的签名规则，使用公开演示密钥 bb_docs_demo_signing_key_2030 离线计算。3DS
    待验证响应的 html 不参与签名计算。该密钥仅用于文档，不能用于实际签名或认证。


    ## TXT 批量交易

    使用 multipart/form-data 上传固定宽度 TXT 文件。文件金额及
    amount_cents、declared_total_amount_cents 均以分计，不含附加费；文件格式和示例文件见“上传 TXT
    批量交易文件”。上传成功仅表示已接收，请保存 batch_id 和 file_identifier，并使用批次结果接口查询逐笔状态。批次
    completed 不代表所有支付成功。查询结果须使用同一商户的 OAuth 凭据；merchant_id
    可省略，如传入则须与原上传请求的业务标识一致。响应字段含义以对应接口说明为准。


    ## 开发语言请求示例

    请求示例提供 cURL、Python、JavaScript、PHP、Go、Java 和 Ruby，同一语言下可切换各业务场景。各语言使用与 cURL
    对应场景相同的请求参数和类型。


    运行环境：cURL；Python 3.9+（标准库）；JavaScript 使用 Node.js 18+，保存为 .mjs 后运行；PHP 8+ 并启用
    cURL 扩展；Go 1.20+；Java 11+（示例类名为 Example，保存为 Example.java）；Ruby 3+（标准库）。


    运行前设置 BASE_URL；业务接口还需设置 ACCESS_TOKEN，获取令牌需设置 CLIENT_ID 和
    CLIENT_SECRET。BASE_URL 使用 API 根地址，不附加 /api/v2。TXT 上传示例从当前目录读取
    payments.txt。请先替换为获准测试数据，并结合响应 code 和业务状态判断结果。
servers:
  - url: https://staging-api.billbuddyhq.com
    description: Staging 测试环境
security:
  - oauth2: []
tags:
  - name: 认证
    description: OAuth2 Client Credentials。
  - name: 交易
    description: 单笔、批量与预约取消。
  - name: 订阅
    description: 订阅模板、创建订阅、创建并激活 DDR 及推送扣费计划。
paths:
  /api/v2/trans/bulk-upload:
    post:
      tags:
        - 交易
      summary: 上传 TXT 批量交易文件
      description: >-
        通过 multipart/form-data 上传固定宽度 TXT 文件，异步处理银行账户或支付 Token 交易。使用 OAuth2
        Bearer Token，无需请求签名，也无需提交客户标识。不要手工设置 Content-Type 的 boundary，由上传工具自动生成。


        ### 文件要求


        文件必须非空且不超过 10 MiB（10,485,760 字节）。文件名连同扩展名不超过 50
        字节，主文件名只允许字母、数字和下划线；扩展名必须为 .txt（大小写不敏感）。内容仅允许可打印 ASCII 字符（0x20–0x7E），不允许
        BOM、制表符、中文或其他多字节字符。每条记录恰好 120 字节，不含换行；所有记录（包括最后一条）以
        CRLF（\r\n）结尾。不得删除行尾填充空格。


        顺序必须为一个 H 文件头、至少一条 D 明细、一个 E 文件尾，E 后不得追加空行或其他内容。D 逻辑行号从 000001 连续递增，E
        的笔数和本金合计必须与全部 D 记录一致。下表所有位置均从 1 开始，未使用区域按表填空格。


        ### 文件头 H


        | 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |

        | --- | --- | --- | --- |

        | 1 | 1 | H | 固定记录类型。 |

        | 2–17 | 16 | BILLBUDDYPAYMENT | 固定标识。 |

        | 18 | 1 | 空格 | 固定填充。 |

        | 19–23 | 5 | 账单方代码 | 五位字母或数字，如 ABC12；实际接入值请向 BillBuddy 确认。 |

        | 24–30 | 7 | 空格 | 固定填充。 |

        | 31–40 | 10 | file_identifier | 恰好十位字母、数字或下划线，如 PAY2030001。 |

        | 41 | 1 | 空格 | 固定填充。 |

        | 42–47 | 6 | 文件生成日期 | 有效 ddmmyy，年份 00–99 表示 2000–2099；如 010130 表示
        2030-01-01。 |

        | 48 | 1 | 预留位 | 空格或 T，t 也可接受；建议填空格。此位不是测试开关，不会切换为模拟支付。 |

        | 49–120 | 72 | 空格 | 预留区域，填空格。 |


        ### 明细 D：公共字段


        | 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |

        | --- | --- | --- | --- |

        | 1 | 1 | D | 固定记录类型。 |

        | 2–17 | 16 | 业务参考号 | 非空，最长 16 字符，左对齐、右补空格。 |

        | 18 | 1 | 空格 | 固定填充。 |

        | 19–28 | 10 | 本金金额 | 十位数字，左补零，单位为分。有效范围 1–999999（AUD 0.01–9999.99），如
        0000002550 表示 AUD 25.50。 |

        | 29–30 | 2 | 空格 | 固定填充。 |

        | 31–32 | 2 | 支付类型 | BC：银行账户；CT：支付 Token。一个文件可混合两种类型。 |

        | 115–120 | 6 | 逻辑行号 | 六位数字，从 000001 开始连续递增，仅计算 D 记录。 |


        ### 明细 D：BC 专用字段


        | 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |

        | --- | --- | --- | --- |

        | 33 | 1 | 空格 | 固定填充。 |

        | 34–40 | 7 | BSB | 格式为三个数字、连字符、三个数字，如 123-456。 |

        | 41 | 1 | 空格 | 固定填充。 |

        | 42–50 | 9 | 银行账号 | 非空，数字账号最长 9 位，右补空格；前导零保留。账号区域内的空格及连字符会被忽略，建议直接传数字。
        |

        | 51–66 | 16 | 空格 | 必须全部为空格。 |

        | 67–98 | 32 | 账户名 | 非空，最长 32 字符，左对齐、右补空格。 |

        | 99 | 1 | 空格 | 固定填充。 |

        | 100–103 | 4 | 0000 | 固定四个零。 |

        | 104–114 | 11 | 空格 | 预留区域，填空格。 |


        ### 明细 D：CT 专用字段


        | 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |

        | --- | --- | --- | --- |

        | 33–82 | 50 | 空格 | 必须全部为空格。 |

        | 83–114 | 32 | 支付 Token | 恰好 32 字符的有效支付 Token，不得为空或带右侧填充空格；使用获准测试环境提供的
        Token。 |


        ### 文件尾 E


        | 位置（从 1 开始） | 字节数 | 字段／固定值 | 格式及限制 |

        | --- | --- | --- | --- |

        | 1 | 1 | E | 固定记录类型。 |

        | 2–18 | 17 | 空格 | 必须全部为空格。 |

        | 19–28 | 10 | 本金总额 | 十位数字、左补零，单位为分，必须等于所有 D 记录本金之和，不含附加费。 |

        | 29–99 | 71 | 空格 | 必须全部为空格。 |

        | 100–111 | 12 | 000000000000 | 固定十二个零。 |

        | 112–114 | 3 | 空格 | 必须全部为空格。 |

        | 115–120 | 6 | 明细总笔数 | 六位数字、左补零，必须等于 D 记录总数，至少为 000001。 |


        ### 受理、查询与重复提交


        code=0、status=received 仅表示文件已接收。文件结构在上传时校验，明细字段及支付 Token
        随后逐行校验；部分行失败时，其余有效行仍可能执行。金额币种为 AUD；文件金额、amount_cents 和
        declared_total_amount_cents 使用分，不含另行计算的附加费。


        file_identifier 在同一商户的所有业务范围和上传入口之间必须唯一。相同标识重复上传相同内容或不同内容均返回
        code=1。文件格式失败后，该标识也可能已被占用。请保存 batch_id 和
        file_identifier，通过批次结果接口核对处理状态；不要因超时、重复上传错误或结果暂不可用而换一个标识重新提交同一批扣款。


        后续查询请沿用上传请求中的 merchant_id 及同一商户的 OAuth 凭据，并使用返回的 batch_id 或文件头中的
        file_identifier 定位批次。响应字段含义见下方定义。


        ### 示例文件


        [下载 payments.txt](/examples/payments.txt)。文件包含一条 AUD 25.50 银行账户记录和一条 AUD
        48.00 Token 记录，共两笔 AUD 73.50；保留了固定宽度空格及
        CRLF。账单方代码、文件标识、账户、Token、参考号和日期均为虚构示例，调用前替换为获准测试数据，且重新计算尾部笔数和金额合计。
      operationId: v2BulkUpload
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                merchant_id:
                  type: string
                  description: 业务标识，必须属于该商户。
                  minLength: 1
                  maxLength: 20
                  example: '48290713'
                file:
                  type: string
                  description: 非空 TXT 文件，最大 10 MiB；文件名、编码、固定宽度布局及换行要求见上方说明。
                  format: binary
              required:
                - merchant_id
                - file
              additionalProperties: false
      responses:
        '200':
          description: >-
            HTTP 200。code=0 表示受理或查询成功，code=1 表示业务或认证失败，code=400
            表示请求参数或上传文件属性不符合要求。
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/BulkUploadResponse'
                  - $ref: '#/components/schemas/BulkUploadValidationError'
                  - $ref: '#/components/schemas/PublicApiError'
              examples:
                accepted:
                  summary: 文件已接收，尚未确认逐笔支付结果
                  value:
                    code: 0
                    message: ok
                    data:
                      request_id: 19eb4182-bcd8-4bb1-a326-498a05f0e4aa
                      batch_id: 2048
                      status: received
                      request_merchant_id: '48290713'
                      request_business_id: '7294815'
                      merchant_id: '7294815'
                      business_id: '48290713'
                      file_identifier: PAY2030001
                      declared_detail_count: 2
                      declared_total_amount_cents: 7350
                duplicate:
                  summary: 相同标识重复上传
                  value:
                    code: 1
                    message: Duplicate upload for the same file_identifier
                    data: {}
                conflict:
                  summary: 相同标识对应不同内容
                  value:
                    code: 1
                    message: >-
                      File identifier conflict: same identifier with different
                      content
                    data: {}
                fileFormatError:
                  summary: 文件尾金额合计不一致
                  value:
                    code: 1
                    message: Summary amount does not match streamed detail sum
                    data:
                      request_id: 19eb4182-bcd8-4bb1-a326-498a05f0e4aa
                      batch_id: 2049
                      status: validation_failed
                      error_count: 1
                      first_error_line: 4
                      first_error_reason: Summary amount does not match streamed detail sum
                      first_error_record_type: E
                fileFormatErrorWithoutBatch:
                  summary: 无有效文件标识，未生成批次
                  value:
                    code: 1
                    message: First record must be H
                    data:
                      request_id: 19eb4182-bcd8-4bb1-a326-498a05f0e4aa
                      batch_id: null
                      status: validation_failed
                      error_count: 1
                      first_error_line: 1
                      first_error_reason: First record must be H
                      first_error_record_type: D
                invalidFile:
                  summary: 文件属性校验失败
                  value:
                    code: 400
                    message: Only .txt files are accepted
                    data: {}
                permissionDenied:
                  summary: 商户标识与凭据不一致
                  value:
                    code: 1
                    message: Permission denied
                    data: {}
                unauthenticated:
                  summary: 认证失败
                  value:
                    code: 1
                    message: Unauthenticated, invalid or expired access token
                    data: []
        '429':
          description: >-
            请求超过频率限制。API 请求共享每分钟 120 次的限额，通常按请求来源 IP 计数，并非每个接口单独 120 次。请遵循
            Retry-After 指定的等待秒数。
      security:
        - oauth2: []
      x-codeSamples:
        - lang: Shell
          label: 上传示例文件
          source: |-
            curl -X POST "${BASE_URL}/api/v2/trans/bulk-upload" \
              -H "Authorization: Bearer ${ACCESS_TOKEN}" \
              -H "Accept: application/json" \
              -F merchant_id=48290713 \
              -F 'file=@payments.txt;type=text/plain'
        - lang: Python
          label: 上传示例文件
          source: >
            import json

            import os

            import urllib.parse

            import urllib.request

            import uuid

            from pathlib import Path


            base_url = os.environ["BASE_URL"].rstrip("/")

            url = base_url + "/api/v2/trans/bulk-upload"

            headers = {"Accept": "application/json"}

            headers["Authorization"] = "Bearer " + os.environ["ACCESS_TOKEN"]

            fields = {
                "merchant_id": "48290713",
            }

            boundary = "----BillBuddy" + uuid.uuid4().hex

            parts = []

            for name, value in fields.items():
                parts.append((
                    f'--{boundary}\r\n'
                    f'Content-Disposition: form-data; name="{name}"\r\n\r\n'
                    f'{value}\r\n'
                ).encode("utf-8"))
            parts.append((
                f'--{boundary}\r\n'
                'Content-Disposition: form-data; name="file"; filename="payments.txt"\r\n'
                'Content-Type: text/plain\r\n\r\n'
            ).encode("utf-8"))

            parts.append(Path("payments.txt").read_bytes())

            parts.append(f'\r\n--{boundary}--\r\n'.encode("ascii"))

            body = b"".join(parts)

            headers["Content-Type"] = "multipart/form-data; boundary=" +
            boundary


            request = urllib.request.Request(url, data=body, headers=headers,
            method="POST")

            with urllib.request.urlopen(request, timeout=30) as response:
                print(response.read().decode("utf-8"))
        - lang: JavaScript
          label: 上传示例文件
          source: >
            import { readFile } from "node:fs/promises";


            const env = (name) => {
              const value = process.env[name];
              if (!value) throw new Error(`Missing environment variable: ${name}`);
              return value;
            };

            const baseUrl = env("BASE_URL").replace(/\/+$/, "");

            const url = new URL(baseUrl + "/api/v2/trans/bulk-upload");

            const headers = { Accept: "application/json" };

            headers.Authorization = `Bearer ${env("ACCESS_TOKEN")}`;

            const body = new FormData();

            body.append("merchant_id", "48290713");

            const file = await readFile("payments.txt");

            body.append("file", new Blob([file], { type: "text/plain" }),
            "payments.txt");


            const response = await fetch(url, {
              method: "POST",
              headers,
              body,
              signal: AbortSignal.timeout(30_000),
            });

            console.log(await response.text());
        - lang: PHP
          label: 上传示例文件
          source: >
            <?php

            $env = static function (string $name): string {
                $value = getenv($name);
                if ($value === false || $value === '') {
                    throw new RuntimeException("Missing environment variable: " . $name);
                }
                return $value;
            };

            $baseUrl = rtrim($env('BASE_URL'), '/');

            $url = $baseUrl . "/api/v2/trans/bulk-upload";

            $headers = ['Accept: application/json'];

            $headers[] = 'Authorization: Bearer ' . $env('ACCESS_TOKEN');

            $params = [
                "merchant_id" => "48290713",
            ];

            $params['file'] = new CURLFile('payments.txt', 'text/plain',
            'payments.txt');

            $body = $params;


            $curl = curl_init($url);

            curl_setopt_array($curl, [
                CURLOPT_CUSTOMREQUEST => "POST",
                CURLOPT_HTTPHEADER => $headers,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_POSTFIELDS => $body,
            ]);

            $response = curl_exec($curl);

            if ($response === false) {
                $error = curl_error($curl);
                curl_close($curl);
                throw new RuntimeException($error);
            }

            curl_close($curl);

            echo $response, PHP_EOL;
        - lang: Go
          label: 上传示例文件
          source: |
            package main

            import (
                "bytes"
                "fmt"
                "io"
                "mime/multipart"
                "net/http"
                "os"
                "strings"
                "time"
            )

            func mustEnv(name string) string {
                value := os.Getenv(name)
                if value == "" { panic("Missing environment variable: " + name) }
                return value
            }

            func main() {
                baseURL := strings.TrimRight(mustEnv("BASE_URL"), "/")
                endpoint := baseURL + "/api/v2/trans/bulk-upload"
                var body bytes.Buffer
                writer := multipart.NewWriter(&body)
                if err := writer.WriteField("merchant_id", "48290713"); err != nil { panic(err) }
                file, err := os.Open("payments.txt")
                if err != nil { panic(err) }
                defer file.Close()
                part, err := writer.CreateFormFile("file", "payments.txt")
                if err != nil { panic(err) }
                if _, err = io.Copy(part, file); err != nil { panic(err) }
                if err = writer.Close(); err != nil { panic(err) }
                req, err := http.NewRequest("POST", endpoint, &body)
                if err != nil { panic(err) }
                req.Header.Set("Accept", "application/json")
                req.Header.Set("Authorization", "Bearer " + mustEnv("ACCESS_TOKEN"))
                req.Header.Set("Content-Type", writer.FormDataContentType())
                client := &http.Client{Timeout: 30 * time.Second}
                response, err := client.Do(req)
                if err != nil { panic(err) }
                defer response.Body.Close()
                result, err := io.ReadAll(response.Body)
                if err != nil { panic(err) }
                fmt.Println(string(result))
            }
        - lang: Java
          label: 上传示例文件
          source: |
            import java.io.ByteArrayOutputStream;
            import java.net.URI;
            import java.net.URLEncoder;
            import java.net.http.HttpClient;
            import java.net.http.HttpRequest;
            import java.net.http.HttpResponse;
            import java.nio.charset.StandardCharsets;
            import java.nio.file.Files;
            import java.nio.file.Path;
            import java.time.Duration;
            import java.util.LinkedHashMap;
            import java.util.Map;
            import java.util.UUID;
            import java.util.stream.Collectors;

            public class Example {
                static String requireEnv(String name) {
                    String value = System.getenv(name);
                    if (value == null || value.isEmpty()) {
                        throw new IllegalArgumentException("Missing environment variable: " + name);
                    }
                    return value;
                }

                public static void main(String[] args) throws Exception {
                    String baseUrl = requireEnv("BASE_URL").replaceAll("/+$", "");
                    String endpoint = baseUrl + "/api/v2/trans/bulk-upload";
                    String boundary = "----BillBuddy" + UUID.randomUUID();
                    ByteArrayOutputStream body = new ByteArrayOutputStream();
                    body.write(("--" + boundary + "\r\n"
                        + "Content-Disposition: form-data; name=\"merchant_id\"\r\n\r\n"
                        + "48290713" + "\r\n").getBytes(StandardCharsets.UTF_8));
                    body.write(("--" + boundary + "\r\n"
                        + "Content-Disposition: form-data; name=\"file\"; filename=\"payments.txt\"\r\n"
                        + "Content-Type: text/plain\r\n\r\n").getBytes(StandardCharsets.UTF_8));
                    Files.copy(Path.of("payments.txt"), body);
                    body.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
                    HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(endpoint))
                        .timeout(Duration.ofSeconds(30))
                        .header("Accept", "application/json");
                    builder.header("Authorization", "Bearer " + requireEnv("ACCESS_TOKEN"));
                    builder.header("Content-Type", "multipart/form-data; boundary=" + boundary);
                    HttpRequest request = builder.method("POST", HttpRequest.BodyPublishers.ofByteArray(body.toByteArray())).build();
                    HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build();
                    HttpResponse<String> response = client.send(request,
                        HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
                    System.out.println(response.body());
                }
            }
        - lang: Ruby
          label: 上传示例文件
          source: |
            require "net/http"
            require "uri"

            base_url = ENV.fetch("BASE_URL").sub(%r{/+$}, "")
            uri = URI(base_url + "/api/v2/trans/bulk-upload")
            params = {
              "merchant_id" => "48290713",
            }
            request = Net::HTTP::Post.new(uri)
            request["Accept"] = "application/json"
            request["Authorization"] = "Bearer " + ENV.fetch("ACCESS_TOKEN")
            http = Net::HTTP.new(uri.host, uri.port)
            http.use_ssl = uri.scheme == "https"
            http.open_timeout = 30
            http.read_timeout = 30
            File.open("payments.txt", "rb") do |file|
              fields = params.to_a
              fields << ["file", file, { filename: "payments.txt", content_type: "text/plain" }]
              request.set_form(fields, "multipart/form-data")
              puts http.request(request).body
            end
components:
  schemas:
    BulkUploadResponse:
      type: object
      properties:
        code:
          type: integer
          description: 文件已接收；不代表逐笔支付成功。
          enum:
            - 0
        message:
          type: string
          description: 处理结果说明，成功为 ok。
          example: ok
        data:
          type: object
          properties:
            request_id:
              type: string
              description: 本次请求的跟踪标识，可提供给 BillBuddy 协助排查。
            batch_id:
              type: integer
              description: 批次标识，用于后续查询。
              format: int64
              minimum: 1
            status:
              type: string
              description: received 表示文件已接收，尚不代表逐笔支付成功。
              enum:
                - received
            request_merchant_id:
              type: string
              description: 上传请求 merchant_id 的原值。
            request_business_id:
              type: string
              description: OAuth 凭据所属商户的标识。
            merchant_id:
              type: string
              description: 批次所属的商户标识。
            business_id:
              type: string
              description: 批次所属的业务标识。
            file_identifier:
              type: string
              description: 文件头中的十位文件标识，只能包含字母、数字和下划线。
              minLength: 10
              maxLength: 10
              pattern: ^[A-Za-z0-9_]{10}$
            declared_detail_count:
              type: integer
              description: 已接收文件的明细总笔数，至少为 1。
              minimum: 1
            declared_total_amount_cents:
              type: integer
              description: 已接收文件的本金合计，单位为分，不含附加费；明细金额是否有效须结合逐行结果判断。
              format: int64
              minimum: 0
          required:
            - request_id
            - batch_id
            - status
            - request_merchant_id
            - request_business_id
            - merchant_id
            - business_id
            - file_identifier
            - declared_detail_count
            - declared_total_amount_cents
          additionalProperties: false
          description: 响应数据。
      required:
        - code
        - message
        - data
      additionalProperties: false
    BulkUploadValidationError:
      type: object
      properties:
        code:
          type: integer
          description: 文件内容不符合固定宽度格式，业务码为 1。
          enum:
            - 1
        message:
          type: string
          description: 首个格式错误原因。
        data:
          type: object
          properties:
            request_id:
              type: string
              description: 本次请求的跟踪标识，可提供给 BillBuddy 协助排查。
            batch_id:
              type: integer
              description: 失败批次标识；未生成批次时为 null。
              format: int64
              minimum: 1
              nullable: true
            status:
              type: string
              description: 文件格式校验失败。
              enum:
                - validation_failed
            error_count:
              type: integer
              description: 本次返回的格式错误数量，不保证列出文件的所有错误。
              minimum: 1
            first_error_line:
              type: integer
              description: 首个格式错误的物理行号，从 1 开始；不能定位时为 null。
              minimum: 1
              nullable: true
            first_error_reason:
              type: string
              description: 首个格式错误原因。
            first_error_record_type:
              type: string
              description: 错误记录类型，如 H、D、E；不能定位时为 null。
              nullable: true
          required:
            - request_id
            - batch_id
            - status
            - error_count
            - first_error_line
            - first_error_reason
            - first_error_record_type
          additionalProperties: false
          description: 响应数据。
      required:
        - code
        - message
        - data
      additionalProperties: false
    PublicApiError:
      type: object
      properties:
        code:
          type: integer
          description: 业务或认证失败为 1；请求参数或文件属性校验失败为 400。
          enum:
            - 1
            - 400
        message:
          type: string
          description: 错误原因，实际文案随失败原因变化。
        data:
          description: 业务及参数错误为空对象；认证失败为空数组。
          oneOf:
            - type: object
              additionalProperties: false
            - type: array
              items: {}
              maxItems: 0
      required:
        - code
        - message
        - data
      additionalProperties: false
  securitySchemes:
    oauth2:
      type: oauth2
      description: OAuth2 client_credentials 授权。access_token 到期后重新获取。
      flows:
        clientCredentials:
          tokenUrl: /oauth/token
          scopes: {}

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.