Batch payments: integrate and test

Create a batch payment, handle the answer and its errors, and test it in test mode.

A batch payment sends many payments to the bank in one file (host-to-host). Use it when your bank integration is host-to-host. A single payment, created with Create a Payment, goes to the bank over its API instead.

The payments in a batch share a payment type, a payment date and a sender account. Each payment has its own id and status. For the statuses and webhooks, see Batch payment flow.

Create a batch payment

Send Create a Batch Payment (POST /v1/payment-batches) with an Idempotency-Key header, so you can retry it safely (see Idempotency).

This example is a DBS Singapore MEPS batch of two payments. The fields each payment needs depend on the bank and the payment type: look them up in your bank's guide under Bank payment rules.

Request
{
  "type": "MEPS",
  "paymentDate": "2026-11-10",
  "senderAccountId": "intacct_0KSEVXZ2ZF0AV",
  "currency": "SGD",
  "payments": [
    {
      "amount": 20000,
      "customerReference": "INV-1001",
      "paymentDetails": "10 boxes of 12 each",
      "bankChargeBearer": "SHARED",
      "receiver": {
        "name": "Donut Shop",
        "bank": "OCBCSGSGXXX",
        "bankAccountNumber": "123456789",
        "address": { "line1": "20 Side Street", "line2": "Unit 02-3A" }
      }
    },
    {
      "amount": 3000,
      "customerReference": "INV-1002",
      "bankChargeBearer": "SHARED",
      "receiver": {
        "name": "Coffee Shop",
        "bank": "UOVBSGSGXXX",
        "bankAccountNumber": "345678901",
        "address": { "line1": "127 Jln Merdeka" }
      }
    }
  ]
}
  • amount is in minor units: 20000 is 200.00 SGD. See Minor units format.
  • senderAccountCurrency is required only when the sender account holds more than one currency in Acme.
  • currency on a payment is optional and defaults to the batch currency. Only TT, US_WIRE_INTL, ACT and BKTR payments can use another currency.
  • customerReference is optional. Acme generates one when you leave it out.
  • For a PAYNOW or other proxy payment, send receiver.proxyType and receiver.proxyValue instead of a bank account. Your bank's guide lists which fields each type takes.

Response

A 200 means Acme created the batch. It does not mean the bank has made the payments yet.

200 OK
{
  "id": "bpmt_0KSEVXZ2ZF0AV",
  "type": "MEPS",
  "paymentDate": "2026-11-10",
  "senderAccountId": "intacct_0KSEVXZ2ZF0AV",
  "senderAccountCurrency": "SGD",
  "currency": "SGD",
  "status": "PROCESSING",
  "payments": [
    {
      "id": "pymt_0KSEVXZ2ZF0B0",
      "amount": 20000,
      "currency": "SGD",
      "customerReference": "INV-1001",
      "paymentDetails": "10 boxes of 12 each",
      "bankChargeBearer": "SHARED",
      "receiver": {
        "name": "Donut Shop",
        "bank": "OCBCSGSGXXX",
        "bankAccountNumber": "123456789",
        "address": { "line1": "20 Side Street", "line2": "Unit 02-3A" }
      },
      "status": "PROCESSING",
      "createdAt": "2026-11-06T02:15:04.000Z",
      "updatedAt": "2026-11-06T02:15:04.000Z"
    },
    {
      "id": "pymt_0KSEVXZ2ZF0B1",
      "amount": 3000,
      "currency": "SGD",
      "customerReference": "INV-1002",
      "bankChargeBearer": "SHARED",
      "receiver": {
        "name": "Coffee Shop",
        "bank": "UOVBSGSGXXX",
        "bankAccountNumber": "345678901",
        "address": { "line1": "127 Jln Merdeka" }
      },
      "status": "PROCESSING",
      "createdAt": "2026-11-06T02:15:04.000Z",
      "updatedAt": "2026-11-06T02:15:04.000Z"
    }
  ],
  "createdAt": "2026-11-06T02:15:04.000Z",
  "updatedAt": "2026-11-06T02:15:04.000Z"
}

Store each payment id

This response is the only one that lists the payments in the batch. Get a Batch Payment and List Batch Payments return the batch without its payments, and List Payments cannot filter by batch. Store each payment id, or match each payment webhook to its batch with batchId.

Each payment in the batch is also a Payment. Get a Payment returns it with these extra fields:

FieldWhat it holds
batchIdThe batch the payment belongs to.
paymentDateThe payment date you sent.
actualPaymentDateThe date the bank used, when the bank changed it (for example after its cutoff).
bankChargeAmount and bankChargeCurrencyThe bank's charge, when the bank reports one. Not every bank does.

Validation

Acme checks the whole batch before it creates anything, and rejects the whole batch if any payment fails. Every batch must:

  • Have type, paymentDate, senderAccountId and currency, and at least one payment.
  • Use a sender account of your organization, in the mode of your API key, that supports the payment type.
  • Give senderAccountCurrency, one of the account's currencies, when the account holds more than one.
  • Give each payment a positive whole amount and a receiver. The maximum amount depends on the bank and the payment type.
  • Give bankChargeBearer, when sent, as SENDER, RECEIVER or SHARED.

