Skip to content
Updated Jul 12, 2026

Claims

The claims service owns the medical claim and prior-authorization lifecycles. It accepts claims submitted complete (there is no DRAFT), drives them through a status machine from SUBMITTED to PAID/CLOSED, itemises them into ClaimLine rows, and emits state-change events for Billing and Notifications to consume. Prior authorization lives in the same service as a parallel, independent workflow.

Field reference: full columns, types and nullability live in the catalog: glossary terms Claim, ClaimLine, ClaimEvent, ClaimDocument, PriorAuth. This page is the narrative; the companion domain page is Claims Lifecycle.

As-built adjudication is a pass-through

The wired review path auto-approves every line at the billed amount. A rule-engine adjudicator (internal/adjudication) that would compute per-line allowed amounts (deny, exclude, cap, scale) exists but is never called on the live path. Treat rule-driven adjudication, prior-auth gating, and pre-approval eligibility checks as built-but-not-wired. See caveats.

What it owns

  • Claim intake from members, providers and internal callers, complete at submission.
  • The claim status machine and its append-only audit trail.
  • ClaimLine itemisation (one row per service); the claim header carries no monetary total.
  • Supporting claim documents (metadata + URL).
  • Prior-authorization records and their own SUBMITTED → PENDING_REVIEW → APPROVED/DENIED flow.
  • A transactional outbox that publishes claim and prior-auth events to Kafka.

Database

Schema claims. Column-level detail is in the catalog.

TablePurpose
claimsRoot claim aggregate: status, policy/term, claimant, incident date, opaque document blob. No monetary total.
claim_linesService line items: amount_claimed/amount_allowed/amount_paid (NUMERIC(14,4)), element_id, coverage_term_key, reason_code.
claim_eventsAppend-only audit log of every claim state transition (event_type, from_status, to_status, actor, note).
claim_documentsSupporting documents attached to a claim (filename, content type, URL).
prior_authsPrior-authorization requests and decisions.
outboxTransactional outbox for reliable Kafka publishing.

The claims row holds only the current status; the full history is the claim_events trail. Every status change writes one claim_events row and one outbox row inside the same transaction as the status update.

Claim state machine

ClaimStatus = SUBMITTED · UNDER_REVIEW · PENDING_INFO · APPROVED · REJECTED · PAID · CLOSED (packages/go/domain/enums.go). REJECTED is the only adverse status; there is no IN_REVIEW and no DENIED on a claim (DENIED exists only on PriorAuth).

review (ReviewClaim) is the adjudication entry point. On the wired path it always returns the auto-approve decision, so it transitions straight to APPROVED. It only routes to UNDER_REVIEW (emitting claim.review_required) when the adjudicator returns MANUAL_REVIEW_REQUIRED, which requires the rule engine that is not active today. In practice auto-approve fires when a claim line is added (a claim is created empty, so line submission is the adjudication moment). requestInfo moves a claim to PENDING_INFO. approve accepts SUBMITTED, UNDER_REVIEW, PENDING_INFO, and APPROVED (the last as an idempotent re-entry for replay/repair). reject is allowed from SUBMITTED, UNDER_REVIEW or PENDING_INFO.

API routes

All /claims* routes are JWT-protected and tenancy-scoped (see Tenancy). /prior-auth* routes require a privileged (admin) token; employer-tenant tokens get 403 because prior-auth rows carry no employer linkage.

