Skip to article content Blogs

Building a Claims & Financing Engine from the Ground Up

Lessons from building a hospital claims and financing platform at a startup, including turning 40-page insurance policies into a one-page summary in under five minutes.

A stack of insurance papers sorted into a claim folder and financing tokens

Summary

At Bima Garage, an insurtech startup in Mumbai, I helped build the platform that connected agents inside hospitals with the back office handling insurance claims. This is what we built, the four features that made it work, and what I would do differently now.

The problem: paying for a hospital stay

Insurance penetration in India is low, and a large share of insured people have low credit scores. When an emergency hits, families take expensive loans just to get someone admitted. People with good insurance are not safe either: claims get delayed, negotiated down, or rejected at exactly the wrong moment.

Bima Garage took that headache over for a one-time processing fee or a yearly subscription. Two journeys started at the hospital desk. An insured patient was onboarded by our agent, and the back office handled the claim with the insurer. A patient with no insurance, or a denied claim, could have the bill financed by Bima Garage, acting as an NBFC, and repay it in EMIs.

Two customer journeys through one case platformHospital intake branches into an insured claim-support journey and a separately governed financing journey. Both use shared case operations, documents, tasks, communication, and audit before resolution.insureduninsured or deniedsupportssupports

Hospital intake + authority

Journey

Claim support case

Separate financing application

Insurer queries + decision

Assessment + accountable decision

Shared case operations
documents, tasks, communication, audit

Case resolution + timeline

Agreement + repayment plan

Two customer journeys through one case platformHospital intake branches into an insured claim-support journey and a separately governed financing journey. Both use shared case operations, documents, tasks, communication, and audit before resolution.insureduninsured or deniedsupportssupports

Hospital intake + authority

Journey

Claim support case

Separate financing application

Insurer queries + decision

Assessment + accountable decision

Shared case operations
documents, tasks, communication, audit

Case resolution + timeline

Agreement + repayment plan

Two customer journeys through one case platform. Both journeys share one case system, but the claim and the loan are decided by different people.
  1. Hospital intake establishes the patient or representative and creates a case.
  2. An insured journey collects policy evidence and coordinates the claim lifecycle.
  3. A separate financing journey performs authorised application, assessment, decision, and agreement steps.
  4. Both journeys use shared document, task, communication, deadline, and audit services.
  5. Claim decisions remain owned by the insurer and authorised operations.
  6. Financing decisions remain owned by the authorised lender and reviewers.
  7. The case timeline records resolution, settlement, repayment, closure, and any correction path.

Two apps, one case

Hospital agents used HobNob, a React Native app. The back office used Desk, a React web portal. Both talked to a Python and Django backend on PostgreSQL, running in Docker on Kubernetes.

Onboarding needed only a few fields and one identifier, a mobile number or a national ID, and that identifier followed the case through its whole life.

If I were starting again, the case is the thing I would design first. Its statuses should be real states with allowed transitions, not labels any screen can overwrite. And I would give every case an internal ID and treat the phone number as an attribute, because phone numbers change hands.

Scoring a loan while the family waits

For an uninsured patient, time is everything. Once they were onboarded, the platform fetched their CIBIL credit score from third-party vendors, combined it with the estimated medical cost, and our scoring algorithm produced a recommendation for our banking partners: whether to approve the loan, and at what rate.

Families are anxious while this happens, so updates had to be instant. Every time a row changed status, a Django signal fired, handed the job to a Celery worker, and the worker used AWS SNS to send WhatsApp and email.

It worked, and it is the part I would build differently. Django’s own documentation on signals warns that they make code hard to follow. Worse, a message sent from a side door can go out for a change that never committed, or be lost for one that did. Celery’s task guide recommends writing tasks to be idempotent, because a message can be redelivered.

The fix is called a transactional outbox. Write the status change and a row saying “this needs sending” in the same transaction. A separate dispatcher publishes those rows, and consumers ignore an event they have already handled.

The transactional outbox, step by stepA case change and its event are committed together, published by a dispatcher, and deduplicated by consumers.ConsumerBrokerDispatcherDatabaseApplicationone transaction withcase change + outboxrowcommittedread unpublishedoutbox rowspublish with event IDmark publisheddeliver, possibly morethan onceskip if event IDalready handled
The transactional outbox, step by stepA case change and its event are committed together, published by a dispatcher, and deduplicated by consumers.ConsumerBrokerDispatcherDatabaseApplicationone transaction withcase change + outboxrowcommittedread unpublishedoutbox rowspublish with event IDmark publisheddeliver, possibly morethan onceskip if event IDalready handled
The transactional outbox, step by step. The event exists only if the change committed. It may arrive twice, so consumers skip repeats.
  1. The application writes the case change and an outbox row in one transaction.
  2. The database commits both or neither.
  3. A dispatcher reads unpublished outbox rows.
  4. It publishes each one with its event ID.
  5. It marks the row as published.
  6. The broker delivers the event, possibly more than once.
  7. The consumer skips event IDs it has already handled.
