> ## 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.

# 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](/examples/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.



## OpenAPI

````yaml /openapi/billbuddy-v2-openapi.en.yaml post /api/v2/trans/bulk-upload
openapi: 3.0.3
info:
  title: BillBuddy API Integration Guide
  version: 2.0.0
  description: >
    BillBuddy API integration guide. It covers authentication, transaction and
    subscription endpoints called by third-party servers. Request and response
    examples contain complete fictional business data and tokens generated
    offline; they cannot authenticate or initiate real transactions.


    ## Getting started

    1. Obtain your Client ID, Client Secret, merchant identifier and business
    identifier from BillBuddy. If an IP allowlist applies, send requests from an
    approved fixed outbound IP address.

    2. Obtain an access_token through POST /oauth/token. Business requests
    require Authorization: Bearer {access_token} and Accept: application/json.
    Use Content-Type: application/json for JSON requests and form encoding for
    token requests.

    3. Call the payment or subscription endpoints as needed. Customers open the
    hosted activation page through activation_url. Store the Client Secret only
    on your server.


    ## Identifiers and amounts

    OAuth credentials identify the merchant; business_id is not a request
    parameter. Endpoints that require a business use merchant_id. For
    single-transaction and batch-result lookups, merchant_id is optional; omit
    it or leave it empty to search all businesses of the authenticated merchant.
    The transaction list uses optional, case-sensitive Mid; omit it to query all
    businesses belonging to the authenticated merchant. A requested business
    must belong to that merchant. Read response identifiers according to each
    endpoint's field definitions.

    TXT file amounts, amount_cents and declared_total_amount_cents use cents.
    Other amounts use major currency units with at most two decimal places:
    10.50 means AUD 10.50. Use strings for bank account numbers, card numbers,
    months, years and postcodes to preserve leading zeros.


    ## Success and failure

    Business JSON responses use code=0 for success and code=1 for ordinary
    failures. Transaction and subscription validation failures usually return
    code=400 and data={} with HTTP 200. V2 business endpoint authentication
    failures use code=1. A missing, malformed or inaccessible business
    merchant_id for plan/list return code=1. HTTP 429 indicates a rate limit.
    The OAuth endpoint uses standard OAuth responses with HTTP 200/400/401/429.

    code=0 means the API request was processed successfully; assess the payment
    outcome separately. Batch accepted means the request was accepted for
    asynchronous processing. Bank Pending, scheduled Pending and outstanding 3DS
    authentication do not indicate payment success. Transactions, DDR
    subscriptions and payment schedules use different status enumerations; refer
    to their field definitions.


    ## Dates and frequencies

    Use YYYY-MM-DD for request dates. Today and tomorrow are evaluated in
    Australia/Sydney, including daylight saving time. Scheduled cancellations
    also use Australia/Sydney: cancellation must occur strictly before 08:15 on
    the processing date; it is no longer permitted at that time. Payment
    complete_date and checkout expires_at use DD/MM/YYYY HH:mm:ss. Refer to
    individual fields for other formats.

    Standard RRULE examples: 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. Weekday abbreviations are
    MO/TU/WE/TH/FR/SA/SU. Weekly/Monthly rules without a specific debit day
    derive it from the start date. A single BYDAY=SA or SU is treated as Monday;
    a weekend start date without an explicit debit day is also treated as
    Monday. A single BYMONTHDAY=29/30/31 falls on the last day of shorter
    months; BYMONTHDAY=-1 means the last day of the month. All rules must be
    valid RRULE expressions.

    For a nonstandard frequency such as every 10 days, provide process_type=12,
    custom_cycle_type=1, custom_cycle=10 and
    day_of_debit=FREQ=DAILY;INTERVAL=10. Units 1/2/3/4 correspond to
    DAILY/WEEKLY/MONTHLY/YEARLY. The minimum interval is 5 for days and 1 for
    other units. INTERVAL must equal custom_cycle. Quarterly, half-yearly and
    yearly schedules also require day_of_debit_dtstart. Its complete date must
    be strictly after the start_date of the same schedule segment and within one
    billing cycle after it. The first actual debit date for createSchedule
    depends on the DDR start date, debit frequency and the requirement that it
    be no earlier than tomorrow.


    ## Using examples

    Schemas define types, nullability, lengths, enumerations and numeric ranges.
    Field descriptions define conditional requirements, dates and business
    restrictions. Use both together. Set BASE_URL, ACCESS_TOKEN, CLIENT_ID and
    CLIENT_SECRET before running curl examples. BillBuddy provides the actual
    test and production domains. Replace example customers, orders, cards,
    accounts and dates with values permitted in your test environment and
    satisfying the date restrictions.


    ## Hosted subscription integration

    Call POST /api/v2/subscription/create, then use the complete activation_url
    unchanged for a browser redirect or an iframe src. Alternatively, use
    activation_html as the iframe srcdoc. Customers enter payment information,
    authorize debits and activate the subscription on the BillBuddy-hosted page.
    Use the full returned URL; do not construct it yourself or parse its
    credentials. Keep the Client Secret and access_token on your server; never
    include them in iframe URLs or page contents.


    Recurring and By agreement subscriptions must be activated before
    start_date. One-off-only subscriptions must be submitted for activation
    within 30 days of creation. An accessible page does not guarantee submission
    is still permitted; the activation response is authoritative.


    ## Example data conventions

    Example merchants, customers, orders, payment tokens, session links and
    OAuth credentials are fictional and cannot be used for real requests. Card
    numbers are standard test card numbers; bank accounts are fictional. Email
    addresses use the reserved example.com domain, and phone numbers use
    Australian numbers reserved for examples. Replace BASE_URL, ACCESS_TOKEN,
    CLIENT_ID and CLIENT_SECRET with actual values from your approved test
    environment. Empty strings, null, empty arrays, 0 and false represent
    meaningful API states in some examples and must not be replaced
    indiscriminately.

    Payment response signatures are calculated offline according to the signing
    rules for each payment scenario, using the public demo key
    bb_docs_demo_signing_key_2030. The html field is excluded for responses
    awaiting 3DS authentication. This key is for documentation only and cannot
    sign or authenticate real requests.


    ## TXT batch transactions

    Upload fixed-width TXT files using multipart/form-data. File amounts,
    amount_cents and declared_total_amount_cents use cents, excluding
    surcharges; see “Upload a TXT transaction file” for the format and example
    file. Upload success only confirms acceptance. Retain batch_id and
    file_identifier and use the batch result endpoint to check individual
    outcomes. Batch completed does not mean every payment succeeded. Use OAuth
    credentials for the same merchant to query results. merchant_id is optional;
    when supplied, it must match the business identifier in the original upload.
    Response identifiers follow the definitions for each endpoint.


    ## Request examples by programming language

    Examples are provided in cURL, Python, JavaScript, PHP, Go, Java and Ruby,
    with the same business scenarios available in each language. Each example
    preserves the parameters and types from its corresponding cURL scenario.


    Requirements: cURL; Python 3.9+ (standard library); JavaScript with Node.js
    18+, saved as an .mjs file; PHP 8+ with the cURL extension; Go 1.20+; Java
    11+ (save the Example class as Example.java); Ruby 3+ (standard library).


    Set BASE_URL before running examples. Business endpoints also require
    ACCESS_TOKEN; token requests require CLIENT_ID and CLIENT_SECRET. BASE_URL
    is the API root without /api/v2. TXT upload examples read payments.txt from
    the current directory. Replace fictional values with approved test data and
    assess the response code and business status separately.