MethodPathAuthDescription
POST/claimsJWTSubmit a new claim
GET/claimsJWTList claims (filter by policy, member, or status)
GET/claims/listJWTAlias of GET /claims (web-admin via APISIX)
GET/claims/{locator}JWTGet claim by locator
PATCH/claims/{locator}/reviewJWTAdjudicate: auto-approve, or move to UNDER_REVIEW if manual review is flagged
PATCH/claims/{locator}/approveJWTApprove a claim in UNDER_REVIEW
PATCH/claims/{locator}/rejectJWTReject with a reason code
PATCH/claims/{locator}/requestInfoJWTMove to PENDING_INFO
PATCH/claims/{locator}/closeJWTClose an approved or rejected claim
POST/claims/{locator}/number/generateJWTReturn a display claim number (returns the locator)
POST/claims/{locator}/linesJWTAdd a ClaimLine
GET/claims/{locator}/lines/listJWTList a claim's lines
GET/claims/{locator}/events/listJWT501 stub, not implemented
POST/claims/{locator}/documentsJWTAttach a document
GET/claims/{locator}/documents/listJWTList a claim's documents
POST/prior-authJWTSubmit a prior-authorization request
GET/prior-authJWTList prior authorizations
GET/prior-auth/listJWTAlias of GET /prior-auth (web-admin via APISIX)
GET/prior-auth/{locator}JWTGet prior auth by locator
PATCH/prior-auth/{locator}/reviewJWTSUBMITTED → PENDING_REVIEW
PATCH/prior-auth/{locator}/approveJWTPENDING_REVIEW → APPROVED
PATCH/prior-auth/{locator}/denyJWTPENDING_REVIEW → DENIED
GET/internal/claims/{locator}X-Internal-Service secretRetrieve claim for Billing
PATCH/internal/claims/{locator}/payX-Internal-Service secretMark claim PAID (called by Billing)

