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.
{
"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" }
}
}
]
}amountis in minor units:20000is 200.00 SGD. See Minor units format.senderAccountCurrencyis required only when the sender account holds more than one currency in Acme.currencyon a payment is optional and defaults to the batchcurrency. OnlyTT,US_WIRE_INTL,ACTandBKTRpayments can use another currency.customerReferenceis optional. Acme generates one when you leave it out.- For a
PAYNOWor other proxy payment, sendreceiver.proxyTypeandreceiver.proxyValueinstead 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.
{
"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:
| Field | What it holds |
|---|---|
batchId | The batch the payment belongs to. |
paymentDate | The payment date you sent. |
actualPaymentDate | The date the bank used, when the bank changed it (for example after its cutoff). |
bankChargeAmount and bankChargeCurrency | The 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,senderAccountIdandcurrency, 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
amountand areceiver. The maximum amount depends on the bank and the payment type. - Give
bankChargeBearer, when sent, asSENDER,RECEIVERorSHARED.
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.
| Status | What it means | What to do |
|---|---|---|
400 | The 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. |
401 | Empty 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. |
403 | The API key does not have the Batch Payments permission. | Contact Acme. |
409 | Empty 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. |
429 | errorCode 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. |
500 | An 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>.
{
"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"
}
]
}paymentErrorsis empty when only batch fields failed, for example a missingpaymentDate.paymentErrorsis left out when the body could not be read, or the sender account, its currency or the payment type is not valid.errorMessagethen says what to fix, for exampleSender 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.bankAccountNumber | What happens |
|---|---|
000000000 or 000000001 | This payment is FAILED. The other payments in the batch still succeed. |
000000002 | The bank rejects the whole batch. The batch is FAILED, and so is every payment in it. |
| Any other number | The payment is COMPLETED. |
What you receive
| Batch | Webhooks, in order | Final status |
|---|---|---|
No 000000002 | payment-batches.submitted (sample), then payments.succeeded and payments.failed for the payments (samples) | Batch COMPLETED. Each payment COMPLETED or FAILED. |
A payment to 000000002 | payment-batches.submitted, then payment-batches.rejected, then payments.failed for every payment | Batch FAILED. Every payment FAILED. |
- A failed test payment's
resultCodedepends on the sender account's bank. For DBS Singapore it isOTHERS. - 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_JOMPAYandGB_SEPA. - A proxy payment (
PAYNOW,HK_FPS_PROXY,MY_DUITNOWby proxy) fails only if it also carries a testbankAccountNumber, 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.