# 08 · Submit

  Pre-flight should hold no surprises. Every check it runs has been running since you added the
  first line.

## Why this stop exists

Two things happen here, in order, and they get conflated: the visit closes, then the claim is
sent. Closing needs consent from the patient. Sending needs the claim to be complete.

## Close the visit with consent

A visit closes the way it opened, with consent. Discharge is authorised in its own right
rather than treated as a formality.

Two pre-discharge checks run before the closing consent is even offered. Every unretired line
must have an active reservation, and where the visit was opened from a card swipe, the
authorisation has to trace back to a live session rather than to some other factor.

<ApiRef method="POST" path="/api/v2/patients/{unique_patient_id}/authentications" to="/rail-api/03-consent" keep="closing_authorization_ref">
  Ask for the closing authentication. It is the same call as visit open, with purpose set to close
  the visit. See [03 · Consent](/rail/stops/Consent#what-you-send-and-what-you-get-back).
</ApiRef>

Closing is a purpose of its own. A proof taken to open the visit can never satisfy it, however
strong and however recent it is, so there is nothing to reuse here and no shortcut worth hunting
for. The reason for that rule, and what it prevents, is at
[one proof, spent more than once](/rail/stops/Consent#one-proof-spent-more-than-once).

`closing_authorization_ref` travels onto the claim.

  Neither `/api/v1/visits/{visit_id}/claim/preflight` nor `/api/v1/visits/{visit_id}/claim/close` is
  registered. The registered preflight, `POST /api/v2/visits/{visit_ref}/preauths/preflight`, checks
  a preauthorisation rather than a claim, and closing exists as `POST /api/v1/claims/close` and
  `POST /api/v1/visits/{visit_id}/close` — different calls with different bodies, not renames of
  these. Read the gates below as the rule; do not wire the paths yet.

## Pre-flight: the six gates

<Wire
  rows={[
    { call: "Reservation exists", does: "Every billed line traces to a reservation", keep: "reservation_id per line" },
    { call: "Amount ≤ reserved", does: "No line bills more than was held for it", keep: "—" },
    { call: "Lines mapped", does: "Every code has an unbroken path to a payer revenue code", keep: "mapping" },
    { call: "Attachments present", does: "Every requiredDocuments entry has a matching document", keep: "document keys" },
    { call: "Diagnosis present", does: "Exactly one primary diagnosis is recorded, each linked to the intervention it justifies. This is recorded at 05 · Treat rather than assembled here", keep: "icd11_code" },
    { call: "Dates within cover", does: "Visit dates fall inside the cover's validity window", keep: "—" },
  ]}
/>

<ApiRef method="POST" path="/api/v1/visits/{visit_id}/claim/preflight" to="/rail-api/08-submit">
  Run every gate and return all failures at once, each with a route to its fix.
</ApiRef>

Every reason carries `fix_stage` and `fix_label`. Use them to send the operator straight to the
screen that can resolve the problem, rather than showing a list and leaving them to hunt.

Severity matters. A `blocker` stops submission and a `warning` does not, but a warning cannot
ride through unexamined either, so it persists until someone confirms it.

## Close and submit

<ApiRef method="POST" path="/api/v1/visits/{visit_id}/claim/close" to="/rail-api/08-submit">
  Re-run every gate, then submit. Idempotent on external_id.
</ApiRef>

There is one endpoint, whichever payer the claim is destined for. The dispatch decision was made
back at eligibility when the cover was chosen and has been carried on every line since, so you
never select a different endpoint per payer.

Submission needs somewhere to deliver the decision, and that is a precondition, not a nicety. If
there is no registered `claim.decision` endpoint and no `X-Callback-Url` header on the request,
you get `CALLBACK_NOT_REGISTERED`. Register the default once at
[00 · Authenticate](/rail/stops/Authenticate) and you will never see the check. Use the header
when one particular claim's outcome belongs somewhere else.

## Six kinds of claim, and eleven states

Two counts are worth correcting, because a state machine built to the wrong one leaves cases with
no branch.

There are six claim types, not four. Ambulance is live and behaves differently from everything
else, and oncology is live for named facilities rather than still in development. Treat the type as
data on the claim and switch on it, rather than assuming one shape.

The authorisation moves through eleven states, not four. These are the states of the
*authorisation* that opened and closed the visit, a different enum from the twelve a
[preauthorisation](/rail/stops/Preauth#the-states) moves through, and the two are easy to confuse.
Anything modelled on "pending, approved, rejected, done" will meet states it quietly drops. Read
the enum from the reference, and when a value arrives that you do not know, hold the claim instead
of discarding it.

  How long you have to submit is one of the numbers our own sources disagree about, and the two
  candidates are far enough apart to imply different products: a monthly reconciliation habit, or a
  daily worklist with alerting. Read [time limits](/rail/reference/Time-Limits), keep the value in
  configuration, and show the deadline on the claim rather than enforcing it silently.

Missing documents have their own terminal deadline. A claim returned for documents can be
resubmitted, but only inside a window, after which it is rejected and cannot be resubmitted at all.
It is the one deadline where nothing can be recovered afterwards, so it belongs on a worklist of its
own rather than in a general queue.

## After it lands

Submissions are reviewed in batches with rules doing part of the work, but anything with an issue
goes to a person. Expect decision latency to reflect that.

Decisions are per line and can be partial:

```json
{
  "event": "claim.decision",
  "claim_id": "CLM-2026-081234",
  "decision": "partially_approved",
  "lines": [
    { "line_ref": "lin_007", "decision": "approved", "approved_amount": { "amount": 1200, "currency": "KES" } },
    { "line_ref": "lin_013", "decision": "rejected", "reason": "Tariff not covered at this facility level." }
  ]
}
```

A claim can also come back **returned**, meaning sent back with feedback for correction rather
than rejected. It keeps its `claim_id` and is resubmitted under the same `external_id`. A
resubmitted bill may not exceed the original.

## Documents are checked in two places

The required list is per procedure, and it splits across two moments: some documents belong to the
preauthorisation and some to the claim. An inpatient submission needs a discharge summary and a final
bill; maternity needs only the discharge summary. Collecting the right documents at the wrong stage
still fails here.

Two practical rules. Uploads must be idempotent, so a retry does not create a second attachment.
PDFs must be sent with the correct content type, because the failure you get when they are not is
confusing rather than explicit.

  We have not confirmed whether the attachment check runs in production. If it is off today, a
  submission with missing documents passes quietly and starts failing the day someone switches it
  on. Build to the published requirements rather than to what the environment accepts today.

## What breaks here

<CostTable
  rows={[
    {
      where: "Submit",
      symptom: "\"It will not let us submit and we cannot see why\"",
      cause: "Pre-flight returned blockers. Every one carries a fix_stage, so surface them as actions rather than as a wall of text.",
    },
    {
      where: "Submit",
      symptom: "\"Callback not registered\"",
      cause: "A callback for claim.decision has to exist before any claim can be sent. Register it at Authenticate rather than here.",
    },
    {
      where: "Submit",
      symptom: "\"The visit will not close\"",
      cause: "Almost always the missing discharge consent. Closing needs an authentication of its own.",
    },
    {
      where: "After",
      symptom: "\"We resubmitted and now there are two claims\"",
      cause: "A new external_id was generated. Reuse the original one, because submission is idempotent on it.",
    },
  ]}
/>

### Errors

| HTTP | `error` | When |
|------|---------|------|
| 422 | `NOT_READY` | Close called while pre-flight is `ready: false`. |
| 409 | `CALLBACK_NOT_REGISTERED` | No callback exists for `claim.decision`. |
| 409 | `CLAIM_DATE_OUT_OF_COVER` | A billed date falls outside the cover's window. |
| 409 | `RESERVATION_MISSING` | A line has no active reservation at discharge. |

<CostTable
  rows={[
    {
      where: "Submit",
      symptom: "\"The claim sat in our queue and the window closed\"",
      cause:
        "Someone assumed the time bar was generous. It is disputed and may be short, so show the deadline on the claim and alert before it rather than after.",
    },
    {
      where: "Submit",
      symptom: "\"A returned claim can no longer be resubmitted\"",
      cause:
        "The missing-documents window elapsed. That deadline is terminal, and nothing recovers the claim afterwards.",
    },
    {
      where: "Anywhere",
      symptom: "\"Our state machine has no branch for this status\"",
      cause:
        "Eleven authorisation states and six claim types, and preauthorisations have their own twelve. Handle an unknown value by holding the claim rather than dropping it.",
    },
  ]}
/>

## Next

**Next:** [09 · Reconcile](/rail/stops/Reconcile)