GET /healthz and GET /readyz are unauthenticated health probes. The /internal/* routes are guarded twice: the APISIX edge returns 404 for /claims/internal/*, and the service itself requires an X-Internal-Service header matching INTERNAL_SVC_SECRET (constant-time compare; missing/wrong header gets 401). The guard fails closed: if the secret is unset every internal request gets 503, so a misconfigured deploy cannot silently re-expose pay.

Tenancy

Employer-facing routes are scoped by the org_locator JWT claim (the employer party locator). The claim is resolved to the employer's member set via group-scheme; a caller with neither org_locator nor an admin role gets 403. org_locator is checked before admin roles, so a stray employer-admin role cannot widen a tenant token.

  • Lists (GET /claims): any query filter is overridden by the caller's verified member set. Privileged (admin) callers are unrestricted.
  • By-locator routes (get, review, approve, reject, requestInfo, close, lines, documents): a claim outside the caller's tenancy returns 404, identical to a claim that does not exist, so sequential locators are not an existence oracle.
  • Submit: the body's memberLocator must belong to the caller's org, else 403 (the caller asserted the key, so no oracle to protect).
  • /internal/* routes carry no user JWT and are deliberately outside tenancy; they are protected by the edge block plus the X-Internal-Service guard.

Events

Claims both publishes and consumes. Every published message is the canonical envelope (packages/go/domain/event_envelope.go): eventId, eventType, occurredAt, correlationId, causationId, client lineage (sessionId / activityId / activityName), payload, and state, the subject entity frozen at emit time (event-carried state transfer). Envelope details live in the Kafka Event Catalog; per-type contracts live in the Event Registry.

Publishes: topic claims.events

Claim and prior-auth events are enqueued in the transactional outbox (subject snapshot in the state column) and published to claims.events (KAFKA_CLAIMS_TOPIC, default claims.events) by the outbox worker. The worker stamps eventId from the outbox row id (stable across re-publication), lifts correlationId and client lineage from the trace_context captured at enqueue, and forwards state. The verb lives in the eventType field, not the topic name.

One event bypasses the outbox: record.viewed describes a read, so it has no transaction to attach to and goes straight to Kafka through the direct producer (internal/kafka/producer.go), lineage lifted from W3C baggage, unkeyed, with no correlationId and no state.

eventTypeEmitted whenstate subjects
claim.submittedA claim is submittedclaim
claim.review_requiredReview flags manual review (→ UNDER_REVIEW); today only via the high-value thresholdclaim
claim.approvedA claim reaches APPROVED (line-add auto-approve, review, or approve)claim
claim.rejectedA claim is rejected (manual reject with reasonCode, or auto-reject on policy cancellation without one)claim
claim.info_requestedA claim moves to PENDING_INFOclaim
claim.closedAn approved or rejected claim is closedclaim
claim.payment_initiatedPayment starts as the claim leaves APPROVED (same transaction as claim.paid)claim
claim.paidBilling confirms payment via the internal pay endpointclaim
prior_auth.submittedA prior-auth is submittedpriorAuth
prior_auth.escalatedA prior-auth is routed into review (SUBMITTED → PENDING_REVIEW)priorAuth
prior-auth.decidedA prior-auth is approved or denied (carries a decision field)priorAuth
record.viewedA 2xx read of a claim record (ENG-406 audit trail, shared audit.RecordView middleware)none

state.claim / state.priorAuth is the full subject row as in scope at the emit site, Document decoded; on transition events it is the pre-transition snapshot. There is no aggregate claims.adjudicated event; an approval is signalled by claim.approved. Note the as-built spelling split kept for wire compatibility: prior_auth.submitted and prior_auth.escalated use underscores, prior-auth.decided a hyphen.

Consumes: topic enrollment.events

The service runs a Kafka consumer (group claims-service, KAFKA_ENROLLMENT_TOPIC, default enrollment.events) and switches on eventType:

eventTypeReaction
policy.cancelledAuto-reject every open claim (SUBMITTED/UNDER_REVIEW/PENDING_INFO) for that policy, emitting claim.rejected per claim. Idempotent on replay.
policy.issuedNo-op (Phase 1).

Contracts are the registry's: payload schema, state subjects, lineage requirements, producers and consumers as code refs, and golden examples all live in packages/go/domain/eventregistry/registry/<eventType>/.

Prior authorization

PriorAuth is a pre-approval for a procedure, lifecycle SUBMITTED (on create) → PENDING_REVIEW (on review) → APPROVED | DENIED. The migration default and an older domain comment say PENDING; that value is a legacy/seed fallback only (approve/deny still accept it), runtime records open as SUBMITTED.

Prior auth is an independent workflow. It does not gate claim adjudication today: the claim review/approve path contains no prior-auth lookup. Enforcing a prior-auth requirement before a claim can advance is planned, not implemented, and likely belongs upstream in eligibility/enrollment.

Dependencies

ServiceHow used
EligibilityWhen a claim reaches APPROVED (auto-approve on line add, review, or approve), claims calls ApplyAccumulators (PATCH /internal/members/{locator}/accumulators/{termLocator}/apply) to record consumption against the member's term. The apply is idempotent per claim line on the eligibility side, so replays cannot double-count. A pre-approval coverage/eligibility check exists in the client (CheckEligibility, GetCoverage) but is not wired into the claim flow.
EnrollmentOn review/approve, fetches the policy to enrich the outbox payload (policy_locator, account_id) for Billing. Also the source of the consumed enrollment.events.
Policy AdminClient exists for benefit-rule/element lookup feeding the rule-engine adjudicator. Not exercised on the wired pass-through path.
BillingCalls the internal endpoints to fetch a claim and confirm payment; consumes claims.events.

External clients are nil-safe: when a URL is unconfigured (tests, minimal deploys) the dependent step is skipped.

Invariants

  • The claim header stores the current status and no monetary total; amounts live on ClaimLine.
  • REJECTED is the only adverse claim status; DENIED is a PriorAuth status.
  • One claim_events row plus one outbox row are written per status change, in the same transaction as the status update.
  • amount_allowed defaults to amount_claimed (un-adjudicated lines allow the billed amount); amount_paid is set to amount_allowed on the PAID transition.
  • All events flow through one topic (claims.events); the verb is the eventType field.

Caveats

  • Rule-driven adjudication is dead code on the wired path. internal/adjudication.Adjudicate and the rule engine are never invoked by ReviewClaim; it hard-codes AUTO_APPROVED, so a reviewed claim auto-approves at the billed amount.
  • No prior-auth gate. Prior auth runs in parallel and never blocks a claim.
  • No pre-approval eligibility check. Only ApplyAccumulators (the on-approval write-back) is called; coverage validation at date of service is built into the client but unused.
  • claims.adjudicated does not exist. Earlier docs invented it; there is no such topic and no adjudicated eventType.
  • Manual approval and accumulator reversal are open. Auto-approve is on for now; rejecting or voiding an approved claim does not yet reverse the applied accumulator amounts (#1434).
  • Appeals are not implemented: no appeal status, event, or re-review path.

Olly Health Insurance Platform