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.The flow
- 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
requestIdand abatchId, and a draft batch is created. - Check the result — poll
GET /v1/batch-payments/verify/{requestId}until it is no longerPROCESSING. Each row comes back valid or invalid, with fees, the payee name-check response, and the source account’s live balance. - Fix and re-verify (optional, repeatable) — send the corrected items back to
/verifywith the samebatchId. The draft is updated in place; you do not accumulate one draft per edit. - Initiate —
POST /v1/batch-payments/submitwithaction: "initiate"and thebatchId. Returns the total, the currency, and anscaChallenge. - Submit — the same route with
action: "submit", thebatchId, and aconfirmationcarrying the challenge. Returns thebatchIdyou already hold. - Track —
GET /v1/batch-payments/{batchId}to watch each item move throughQUEUED→SUCCESSorFAILED.
Sending a file
If your system already produces a payment file, send it as-is instead of mapping it topayments 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.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.
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.
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 ininvalidPayments 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
ReqdExctnDtis 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’sexpiresAtismin(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, andinitiate/submitwill then fail on draft expiry — an error that does not mention the date. If a200from verify comes back with anexpiresAtin the past, a stale execution date is why. InstdAmt/@Ccyis 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.EndToEndIdis 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 awarnings
array alongside requestId and batchId:
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
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
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
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.
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
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.