Batch payments let a back-office operator send many payments in one go, drawing on the accounts of any customers they are responsible for. It is a multi-step flow across two endpoints — you verify the items on one, then initiate and submit on the other.
The two are separate so that preparing a batch and paying one can be granted independently: an operator can be allowed to prepare and check a batch without being able to move the money. Items can be sent as JSON, or you can post a CSV or pain.001 file and let Orenda parse it — see Sending a file.
These are the multi-customer forms — no customerId in the path. Each item’s payer account is resolved globally and authorised per item against you: an account you are not responsible for, including an unassigned one, is rejected with 401. A back-office role that resolves to no customers is rejected the same way.Single-customer variants exist at /v1/customers/{customerId}/batch-payments/… and behave identically apart from being pinned to that one customer.

Access

Verifying a batch and paying one are gated separately, so an operator can be given the ability to prepare and check a batch without the ability to pay it. Orenda configures that when your role is set up.
This is not a complete separation of duties today. The older unsuffixed batch routes remain available and accept every action, including submit, and the access that lets an operator read batches also reaches those routes — so an operator who can read batches can still pay through the older route. Closing that is follow-up work. If you need a strict prepare-but-cannot-pay role, talk to us before relying on it.

The flow

  1. Verify — send the items. Each is validated and the payee name is checked (Confirmation of Payee in the UK, Verification of Payee in the EU). You get a requestId and a batchId, and a draft batch is created.
  2. Check the result — poll GET /v1/batch-payments/verify/{requestId} until it is no longer PROCESSING. Each row comes back valid or invalid, with fees, the payee name-check response, and the source account’s live balance.
  3. Fix and re-verify (optional, repeatable) — send the corrected items back to /verify with the same batchId. The draft is updated in place; you do not accumulate one draft per edit.
  4. InitiatePOST /v1/batch-payments/submit with action: "initiate" and the batchId. Returns the total, the currency, and an scaChallenge.
  5. Submit — the same route with action: "submit", the batchId, and a confirmation carrying the challenge. Returns the batchId you already hold.
  6. TrackGET /v1/batch-payments/{batchId} to watch each item move through QUEUEDSUCCESS or FAILED.

Sending a file

If your system already produces a payment file, send it as-is instead of mapping it to payments yourself. verify accepts a file in place of the array:
Send either payments or file, never both: a request carrying two versions of what to pay is refused rather than resolved by precedence. The response is identical either way — a requestId and a batchId — and so is everything after it. A file is parsed into exactly the items you would have sent by hand, then verified, drafted, initiated and submitted through the same path. From step 2 onward there is no difference at all.
There is no separate upload endpoint and no size negotiation. A batch is capped at 1000 payments, which puts a full file at roughly a megabyte of text — comfortably inside the request limit. Send it inline. content is capped at 4,000,000 characters; a file over that, or one holding more than 1000 payments, is rejected with 400 before it is parsed.
A file is accepted on the /verify route only, and only for verify. The older unsuffixed batch routes do not take one — they are live and being retired, so they stay frozen. Sending items as payments still works everywhere it did before.

What we accept

pain.001 — the ISO 20022 customer credit transfer initiation message. We do not read the namespace, so .03 through .09 all parse on the strength of their element names; treat anything outside that range as untested rather than supported. Namespace prefixes, BIC and BICFI, and all three ReqdExctnDt forms — bare, Dt and DtTm — are handled. One PmtInf block may carry many payments or exactly one; its payer and execution date apply to every payment inside it. We read Cdtr/Nm, the creditor IBAN (or Othr/Id for a domestic account number), CdtrAgt BIC and — for UK payments — the sort code from CdtrAgt/FinInstnId/ClrSysMmbId/MmbId. That member id is treated as a sort code only when ClrSysId/Cd says GBDSC, or when no scheme is named and the payee has no IBAN. FinInstnId is a sequence, so a SEPA file may legitimately carry a BIC, an IBAN and a national clearing id together — a German export names DEBLZ with an 8-digit Bankleitzahl, which is not a sort code and is not read as one. The reference comes from RmtInf/Ustrd, joined if it repeats, falling back to RmtInf/Strd/CdtrRefInf/Ref. EndToEndId is carried through, subject to the length and charset check below. Elements we do not need — ChrgBr, PmtMtd, BtchBookg, CtrlSum — are ignored, not rejected.
camt.053 is not a payment file. It is a statement format: it reports what has already happened on an account and carries no instruction to pay anyone. Sending one is rejected. The file you want is pain.001.
CSV — the published batch template. The header row is found by looking for the Payee Name column, so title rows above it are fine. Quoted fields are honoured, so a payee name containing a comma will not shift the rest of the row.
Amount must be dot-decimal (1234.56) and must not contain an unquoted comma. A decimal comma (92,34) or a thousands separator splits the cell in two and shifts every later column — quote the field if it has to contain one.Columns are matched by name, so we cannot see a positional shift directly. Any row whose column count differs from the header raises a warnings entry naming the row — read those before submitting, because a shifted amount can otherwise verify clean at the wrong value.
Scheduled Date must be YYYY-MM-DD or YYYY/MM/DD; a single-digit month or day is padded for you. Anything else — 01/09/2026, n/a, a free-text note — is passed through untouched and comes back as an invalid row rather than being guessed at, because 01/09 is a different day in different countries and guessing moves money on the wrong one. An empty cell is fine and means “send it now”.

