Handle response files
Most partners answer a batch file with a file of their own that says what became of each payment. The platform stores that reply, then calls your response hook to read it and settle the payments. You switch this on with handleResponseFunction and responseVia in fileConfig (see Configure file batching). A reply for a module without handleResponseFunction is refused, because nothing could ever read it.
How the reply reaches the platform
responseVia picks one of two routes. Either way, the platform only stores the reply when it arrives. It reads nothing until your response hook runs.
http_response: the reply comes back on your own request
http_response: the reply comes back on your own requestThe partner answers your delivery request with the reply. Return its body from your submission hook as move_to_inbox, next to the move_to_outbox for the file.
export const submitPayments = async ({ payment_batch_id, payments }) => {
const file = buildFile(payments);
const references = Object.fromEntries(payments.map((p) => [p.payment_id, referenceFor(p)]));
const response = await sendToPartner(file);
return [
{
name: 'move_to_outbox',
content: file,
file_name: `ACMEPAY-20260928-${payment_batch_id.slice(0, 8).toUpperCase()}.txt`,
references,
},
{
name: 'move_to_inbox',
content: await response.text(),
file_name: `ACMEPAY-20260928-${payment_batch_id.slice(0, 8).toUpperCase()}-REPLY.txt`,
},
];
};| Field | Type | Description |
|---|---|---|
content | string | The reply exactly as the partner sent it. Required. |
file_name | string | The name the reply is stored under. Optional. |
Always return
move_to_inboxtogether withmove_to_outbox. A reply is about a file, so on a run
that stored no file the platform dropsmove_to_inbox.
webhook: the partner posts the reply later
webhook: the partner posts the reply laterThe partner sends the reply to your module's batch response endpoint.
POST https://api.rootplatform.com/v1/insurance/collection-modules/{collection_module_token}/batch-responses
Content-Type: text/plain
X-File-Name: ACMEPAY-20260928-REPLY.txt
<the reply file>| Part | Description |
|---|---|
collection_module_token | A collection module token for your module. It is the only thing that authenticates the request, so treat the full URL as a secret. |
Content-Type | Any type that is not a form, such as text/plain or application/octet-stream. A request with no Content-Type arrives empty. |
X-File-Name | Optional. The name the reply is stored under. Up to 255 characters, starting with a letter or a digit, then only letters, digits, ., - and _. |
| Body | The reply file as UTF-8 text, up to 30 MB. |
The platform answers 202 Accepted once the reply is stored. It answers 400 when the body is empty, is not valid UTF-8, or X-File-Name breaks the rules above. The stored copy is byte for byte what the partner sent, byte-order mark included.
Which batches the reply is offered to
A reply does not say which batch it belongs to, so the platform offers it to every batch that could be waiting for one. That is each batch from your module that:
- was delivered in the last 30 days,
- is
submitting,submitted,acceptedorfailed, and - is not archived.
The newest 25 are offered, newest first. Your hook runs once for each of them, and each run sees only that batch's payments and references. That is what stops a reply settling a payment in the wrong batch.
So most runs are about a batch the reply does not mention. For those, return an empty array. That is the normal answer, not an error.
Archive each batch once its reply has settled it. An archived batch stops being offered replies. If
more than 25 batches are waiting at once, the older ones are not offered the reply at all, and the
platform logs a warning.
If no batch is waiting when a reply arrives, the reply fails and is retried. This covers a reply that beats its batch's delivery stamp by a moment. A reply that still matches nothing goes to a person, because it is either too old or not for this module.
The response hook
The platform calls the function you named in fileConfig.handleResponseFunction, once per batch the reply is offered to.
export const handleResponseFile = async ({ file, references }) => {
// references maps payment id to the reference you wrote. Flip it to look up a line.
const paymentIdFor = Object.fromEntries(
Object.entries(references).map(([paymentId, reference]) => [reference, paymentId]),
);
const actions = [];
for (const line of parseReply(file.content)) {
const paymentId = paymentIdFor[line.reference];
if (!paymentId) continue; // This line belongs to another batch.
actions.push(
line.paid
? {
name: 'update_payment',
payment_id: paymentId,
status: 'successful',
external_reference: line.transactionId,
}
: {
name: 'update_payment',
payment_id: paymentId,
status: 'failed',
failure_reason: line.reason,
failure_code: line.code,
failure_action: 'allow_retry',
},
);
}
return actions;
};What you are given
| Field | Type | Description |
|---|---|---|
file | object | The reply. See below. |
payments | array | The payments on this batch. |
references | object | A map of payment id to the reference you returned in move_to_outbox. A payment with no reference is left out. |
organization | object | The organization the batch belongs to. |
environment | string | The environment the run is in. |
file carries the fields below.
| Field | Type | Description |
|---|---|---|
name | string | The name the reply was stored under, with a timestamp in front. |
location | string | Where the platform stored the reply. Useful in your log lines. |
content | string | The reply exactly as the partner sent it. |
received_at | string | When the platform received the reply, as an ISO 8601 timestamp. |
Each entry in payments carries the fields below.
| Field | Type | Description |
|---|---|---|
payment_id | string | The platform's id for the payment. |
policy_id | string | The policy being collected on. |
amount | number | The amount to collect. |
currency | string | The currency of the amount. |
status | string | The payment's status right now. |
As with the batch file, the platform never opens the reply. Its layout is between you and your partner.
Report what the reply says
Your hook returns an array of actions. Three are accepted from this hook, and they run in the order you return them.
| Action | Return it when | What the platform does |
|---|---|---|
update_payment | The reply gives an outcome for a payment | Settles the payment |
move_to_archive | You are done with the batch's file | Copies the stored batch file into a dated archive folder |
update_payment_batch | The reply says something about the batch | Moves the batch to accepted, failed or archived |
Inside update_payment
update_payment| Field | Type | Description |
|---|---|---|
payment_id | string | The payment to settle. Required. It must be a payment on this batch. |
status | string | successful, failed or processing. Required. |
failure_reason | string | Why the payment failed, in words. |
failure_code | string | The partner's code for the failure. |
failure_action | string | What happens next: allow_retry, block_retry or block_payment_method. |
external_reference | string | The partner's own id for the transaction. |
customer_reference | string | The reference the customer sees. |
Use processing when the partner has the payment but has not decided yet, and settle it from a later reply. Settling a payment does everything a settled payment normally does: the ledger entry, retries, payment events and webhooks, and customer notifications.
Moving the batch
A batch can only move forward, along these steps.
| From | To |
|---|---|
submitted | accepted, failed or archived |
accepted | archived |
failed and archived are final. To close a batch, return its actions in this order: the update_payment actions, then move_to_archive, then update_payment_batch with archived.
move_to_archive and update_payment_batch act on the batch in scope by default. You may pass a payment_batch_id to name another batch, as long as it belongs to your module.
Repeats, retries and rejections
Replies get resent, and messages get delivered twice. The platform is built so that neither settles anything twice.
- A reply is applied to a batch once. The platform records the outcome of each reply against each batch:
settled,unresolvedwhen your hook returned nothing, orrejected. A batch that already has an outcome for a reply is skipped. - A repeated outcome changes nothing. A payment already at the status you report is left alone, so a partner resending the same reply is safe.
failedon asuccessfulpayment is a reversal. The platform creates a reversal and a ledger debit. Report it only when the partner has really reversed the payment.- A hook that throws is retried. Nothing is recorded, so the next attempt finds the batch still unsettled. Throw for a problem that might clear, such as a timeout calling your partner.
- An action the platform refuses rejects the whole list. A badly shaped action, a payment that is not on this batch, or an action this hook may not return all count. Nothing in the list is applied, and the reply is recorded as
rejectedfor that batch. It is not retried, because the same reply would get the same answer, so a person has to pick it up.
Settle only what the reply says. A line you cannot match, or cannot read, is not a failure. Leave
that payment out and let a later reply, or a person, settle it. Reportingfailedfor a payment the
partner actually collected tells the customer they were not charged when they were.
Updated about 5 hours ago