# 09 · Reconcile

  Approved is not paid, and a facility with no approved bank record is paid nothing.

  Both calls on this page — registering a callback and polling remittances — are unregistered in
  the service today. There is no reconciliation surface behind the gateway yet.

  Read this stop now anyway, because what it describes is the work you will have to do and the
  shape it will take, and because the mistakes below are made at design time rather than at
  integration time. Callback endpoints, meanwhile, are registered per tenant at
  `/api/v1/tenants/{tenant_id}/endpoints`, which is live — see
  [00 · Authenticate](/rail/stops/Authenticate).

## Why this stop exists

This is the stop integrations most often leave until it is urgent. An approved claim is a
decision, not a payment. The money arrives separately, in batches, on a schedule of its own,
and matching it back to what you billed takes real work.

It is also where "we have been live three weeks and no money has arrived" gets answered. The
answer is usually banking rather than claims.

## Register a callback once

<ApiRef method="POST" path="/api/v1/callbacks" to="/rail-api/09-reconcile">
  One registration covers preauth decisions, claim decisions and remittance postings.
</ApiRef>

Register it at Authenticate, before you need it. Claim submission will not proceed without a
`claim.decision` registration.

## The remittance event

```json
{
  "event": "remittance.posted",
  "remittance_id": "RMT-2026-08-0091",
  "batch_ref": "BATCH-2026-08-19",
  "posted_at": "2026-08-19T14:00:00+03:00",
  "claims": [
    {
      "claim_id": "CLM-2026-081234",
      "amount_paid": { "amount": 1200, "currency": "KES" },
      "status": "paid"
    }
  ]
}
```

Upstream, payers post remittances on their own schedules and in their own batch shapes. You
receive one event shape. Treat the batch reference as the idempotency key. A batch can be
retried, and receiving one twice must not pay you twice in your own ledger.

## Poll to close gaps

<ApiRef method="GET" path="/api/v1/remittances" to="/rail-api/09-reconcile">
  Paginated remittances since a timestamp. The body is the same as the callback, so you can reconcile with it after any webhook gap.
</ApiRef>

Webhooks tell you sooner, but they do get missed. Run a scheduled poll as well, so a missed
delivery does not become a month-end problem.

## Match on the invoice, never on the money

Getting this wrong causes more rework than anything else at this stop.

Match a remittance to the **provider invoice reference**. Do not match on member, amount or
date, because all three collide and amounts rarely equal what you billed once deductions
land. Model **many advices to one invoice**: interim payments are normal, and a single
invoice may be settled across several batches.

<Wire
  rows={[
    { call: "Discount", does: "An agreed reduction that lowers revenue", keep: "posts to revenue reduction" },
    { call: "Member copay", does: "The patient's share, collected by you", keep: "already yours" },
    { call: "Withholding tax", does: "Deducted at source", keep: "reclaimable, not lost" },
    { call: "Rejected amount", does: "Refused, with a reason", keep: "reason code" },
    { call: "Shortfall", does: "Paid less than billed, and none of the above explains it", keep: "posts to suspense" },
  ]}
/>

Discount and shortfall land in **different ledger accounts**. Treating a shortfall as a
discount quietly writes off money you could still recover.

## Why money has not arrived

Ordered by how often each one turns out to be the cause:

<BuildSequence
  steps={[
    "No approved banking record. A human reviews it, and until it is approved nothing pays out however many claims are approved. Start this on day one, alongside the build.",
    "The claim is approved but not yet in a payment batch. Approved is a decision, and batches run on their own schedule.",
    "The claim was returned rather than decided, and nobody resubmitted it. Returned is not the same as rejected. It is waiting for you.",
    "The claim was reconciled against the wrong invoice reference, so the payment did arrive and then sat unmatched in your own system.",
    "The claim timed out. There is a hard time bar from the date of service, and a claim that misses it cannot be revived.",
  ]}
/>

## What breaks here

<CostTable
  rows={[
    {
      where: "Reconcile",
      symptom: "\"We have been live for weeks and nothing has been paid\"",
      cause: "Almost always an incomplete or unapproved banking record, not a claims problem at all.",
    },
    {
      where: "Reconcile",
      symptom: "\"The payment does not match any claim\"",
      cause: "Matching was attempted on member, amount or date. Match on the provider invoice reference.",
    },
    {
      where: "Reconcile",
      symptom: "\"We were paid twice for one batch\"",
      cause: "The batch was retried and ingested twice. batch_ref is the idempotency key.",
    },
    {
      where: "Reconcile",
      symptom: "\"The totals are short and we cannot explain it\"",
      cause: "Deductions were collapsed into one figure. Reconcile the claim total and explain each gap by its own flag.",
    },
  ]}
/>

## You have reached the end of the rail

That is the full journey: your system on the rail, a patient resolved, cover chosen,
consent proved, an encounter opened and closed, a claim assembled and sent, and money
matched back to it.

Every scenario you meet from here is this sequence with different values in it.

**Back to:** [the whole line](/rail/concepts/The-Eight-Steps) · [why it exists](/rail/why/The-Problem)