How rows are treated

A file is translated, not judged. Every row in it becomes a payment, and validation happens where it always has — in the verification step you poll for. That matters most when a row is wrong. A payee with no account number does not disappear from the batch; it comes back in invalidPayments with the reason. You always get out as many rows as you put in, so a total you can reconcile against your own file. A few behaviours worth knowing before you send a file that was sitting on a queue for a while:
  • A past ReqdExctnDt is kept, not corrected. We will not quietly move the date to today and pay it — a date nobody chose is worse than a rejection you can see. But be precise about where you will see it: the draft’s expiresAt is min(now + 7 days, end of the earliest scheduled date), so a past date makes it already elapsed on the verify response itself. The draft is unusable from the moment it is created, and initiate/submit will then fail on draft expiry — an error that does not mention the date. If a 200 from verify comes back with an expiresAt in the past, a stale execution date is why.
  • InstdAmt/@Ccy is not applied. Payments settle in the currency of the payer account. The instructed currency is read only to warn you when one file mixes several.
  • EndToEndId is checked against what the rails accept — 2 to 35 characters, and a restricted character set. An id outside that is dropped, with a warning, rather than rewritten: it is your reconciliation handle, so a replacement we invented would be worse than none. Re-uploading the same export reuses its ids, which a provider may reject as duplicates — change them if you are re-sending a corrected file.

Warnings

When the parser could not carry something across, the verify response includes a warnings array alongside requestId and batchId:
These are not row rejections — the payments are still queued for verification. They tell you what the file said that we could not use. The field is absent when there is nothing to report, so a clean file’s response is exactly what a JSON one returns.

Why the payload is sent more than once

verify, initiate and submit each carry the full payments array. That is deliberate: the operator can edit rows between steps, so the batch is never assumed to be unchanged. The batchId is what ties the calls together — it identifies which batch you are working on, not what it contains. The draft is an anchor, not a copy: it records that a batch is prepared and awaiting submission, and points at the verification payload. The rows you finally submit are the ones that get paid. This is also why a file is accepted at verify only. initiate and submit take payments, and what you send them is the validPayments you polled for — the rows that came back checked, minus anything you chose to drop. Re-sending the file there would mean paying rows that never passed verification.

Listing batches

Returns the batches you can see, newest first, paginated by limit and nextToken. Keep paging while nextToken is present — a short or empty page is not the end of the list, because abandoned FUNDING_FAILED batches are filtered out inside the query.

Item counts and totals

Add includeItemSummary=true and every batch carries itemsStatus — how many of its payments are completed, pending, processing and failed — and totalAmount, the sum of the item amounts. Leave it off and both fields are absent. It is opt-in for every caller, back-office included, and open to customer callers on their own batches: the summary is a read of the batch’s own data, not a privilege. It is opt-in because the counts are computed from the item rows: each batch on the page is read in full, so a large page of large batches is a lot of reading. Ask for the summary on the page sizes you actually display, and a list you only want the ids from pays nothing. Only the exact string true turns it on. 1, TRUE and yes all leave the summary out, so a typo omits a field rather than silently changing the shape of the response.
customerName and custodianName are a separate enrichment and remain back-office only. A customer asking for the summary gets their own batch totals, not the custodian names attached to them.