def transition_case(case_id, command, actor):
    with database.transaction():
        case = cases.lock(case_id)
        transition = policy.validate(case, command, actor)

        case.apply(transition)
        cases.save(case)

        outbox.append(
            event_id=stable_event_id(case.id, case.version),
            event_type="case.status_changed",
            aggregate_id=case.id,
            payload={
                "from": transition.previous_state,
                "to": transition.next_state,
                "reason_code": transition.reason_code,
            },
        )

    return case.snapshot()


def publish_outbox(record):
    broker.publish(
        topic=record.event_type,
        key=record.aggregate_id,
        payload=record.payload,
        idempotency_key=record.event_id,
    )
    outbox.mark_published(record.event_id)

Collecting repayments with eNACH

Financing a hospital bill is half the job. Collecting the EMIs reliably is the other half. We looked at standard gateways like Paytm and Razorpay, but we needed something that ran with no human involved, month after month.

We chose eNACH. The customer registers a one-time mandate, and the system can pull the EMI from their bank account on a set date. We could also pause or cancel the mandate from code when a repayment plan changed.

A recurring payment mandate lifecycleA mandate registers and activates before producing scheduled debit attempts. Attempts settle or fail and reconcile independently. The mandate can pause, change, cancel, expire, or complete.acceptedrejectedsettledfailedyesno

Mandate request

Registration result

Active mandate

Rejected

Schedule debit attempt

Present debit

Authoritative outcome

Reconcile repayment ledger

Retry allowed?

Communication + review

Paused or changed

Cancelled, expired, or completed

A recurring payment mandate lifecycleA mandate registers and activates before producing scheduled debit attempts. Attempts settle or fail and reconcile independently. The mandate can pause, change, cancel, expire, or complete.acceptedrejectedsettledfailedyesno

Mandate request

Registration result

Active mandate

Rejected

Schedule debit attempt

Present debit

Authoritative outcome

Reconcile repayment ledger

Retry allowed?

Communication + review

Paused or changed

Cancelled, expired, or completed

A recurring-payment mandate lifecycle. A mandate, each debit, and the bank’s settlement are separate records, not one auto-pay flag.
  1. Create a mandate request under the applicable participant and provider rules.
  2. Track registration separately from activation and rejection.
  3. An active mandate schedules distinct debit attempts with stable identities.
  4. A presented debit waits for authoritative settlement or failure status.
  5. Successful debits reconcile against the repayment ledger.
  6. Failed debits follow a policy-controlled retry, communication, or review path.
  7. The mandate can pause, change, cancel, expire, or complete without rewriting prior attempts.
  8. Scheduled reconciliation finds missing or inconsistent provider outcomes.

The lesson here is that a mandate is not an on/off switch. Registration, activation, each debit, a failed debit, a retry, a pause, and a cancellation all happen at different times and are reported by different parties. “The API accepted it” does not mean the money arrived. You only know that by reconciling against the bank’s reports. NPCI’s NACH overview describes the rails. The limits and obligations come from the current rules and your provider.

Cutting turnaround time by reading the insurers’ email

For a long time our biggest bottleneck was claim tracking. Back-office staff refreshed insurer portals and inboxes all day to find out whether a claim had moved.

Guided by a senior solution architect, we built an email polling engine. Scheduled jobs polled our dedicated claims inboxes, and scripts read the automated emails insurers sent. They pulled out the claim ID and the status, such as Query Raised, Approved, or Rejected, and updated PostgreSQL. Hours of manual data entry became seconds, and each update set off the notifications to the hospital desk.

Ingesting a partner status updateAn inbound message is stored, deduplicated, parsed into a candidate event, and either applied or quarantined.yesnoyesno

Webhook, API, or partner email

Store raw message + digest

Seen this message before?

Ignore the replay

Parse allowlisted fields into a candidate event

Sender, case, and transition valid?

Apply the case transition

Quarantine for an operator

Ingesting a partner status updateAn inbound message is stored, deduplicated, parsed into a candidate event, and either applied or quarantined.yesnoyesno

