curl -X POST "${BASE_URL}/api/v2/trans/bulk-upload" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Accept: application/json" \
-F merchant_id=48290713 \
-F '[email protected];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
Upload a TXT transaction file
Upload a fixed-width TXT file using multipart/form-data for asynchronous bank account or payment token transactions. Use an OAuth2 Bearer token; no request signature or customer identifier is required. Let your upload tool generate the Content-Type boundary.
File requirements
The file must be nonempty and no larger than 10 MiB (10,485,760 bytes). The filename including extension must not exceed 50 bytes. The base name may contain only letters, digits and underscores, and the extension must be .txt (case-insensitive). Content must contain only printable ASCII characters (0x20–0x7E). BOM, tabs and multibyte characters are not allowed. Each record must be exactly 120 bytes before the line ending. Every record, including the last, must end with CRLF (\r\n). Preserve trailing padding spaces.
Use one H header, at least one D detail and one E trailer, in that order. Do not append blank lines or other content after E. D logical line numbers start at 000001 and increase consecutively. The trailer count and base amount total must match all D records. All positions below start at 1; fill unused areas with spaces as specified.
Header H
| Position (starting at 1) | Bytes | Field / fixed value | Format and restrictions |
|---|---|---|---|
| 1 | 1 | H | Fixed record type. |
| 2–17 | 16 | BILLBUDDYPAYMENT | Fixed identifier. |
| 18 | 1 | Space | Fixed padding. |
| 19–23 | 5 | Biller code | Five letters or digits, such as ABC12; confirm the applicable value with BillBuddy. |
| 24–30 | 7 | Spaces | Fixed padding. |
| 31–40 | 10 | file_identifier | Exactly ten letters, digits or underscores, such as PAY2030001. |
| 41 | 1 | Space | Fixed padding. |
| 42–47 | 6 | File creation date | Valid ddmmyy; years 00–99 mean 2000–2099. For example, 010130 means 2030-01-01. |
| 48 | 1 | Reserved | Space or T; lowercase t is also accepted. A space is recommended. This is not a test switch and does not enable simulated payments. |
| 49–120 | 72 | Spaces | Reserved; fill with spaces. |
Detail D: common fields
| Position (starting at 1) | Bytes | Field / fixed value | Format and restrictions |
|---|---|---|---|
| 1 | 1 | D | Fixed record type. |
| 2–17 | 16 | Business reference | Non-blank, up to 16 characters, left-aligned and padded with spaces. |
| 18 | 1 | Space | Fixed padding. |
| 19–28 | 10 | Base amount | Ten digits, zero-padded on the left, in cents. Valid range: 1–999999 (AUD 0.01–9999.99). For example, 0000002550 means AUD 25.50. |
| 29–30 | 2 | Spaces | Fixed padding. |
| 31–32 | 2 | Payment type | BC: bank account; CT: payment token. Both types may appear in one file. |
| 115–120 | 6 | Logical line number | Six digits, starting at 000001 and increasing consecutively across D records. |
Detail D: BC fields
| Position (starting at 1) | Bytes | Field / fixed value | Format and restrictions |
|---|---|---|---|
| 33 | 1 | Space | Fixed padding. |
| 34–40 | 7 | BSB | Three digits, a hyphen, then three digits, such as 123-456. |
| 41 | 1 | Space | Fixed padding. |
| 42–50 | 9 | Bank account number | Non-blank numeric account number, up to 9 digits, padded with spaces on the right; leading zeros are preserved. Spaces and hyphens in this area are ignored; digits only are recommended. |
| 51–66 | 16 | Spaces | Must contain only spaces. |
| 67–98 | 32 | Account name | Non-blank, up to 32 characters, left-aligned and padded with spaces. |
| 99 | 1 | Space | Fixed padding. |
| 100–103 | 4 | 0000 | Four fixed zeros. |
| 104–114 | 11 | Spaces | Reserved; fill with spaces. |
Detail D: CT fields
| Position (starting at 1) | Bytes | Field / fixed value | Format and restrictions |
|---|---|---|---|
| 33–82 | 50 | Spaces | Must contain only spaces. |
| 83–114 | 32 | Payment token | A valid payment token of exactly 32 characters; must not be blank or padded with trailing spaces. Use a token supplied for your approved test environment. |
Trailer E
| Position (starting at 1) | Bytes | Field / fixed value | Format and restrictions |
|---|---|---|---|
| 1 | 1 | E | Fixed record type. |
| 2–18 | 17 | Spaces | Must contain only spaces. |
| 19–28 | 10 | Total base amount | Ten digits, zero-padded on the left, in cents; must equal the sum of all D base amounts, excluding surcharges. |
| 29–99 | 71 | Spaces | Must contain only spaces. |
| 100–111 | 12 | 000000000000 | Twelve fixed zeros. |
| 112–114 | 3 | Spaces | Must contain only spaces. |
| 115–120 | 6 | Total detail count | Six digits, zero-padded on the left; must equal the D record count and be at least 000001. |
Acceptance, lookup and duplicate submissions
code=0 with status=received only confirms file acceptance. File structure is validated on upload; detail fields and payment tokens are then validated per record. Valid records may still execute when other records fail. Currency is AUD. File amounts, amount_cents and declared_total_amount_cents are in cents, excluding separately calculated surcharges.
file_identifier must be unique across all businesses and upload entry points for the same merchant. Reusing an identifier with either identical or different content returns code=1. A rejected file may also reserve its identifier. Retain batch_id and file_identifier and check the batch result endpoint. Do not submit the same payments under a new identifier after a timeout, duplicate-upload error or temporarily unavailable result.
For later lookups, reuse merchant_id from the upload request and OAuth credentials for the same merchant and identify the batch using the returned batch_id or the file_identifier from the file header. Response fields are defined below.
Example file
Download payments.txt. It contains one AUD 25.50 bank account record and one AUD 48.00 token record, totaling two payments and AUD 73.50, with fixed-width spaces and CRLF preserved. The biller code, file identifier, account, token, references and date are fictional. Replace them with approved test data and recalculate the trailer count and amount before calling the endpoint.
curl -X POST "${BASE_URL}/api/v2/trans/bulk-upload" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Accept: application/json" \
-F merchant_id=48290713 \
-F '[email protected];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
Authorizations
OAuth2 client_credentials authorization. Obtain a new access_token when it expires.
Body
Response
HTTP 200. code=0 means accepted or queried successfully; code=1 indicates a business or authentication error; code=400 indicates invalid parameters or upload file properties.