servers:
  - url: https://staging-api.billbuddyhq.com
    description: Staging environment
security:
  - oauth2: []
tags:
  - name: Authentication
    description: OAuth2 Client Credentials。
  - name: Transactions
    description: Single payments, batch payments and scheduled payment cancellations.
  - name: Subscriptions
    description: >-
      Subscription templates, hosted subscription creation, DDR creation and
      activation, and payment schedule submission.
paths:
  /api/v2/trans/bulk-upload:
    post:
      tags:
        - Transactions
      summary: Upload a TXT transaction file
      description: >-
        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](/examples/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.
      operationId: v2BulkUpload
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                merchant_id:
                  type: string
                  description: Business identifier; must belong to that merchant.
                  minLength: 1
                  maxLength: 20
                  example: '48290713'
                file:
                  type: string
                  description: >-
                    Nonempty TXT file, up to 10 MiB; see the requirements above
                    for filename, encoding, fixed-width layout and line endings.
                  format: binary
              required:
                - merchant_id
                - file
              additionalProperties: false
      responses:
        '200':
          description: >-
            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.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/BulkUploadResponse'
                  - $ref: '#/components/schemas/BulkUploadValidationError'
                  - $ref: '#/components/schemas/PublicApiError'
              examples:
                accepted:
                  summary: File accepted; individual payment outcomes are not confirmed
                  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: Duplicate file identifier
                  value:
                    code: 1
                    message: Duplicate upload for the same file_identifier
                    data: {}
                conflict:
                  summary: Same identifier with different content
                  value:
                    code: 1
                    message: >-
                      File identifier conflict: same identifier with different
                      content
                    data: {}
                fileFormatError:
                  summary: Trailer amount does not match detail amounts
                  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: No valid file identifier; no batch created
                  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: Invalid upload file properties
                  value:
                    code: 400
                    message: Only .txt files are accepted
                    data: {}
                permissionDenied:
                  summary: Permission denied
                  value:
                    code: 1
                    message: Permission denied
                    data: {}
                unauthenticated:
                  summary: Authentication failed
                  value:
                    code: 1
                    message: Unauthenticated, invalid or expired access token
                    data: []
        '429':
          description: >-
            Rate limit exceeded. API requests share a limit of 120 requests per
            minute, normally counted by source IP; this is not a separate
            allowance for each endpoint. Wait for the seconds specified by
            Retry-After.
      security:
        - oauth2: []
      x-codeSamples:
        - lang: Shell
          label: Upload the example file
          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: Upload the example file
          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: Upload the example file
          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: Upload the example file
          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: Upload the example file
          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: Upload the example file
          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: Upload the example file
          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: File accepted; this does not confirm individual payment success.
          enum:
            - 0
        message:
          type: string
          description: Result message; ok on success.
          example: ok
        data:
          type: object
          properties:
            request_id:
              type: string
              description: >-
                Tracking identifier for this request; provide it to BillBuddy
                when requesting assistance.
            batch_id:
              type: integer
              description: Batch identifier for subsequent lookups.
              format: int64
              minimum: 1
            status:
              type: string
              description: >-
                received means the file was accepted; individual payments are
                not yet confirmed.
              enum:
                - received
            request_merchant_id:
              type: string
              description: Original merchant_id from the upload request.
            request_business_id:
              type: string
              description: >-
                Identifier of the merchant associated with the OAuth
                credentials.
            merchant_id:
              type: string
              description: Merchant identifier associated with the batch.
            business_id:
              type: string
              description: Business identifier associated with the batch.
            file_identifier:
              type: string
              description: >-
                Ten-character identifier from the file header; letters, digits
                and underscores only.
              minLength: 10
              maxLength: 10
              pattern: ^[A-Za-z0-9_]{10}$
            declared_detail_count:
              type: integer
              description: Detail count in the accepted file, at least 1.
              minimum: 1
            declared_total_amount_cents:
              type: integer
              description: >-
                Total base amount in the accepted file, in cents, excluding
                surcharges. Check per-record results to determine whether each
                amount is valid.
              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: Response data.
      required:
        - code
        - message
        - data
      additionalProperties: false
    BulkUploadValidationError:
      type: object
      properties:
        code:
          type: integer
          description: >-
            File content does not meet the fixed-width format; business code is
            1.
          enum:
            - 1
        message:
          type: string
          description: Reason for the first format error.
        data:
          type: object
          properties:
            request_id:
              type: string
              description: >-
                Tracking identifier for this request; provide it to BillBuddy
                when requesting assistance.
            batch_id:
              type: integer
              description: Rejected batch identifier; null if no batch was created.
              format: int64
              minimum: 1
              nullable: true
            status:
              type: string
              description: File format validation failed.
              enum:
                - validation_failed
            error_count:
              type: integer
              description: >-
                Number of format errors reported; this is not an exhaustive list
                of every issue in the file.
              minimum: 1
            first_error_line:
              type: integer
              description: >-
                Physical line number of the first format error, starting at 1;
                null if unavailable.
              minimum: 1
              nullable: true
            first_error_reason:
              type: string
              description: Reason for the first format error.
            first_error_record_type:
              type: string
              description: >-
                Record type at the error, such as H, D or E; null if
                unavailable.
              nullable: true
          required:
            - request_id
            - batch_id
            - status
            - error_count
            - first_error_line
            - first_error_reason
            - first_error_record_type
          additionalProperties: false
          description: Response data.
      required:
        - code
        - message
        - data
      additionalProperties: false
    PublicApiError:
      type: object
      properties:
        code:
          type: integer
          description: >-
            1 for business or authentication errors; 400 for parameter or file
            property validation errors.
          enum:
            - 1
            - 400
        message:
          type: string
          description: Error reason; exact wording depends on the failure.
        data:
          description: >-
            Empty object for business or validation errors; empty array for
            authentication failures.
          oneOf:
            - type: object
              additionalProperties: false
            - type: array
              items: {}
              maxItems: 0
      required:
        - code
        - message
        - data
      additionalProperties: false
  securitySchemes:
    oauth2:
      type: oauth2
      description: >-
        OAuth2 client_credentials authorization. Obtain a new access_token when
        it expires.
      flows:
        clientCredentials:
          tokenUrl: /oauth/token
          scopes: {}

````

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