Build and submit the batch file
The platform hands your submission hook a batch of due payments, and your hook sends the partner a file and reports back what it did.
The submission hook
The platform calls the function you named in batching.submitPaymentsFunction, once per batch. Your hook builds the file, sends it to the partner itself, and returns a list of actions describing what happened.
export const submitPayments = async ({ payment_batch_id, payments, organization, environment }) => {
const file = buildFile(payments);
const references = Object.fromEntries(payments.map((p) => [p.payment_id, referenceFor(p)]));
await sendToPartner(file);
return [
{
name: 'move_to_outbox',
content: file,
file_name: `ACMEPAY-20260928-${payment_batch_id.slice(0, 8).toUpperCase()}.txt`,
references,
},
];
};What you are given
| Field | Type | Description |
|---|---|---|
payment_batch_id | string | The batch these payments were grouped into. Use it for your log lines and for building an idempotency key on your own request. |
payments | array | Every payment in the batch. One file covers all of them. |
organization | object | The organization the batch belongs to. |
environment | string | The environment the run is in. |
Each entry in payments carries the fields below.
| Field | Type | Description |
|---|---|---|
payment_id | string | The platform's id for the payment. You need this to build your references map. |
policy_id | string | The policy being collected on. |
amount | number | The amount to collect. |
currency | string | The currency of the amount. |
premium_type | string | What the payment is for. |
billing_period_start | string | The first day of the period the payment covers. |
billing_period_end | string | The last day of that period. |
policyholder | object | The policyholder the partner will debit. |
policy | object | The full policy. |
payment_method | object | The method the payment was created against. |
payment_methodis the method the payment was created against, which is not always the one on the
policy today. Moving a policy to a new method does not move payments that already exist, so read
the banking details off this field rather than offpolicy.
Build the file
The platform never opens your file. Fixed width, CSV, JSON, XML, whatever your partner asked for, you build it and you send it. Nothing on the platform validates its layout, so your partner's spec is the only spec.
Delivery is yours too. Your hook makes its own HTTPS request to the partner, and the platform does not deliver anything on your behalf.
Write a reference for every payment
Your file needs an identifier per line that the partner will quote back to you in its reply. The references map tells the platform which identifier you used for which payment.
references: {
'8a4c3f1e-...': 'ACME-000481',
'2b9d7e5c-...': 'ACME-000482',
}The platform stores each reference on its payment, then hands the whole map back to your response hook when the reply arrives. That is the only thing tying a line in the partner's reply to a payment on the platform.
Give every payment in the batch a reference. A batch missing even one cannot be settled from the
reply, and because the partner already holds the file, the platform will not record those payments
as failed either. It raises an alarm and waits for a person.
Report what you sent
Your hook returns an array of actions. Five are accepted from this hook.
| Action | Return it when | What the platform does |
|---|---|---|
move_to_outbox | The partner has the file | Stores the file, stamps each payment's reference, records the batch as delivered, and reports every payment as submitted |
update_payment_batch | You want to move the batch, or record that it failed | Sets the batch to submitted, accepted, failed or archived |
move_to_archive | After the file is delivered | Copies the stored file into a dated archive folder |
move_to_inbox | The partner replied inside your own delivery response | Stores the reply and queues it for your response hook |
release_batch_for_retry | Your request never left your module | Returns the batch to pending, so it is submitted again |
Inside move_to_outbox
move_to_outbox| Field | Type | Description |
|---|---|---|
content | string | The file exactly as you sent it. |
file_name | string | The name the file is stored under. |
references | object | A map of payment id to the reference you wrote for that payment. |
Although file_name is optional in the contract, the first move_to_outbox on a batch has to carry one, because nothing else can supply it. Once a batch has a name, that name is pinned and later actions cannot change it. A retry that sends a different name is stored under the pinned one, and the platform logs the difference.
Choose the right action for what happened
This is the part to get right. The platform cannot see your request, so your actions are the only account of it, and the wrong one debits people twice.
| What happened | Return | Result |
|---|---|---|
| The partner accepted the file | move_to_outbox | The payments are reported submitted and wait for the reply |
| The partner rejected the file | update_payment_batch with failed, and no move_to_outbox | The payments settle under the reason you gave |
| Your request never left your module | release_batch_for_retry, on its own | The batch returns to pending and is submitted again |
| You cannot tell | Neither | The batch stays where it is and a person establishes what the partner holds |
Return
release_batch_for_retryonly when you can show your request never reached the partner: a
refused connection, a DNS failure, or a TLS failure before the request was written. A timeout, a
connection dropped mid-request, or any status code at all means the partner may hold your file.
Releasing a batch the partner already has sends the file a second time and debits every payment in
it twice.
A partner that rejects the file is a failure, not a retry. Report it with update_payment_batch and
a failed status, leave move_to_outbox out, and the payments settle under the reason you gave.
What the platform does with your actions
move_to_outbox runs first, because everything else is about a file that already exists. The rest run afterwards, in a fixed order: the archive copy, then the inbox reply, then the batch status. The order is the platform's, not the order you returned them in.
Once your file is stored, the platform stamps the batch as delivered, pins the file name, writes each reference onto its payment, and reports all of the payments as submitted. Your batch status is yours to move, so the platform leaves it alone until your update_payment_batch runs.
move_to_archiveandmove_to_inboxboth need a stored file, so they are ignored on a run that
staged none. A failure while filing the archive copy never fails the payments. The file is with
the partner either way, and the archive is the platform's own record rather than the partner's.
When the platform stops and calls a person
Three returns leave the platform holding two facts it cannot reconcile. In each case it raises an
alarm and settles nothing, because guessing would be a false statement about money.
- You returned
move_to_outboxtogether with afailedstatus. One says the partner has the file, the other says the batch did not take. Report a rejection withoutmove_to_outboxinstead. - You returned
move_to_outboxwithout a reference for every payment. The partner will debit them and no reply can be matched back. - A run picked up a batch already sitting in
submitting. A previous run may have had the file on the wire, and nothing left in the system can tell an unsent request from an unacknowledged one.
Returning nothing at all ends the batch instead. Your hook ran and answered, so the platform takes that as an answer, and records the batch as failed rather than calling your hook again.
Next step
Handle response files covers the reply: how it reaches the platform, and how your response hook
settles the payments.
Updated about 2 hours ago