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'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"))
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());
<?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;
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))
}
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());
}
}
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
{
"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
}
}上传 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。文件包含一条 AUD 25.50 银行账户记录和一条 AUD 48.00 Token 记录,共两笔 AUD 73.50;保留了固定宽度空格及 CRLF。账单方代码、文件标识、账户、Token、参考号和日期均为虚构示例,调用前替换为获准测试数据,且重新计算尾部笔数和金额合计。
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'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"))
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());
<?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;
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))
}
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());
}
}
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
{
"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
}
}授权
OAuth2 client_credentials 授权。access_token 到期后重新获取。