Webhook, API, or partner email

Store raw message + digest

Seen this message before?

Ignore the replay

Parse allowlisted fields into a candidate event

Sender, case, and transition valid?

Apply the case transition

Quarantine for an operator

Ingesting a partner status update. An incoming message never updates a case directly. It has to pass checks first.
  1. A webhook, API call, or partner email arrives.
  2. Store the raw message and its digest.
  3. If the message was seen before, ignore the replay.
  4. Parse allowlisted fields into a candidate event.
  5. Check the sender, the case, and the transition.
  6. If all are valid, apply the case transition.
  7. Otherwise quarantine it for an operator.

It worked, but email is somebody else’s API with no contract, and templates change without warning. If I built it again I would keep the raw message, ignore one already seen, and send anything unfamiliar or mismatched to a person instead of guessing. The diagram above is that version.

Forty pages down to one

This feature landed on my desk at very short notice. An insurance policy is 20 to 40 pages of dense legal language. We wanted to turn it into a one-page summary called KYP, Know Your Policy.

To pull text out of large PDFs we tried Tesseract, AWS Textract, and Google Cloud Vision. Azure Form Recognizer, now called Azure Document Intelligence, was the best fit for speed and accuracy on unstructured Indian financial documents.

The real problem was load. A 40-page PDF is heavy, and five uploads at once could tie up our Django threads and take the API down. So the file went straight to S3, a message went onto a RabbitMQ queue, and the user saw a “Processing” screen. Workers took messages one at a time, sent the PDF to Azure, parsed the JSON that came back, built the summary, and told the user it was ready. We delivered it in under five minutes without ever loading the main servers.

The version I would ship today adds one step. Extraction returns a confidence for each field, and a person should check the uncertain ones before a summary reaches a patient. This is that version.

The human reviewed KYP document pipelineA securely uploaded policy is queued, extracted into a versioned schema, validated against source pages, reviewed where confidence is low, and converted into a cited summary that requires accountable approval.4. Sign-off

Authorised approval

Versioned one-page summary

3. Reviewyesno

Review needed?

Field-level human review

Cited KYP summary draft

2. Extraction

OCR + schema extraction

Types, rules, citations, confidence

1. Intake

Scoped policy upload

Encrypted object storage

Durable processing queue

The human reviewed KYP document pipelineA securely uploaded policy is queued, extracted into a versioned schema, validated against source pages, reviewed where confidence is low, and converted into a cited summary that requires accountable approval.4. Sign-off

Authorised approval

Versioned one-page summary

3. Reviewyesno

Review needed?

Field-level human review

Cited KYP summary draft

2. Extraction

OCR + schema extraction

Types, rules, citations, confidence

1. Intake

Scoped policy upload

Encrypted object storage

Durable processing queue

The human-reviewed KYP document pipeline. Extraction does the reading. Validation and a human check keep the summary faithful to the policy.
  1. The authorised user uploads a policy through a scoped object-storage path.
  2. Media validation confirms identity, type, checksum, and security policy.
  3. A durable queue sends the document to a versioned extraction worker.
  4. OCR and layout or field extraction preserve page-level source coordinates.
  5. Deterministic validation checks types, cross-field rules, missing values, and contradictions.
  6. Low-confidence or conflicting fields become explicit review tasks.
  7. A summary draft uses only validated fields and cited source passages.
  8. An authorised reviewer corrects and approves the one-page KYP output.
  9. The system records versions, evidence, reviewer, and retention state.

What I took from it

Startups teach you that the idea is the cheap part. What matters is the execution: how you design the database, how you handle the queues, and how you deal with the edge cases.

  • Design the case and its states before any screen.
  • Tie every notification to a change that actually committed.
  • Money needs reconciliation, not optimism.
  • Put a queue in front of anything slow.

References

  1. Building a Claims & Financing Engine from the Ground Up

    Abhishek Choudhury. Original project article, published February 2025.

  2. NACH: High Volume Repetitive Payment Solution

    National Payments Corporation of India. An overview of NACH transactions and mandate management.

  3. Django signals

    Django Software Foundation. Signal behavior and the maintainability warning for implicit coupling.

  4. Celery tasks

    Celery Project. Task delivery, idempotency, timeouts, and worker behavior.

  5. Azure Document Intelligence documentation

    Microsoft. Current product terminology, capabilities, versions, and lifecycle.

  6. Transparency note for Document Intelligence

    Microsoft. Responsible-use considerations for the document-processing service.