Drafts and expiry

A draft is created at verify and lives until it is paid or expires. expiresAt is 7 days, or the end of the earliest scheduledDate in the batch, whichever comes first. That second bound matters: a batch containing a scheduled date that has passed is rejected at submit as a whole, not row by row, so such a draft is already unusable — expiring it makes that visible rather than a surprise at payment time. An expired draft still appears in the list, marked EXPIRED, because the record that a batch was prepared and never sent is worth keeping. It can no longer be submitted, and opening it returns the draft without its payload — the verified items are held on their own retention clock. To recover one, verify the items again; that creates a new draft with a fresh batchId.

Finding drafts

Lists batches that have been verified but not yet paid, newest first, with the same pagination as the normal listing. Omit drafts and you get submitted batches exactly as before — a draft never appears in the default list. A draft appears here as soon as verification is requested, not when it finishes. Opening one before the check completes returns the draft with payloadStatus: "PENDING" and no payments — see below. Each entry gives the batchId to submit with, a status of DRAFT or EXPIRED, paymentsCount, expiresAt and createdDateTime. Drafts have no item rows until they are paid, so itemsStatus and totalAmount are absent — not zero, which would render a £50,000 draft as a £0.00 one, and they stay absent even with includeItemSummary=true. paymentsCount is what a draft has instead. Open one with GET /v1/batch-payments/{batchId} — for a draft the response carries a draft object holding the verified payments, split into validPayments and invalidPayments, so a reviewer can see exactly what was checked before deciding to pay. Alongside them, payloadStatus says why payments is or is not there: AVAILABLE when it was read, PENDING while verification is still running (or if it failed), and UNAVAILABLE once the draft has expired and the payload has aged out.
Listing drafts is a read, so it is scoped more broadly than paying one: a program-wide back-office role sees every draft in the program. Otherwise you see the drafts prepared through the back-office route by your own team — not everything belonging to the customers you are responsible for. A draft a customer prepared for themselves through the customer route stays with that customer, and opening a draft you do not own is refused as if it did not exist.
A draft entry carries the same programId as a submitted batch, so both map onto one client type. Note that the attribute is not stored on the draft row itself — writing it would place drafts in the ordinary batch index — so it is filled in on the way out. This is what makes the two-person flow work without passing ids around outside the API: one operator verifies, another finds the draft in this list and pays it.

Withdrawing a draft

Takes back a batch that was prepared and then thought better of. The draft is removed — it stops being listable and openable at once — and the verified payload behind it, the payee names, accounts and amounts checked at verification, is deleted with it. Drafts only. A batch that has been submitted has money in flight behind it and is not deletable through this call; it returns 409 with BATCH_NOT_A_DRAFT. An expired draft can be deleted — that is how lapsed drafts are cleared out of the list. Not idempotent. Once the draft is gone there is nothing left to recognise, so a repeated delete returns 404, not a second success. Treat the first 200 as the answer. The 409 comes in two flavours and they call for opposite reactions: BATCH_NOT_A_DRAFT means the batch was submitted — terminal, never retry — while BATCH_DELETE_CONFLICT means it was re-verified while your delete was in flight, so re-read it and try again. A withdrawn batchId is free to be used again: verifying with it creates a new draft under that id. A submit already in flight for the old one still fails — the draft it needs is gone.
Deleting is scoped exactly like opening: you can withdraw a draft you can open, and no more. A program-wide back-office role can withdraw any draft in the program; otherwise you are limited to the drafts prepared through the back-office route by your own team. A draft you do not own is refused as if it did not exist — a 404, never a 403.The permission is its own — accounts.payments.batchDelete, not the accounts.payments.batch that every read in this surface sits behind — so a role can be given the whole batch view without the ability to take back someone’s prepared work.

Step-up authentication

submit requires a confirmation — a passkey, a 2FA code, or a PIN on programs that support it. The challenge is issued against your own signed-in identity, never the customer being paid: an operator paying on behalf of a customer authenticates with their own passkey, and the assertion is bound to that identity. For back-office callers the credential is resolved against the shared back-office pool rather than the program in the URL — which means a back-office passkey ceremony always resolves to production, even for a sandbox batch. Send the Origin header on the challenge request; it is carried into the WebAuthn ceremony and a mismatch fails verification.

Errors worth expecting