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

The 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`,
    },
  ];
};
FieldTypeDescription
contentstringThe reply exactly as the partner sent it. Required.
file_namestringThe name the reply is stored under. Optional.
ℹ️

Always return move_to_inbox together with move_to_outbox. A reply is about a file, so on a run
that stored no file the platform drops move_to_inbox.

webhook: the partner posts the reply later

The 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>
PartDescription
collection_module_tokenA collection module token for your module. It is the only thing that authenticates the request, so treat the full URL as a secret.
Content-TypeAny type that is not a form, such as text/plain or application/octet-stream. A request with no Content-Type arrives empty.
X-File-NameOptional. The name the reply is stored under. Up to 255 characters, starting with a letter or a digit, then only letters, digits, ., - and _.
BodyThe 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, accepted or failed, 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

FieldTypeDescription
fileobjectThe reply. See below.
paymentsarrayThe payments on this batch.
referencesobjectA map of payment id to the reference you returned in move_to_outbox. A payment with no reference is left out.
organizationobjectThe organization the batch belongs to.
environmentstringThe environment the run is in.

file carries the fields below.

FieldTypeDescription
namestringThe name the reply was stored under, with a timestamp in front.
locationstringWhere the platform stored the reply. Useful in your log lines.
contentstringThe reply exactly as the partner sent it.
received_atstringWhen the platform received the reply, as an ISO 8601 timestamp.

Each entry in payments carries the fields below.

FieldTypeDescription
payment_idstringThe platform's id for the payment.
policy_idstringThe policy being collected on.
amountnumberThe amount to collect.
currencystringThe currency of the amount.
statusstringThe 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.

ActionReturn it whenWhat the platform does
update_paymentThe reply gives an outcome for a paymentSettles the payment
move_to_archiveYou are done with the batch's fileCopies the stored batch file into a dated archive folder
update_payment_batchThe reply says something about the batchMoves the batch to accepted, failed or archived

Inside update_payment

FieldTypeDescription
payment_idstringThe payment to settle. Required. It must be a payment on this batch.
statusstringsuccessful, failed or processing. Required.
failure_reasonstringWhy the payment failed, in words.
failure_codestringThe partner's code for the failure.
failure_actionstringWhat happens next: allow_retry, block_retry or block_payment_method.
external_referencestringThe partner's own id for the transaction.
customer_referencestringThe 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.

FromTo
submittedaccepted, failed or archived
acceptedarchived

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, unresolved when your hook returned nothing, or rejected. 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.
  • failed on a successful payment 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 rejected for 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. Reporting failed for a payment the
partner actually collected tells the customer they were not charged when they were.


Did this page help you?