A batch holds at most 1,000 payments for most banks, and fewer for some types (ANZ, for example). Each bank adds its own rules: which currencies each type takes, which receiver fields are required or not allowed, and field lengths and characters. Your bank's guide lists them under Bank payment rules.

Possible errors

For the 4xx and 503 answers below, Acme created nothing, so nothing reached the bank. Only 400 and 429 carry an errorCode.

StatusWhat it meansWhat to do
400The request failed validation, or the sender account, its currency or the payment type is not valid. errorCode is INVALID_REQUEST_PARAMETER. See the example below.Fix every listed field and send the whole batch again, with a new Idempotency-Key.
401Empty body. The API key is missing, wrong or disabled, or the request came from an IP address your organization does not allow.Check the API key (a test key for test mode, a live key for live mode). If it is right, contact Acme.
403The API key does not have the Batch Payments permission.Contact Acme.
409Empty body. The Idempotency-Key was used in the last hour with a different request, or the first request with this key is still running.Send the first request again, unchanged (the same body, byte for byte) and with the same key. If it is still running you get a 409 again: wait a few seconds and repeat. Otherwise you get its saved answer: a 200 means the batch exists; a 400 means nothing was created; a 500: follow the 500 row.
429errorCode RATE_LIMIT_EXCEEDED. Your API key sent too many requests (Rate limits). Creating a batch does not call the bank, so this is never the bank's limit.Wait for the Retry-After interval, then retry with the same Idempotency-Key.
503{"message": "Bank is under maintenance."}. Live mode only: the bank, or this payment type, has a maintenance window. See Scheduled maintenance.Send the batch again after the window ends.
500An error at Acme. In rare cases (a database connection lost while the batch was being saved) the batch may still have been created.Wait a minute, then check List Batch Payments for a batch created at that time. If there is none, retry with a new Idempotency-Key: the same key can replay this answer for an hour. If it fails again, contact Acme with the time of the request.

Example 400 response

paymentErrors lists every payment field that failed, with the payment's position in payments (from 0). errorMessage repeats one of the failures as <field>: <rule>.

400 Bad Request
{
  "errorCode": "INVALID_REQUEST_PARAMETER",
  "errorMessage": "payments[0].currency: not supported, only SGD is supported",
  "paymentErrors": [
    { "index": 0, "field": "currency", "message": "not supported, only SGD is supported" },
    {
      "index": 1,
      "field": "receiver.address",
      "message": "must be provided for this type of payment"
    }
  ]
}
  • paymentErrors is empty when only batch fields failed, for example a missing paymentDate.
  • paymentErrors is left out when the body could not be read, or the sender account, its currency or the payment type is not valid. errorMessage then says what to fix, for example Sender account not found. Please recheck or contact Acme.

If you get no response at all (a timeout), send the same request again (the same body, byte for byte) with the same Idempotency-Key within an hour. You get the original answer, and no second batch is created. If that retry gets a 409, the first request is still running: wait a few seconds and retry again with the same key.

Test mode

Use your test API key to try your integration before going live. In test mode, Acme checks the batch with the same rules as in live mode, then simulates the bank instead of sending a file. This works the same for every bank.

Steps

Create a batch with your test key

Send the request above with your test API key and a test sender account. Every payment succeeds, except one sent to a receiver account number from the table below.

Wait for the simulated bank

Acme sends test batches on the same schedule as live ones. Expect payment-batches.submitted, then the payment results, usually 10 to 25 minutes after you create the batch.

Check the webhooks and statuses

Compare what you receive with the outcomes below, then check your system updated each payment.

Test account numbers

Put one of these in receiver.bankAccountNumber. Any receiver bank works.

receiver.bankAccountNumberWhat happens
000000000 or 000000001This payment is FAILED. The other payments in the batch still succeed.
000000002The bank rejects the whole batch. The batch is FAILED, and so is every payment in it.
Any other numberThe payment is COMPLETED.

What you receive

BatchWebhooks, in orderFinal status
No 000000002payment-batches.submitted (sample), then payments.succeeded and payments.failed for the payments (samples)Batch COMPLETED. Each payment COMPLETED or FAILED.
A payment to 000000002payment-batches.submitted, then payment-batches.rejected, then payments.failed for every paymentBatch FAILED. Every payment FAILED.
  • A failed test payment's resultCode depends on the sender account's bank. For DBS Singapore it is OTHERS.
  • A batch that completes sends no webhook of its own. Check the payments.
  • These payment types have no receiver account number, so a test payment of these types always succeeds: AU_BPAY, MY_JOMPAY and GB_SEPA.
  • A proxy payment (PAYNOW, HK_FPS_PROXY, MY_DUITNOW by proxy) fails only if it also carries a test bankAccountNumber, which some banks' rules do not allow.
  • Test mode has no maintenance windows, so it never answers 503.

For other test values, such as for single payments and direct debit, see Test mode values.

On this page