Two auth tiers — User-facing endpoints (preview, create, get, list) use a user JWT Bearer. Admin endpoints (list all, update) require an admin JWT Bearer (
Authorization: Bearer <admin-jwt>).Overview
An insurance policy ties a primary user to an insurance-type benefit and a set of dependants to be covered. The SELF dependant (the primary user themselves) is always auto-included in the covered members — you do not need to add it manually todependant_ids.
Premium amounts are read directly from the matched plan variant in the benefit’s benefit_details.plans map. The server derives a plan_code (e.g. 1A, 2A, 2A1C, 2A2C) from the resolved member list and looks up the variant’s daily_premium_amount, annual_premium_amount, coverage_amount, currency, and optional grace_period_days. There is no per-dependant age/gender bracket computation anymore.
The purchase flow has two steps:
- Preview — compute the premium breakdown without creating a record
- Create — purchase the policy; status starts as
pending
metadata.dependant_mrn_map.
Activation (status → active) is done by an admin once the external insurer issues a policy number.
Purchase Flow
Auth Guards by Endpoint
| Endpoint | JWT user | Admin key | Notes |
|---|---|---|---|
POST /users/{id}/insurance_policies/preview | ✓ | — | Dependants must belong to the token user |
POST /users/{id}/insurance_policies/preview_enrollment_form | ✓ | — | Read-only — returns application/pdf. Zero DB writes, no Narayana calls |
POST /users/{id}/insurance_policies | ✓ | — | One active policy per user+benefit |
GET /users/{id}/insurance_policies/{pid} | ✓ | — | Returns 404 for wrong user |
GET /users/{id}/insurance_policies/{pid}/details | ✓ | — | Wrapper response including the linked benefit summary |
GET /users/{id}/insurance_policies | ✓ | — | Filters by status, benefit_id |
GET /insurance_policies | — | ✓ | Filter by user, benefit, status, time range |
PATCH /insurance_policies/{pid} | — | ✓ | Set status, external_policy_id, dates |
Key Concepts
Plan code derivation
The server derives aplan_code for each preview/purchase request from the resolved member list (SELF + selected dependants):
- Adult vs child cutoff: members with
age < 25count as Children,age ≥ 25as Adults. - Code format:
{adult_count}Awhen there are no children, otherwise{adult_count}A{child_count}C. - Examples:
1A(self only),2A(self + spouse),2A1C(self + spouse + 1 child),2A2C(self + spouse + 2 children).
benefit_details.plans map is a free-form dictionary of plan_code → PlanVariant. If the derived code is not a key in that map (e.g. 3A, 1A2C, or any combination the provider has not priced), the request is rejected with 400 IP-1009 PlanVariantNotAvailable. The benefit’s plans map is the single source of truth for which combinations are sellable.
Premium amounts
EachPlanVariant carries:
| Field | Type | Description |
|---|---|---|
description | string (optional) | Variant copy (e.g. “Self + Spouse + 1 Child”) |
daily_premium_amount | integer (minor units) | Daily premium |
annual_premium_amount | integer (minor units) | Annual premium (replaces the old monthly_premium_amount) |
coverage_amount | integer (minor units) | Sum insured |
currency | string | ISO 4217 (currently INR) |
grace_period_days | integer (optional) | Days after expiry during which renewal is still possible |
premium_amounts JSONB column with shape { daily, annual, currency } — one common currency for both daily and annual amounts. Coverage and grace period are not snapshotted on the policy row; they are re-derived from the matched variant when needed (e.g. by the document renderer).
Policy Status Lifecycle
Pre-purchase confirmation
Before creating a policy, the mobile/SDK frontend can render the enrollment form PDF for a prospective (not-yet-issued) policy and ask the user to verify their selected coverage, members, nominee, and computed premium. The endpoint is read-only and idempotent — it performs zero database writes, makes no Narayana calls, and creates no MRN rows. It is safe to re-call as the user adds or removes dependants, swaps the nominee, or switches plans. The rendered PDF carries aPREVIEW — not yet issued banner, a synthetic policy id of all zeros, and the same applicant / coverage / members / nominee blocks as the post-issuance enrollment form. After the user reviews and confirms in-app, the frontend submits POST /users/{user_id}/insurance_policies with the same payload to create the policy.
Preview Enrollment Form
Authentication:
Authorization: Bearer <access_token>. Auth gate is actor.require_self_or_trusted_backend(&user_id) — same as preview and create.- Loads the benefit + provider, asserts it is
Activeand of typeInsurancePolicy. - Resolves the caller-supplied dependant ids (ownership-checked) and auto-includes the SELF dependant at the head of the member list.
- Derives the
plan_codefrom the member list (adults/children) and looks up the matchingPlanVariantin the benefit. Unsupported combinations return 400IP-1009 PlanVariantNotAvailable. - Validates the nominee per plan: required when
plan_code != "1A". Missing returns 400IP-1015 NomineeRequiredForPlan. When present, it must still be the policyholder’s spouse. - Reads
daily_premium_amount/annual_premium_amount/coverage_amount/currency/grace_period_daysfrom the matched variant (no on-the-fly bracket calc). - Renders the enrollment-form PDF directly from the resolved data.
- No
INSERT/UPDATEagainstinsurance_policiesormrns. No Narayanafind_or_register_patientscall.
Benefit to preview against. Must be
Active and of type InsurancePolicy.Caller-supplied dependants to cover. SELF is auto-included if absent — you may pass an empty array.
Nominee details — either
Dependant (existing dependant id, must be a SPOUSE) or External (inline name + DOB + relationship, relationship must be spouse). Optional: omit when the derived plan is 1A (self only). Required for every other plan; missing → 400 IP-1015.200 OK with Content-Type: application/pdf. Body is the binary PDF (enrollment_form_preview.pdf).
Endpoints
POST .../preview
Preview the premium breakdown for a set of dependants without creating a policy.
POST .../preview_enrollment_form
Render the enrollment form PDF for a prospective policy. Read-only — safe to re-call as the user adjusts dependants/nominee.
POST .../insurance_policies
Purchase an insurance policy. Status starts as
pending.GET .../insurance_policies/{id}
Fetch a single policy by its internal UUID.
GET .../insurance_policies/{id}/details
Fetch a single policy bundled with its linked benefit summary (name, type, provider, benefit_details).
GET .../insurance_policies
List the authenticated user’s policies. Filter by
status or benefit_id.GET /insurance_policies (admin)
Admin: list all policies with full filtering including time range and user.
PATCH /insurance_policies/{id} (admin)
Admin: set status, external policy ID, start/end dates.
Request / Response Examples
The SELF dependant is always auto-included in
dependant_ids — you do not need to add it. The members array in the preview response and dependant_ids in the create response will both include SELF automatically.Member fields — primary_member and dependants
Policy responses carry the policyholder identity and full dependant roster as structured objects. Names are resolved at response build time (not snapshotted on the row), so renames flow through automatically — same model as benefit_name. primary_member.id and dependants[].id are the canonical identifiers going forward.
| Field | Type | Notes |
|---|---|---|
primary_member.id | string | Policyholder’s user id. |
primary_member.first_name | string | Always populated — purchase requires a completed onboarding. |
primary_member.last_name | string | Always populated — purchase requires a completed onboarding. |
primary_member.gender | enum | MALE, FEMALE, OTHER. Always populated post-onboarding. |
dependants[].id | uuid | Dependant id. |
dependants[].first_name | string | Required (mirrors the dependants table). |
dependants[].last_name | string | Required. |
dependants[].salutation | enum | MR, MRS, MS, MASTER. Required. |
dependants[].relationship | enum | SELF, SPOUSE, MOTHER, FATHER, CHILD, FATHER_IN_LAW, MOTHER_IN_LAW, SIBLING. Required. |
dependants[].gender | enum | MALE, FEMALE, OTHER. Required. |
dependants includes the SELF entry (the policyholder represented as a dependant on the policy roster). UIs that want to surface only the additional covered family members should filter by relationship !== "SELF".nominee_details — Tagged Union
The nominee_details field is a JSONB tagged union. Use one of two variants:
| Variant | Fields | Description |
|---|---|---|
dependant | type, dependant_id | Nominee is an existing dependant on the user’s profile |
external | type, name, relationship, date_of_birth, gender, phone | Nominee is a person not registered as a dependant |
Preview members Array
The preview response returns a unified members array of MemberDetail objects instead of separate primary_member and dependants fields:
| Field | Type | Description |
|---|---|---|
dependant_id | uuid | The dependant’s internal ID |
first_name | string | First name |
last_name | string | Last name |
salutation | string | Title (mr, mrs, ms, master) |
age | integer | Age computed from date of birth |
gender | string | male or female |
relationship | string | Relationship to the primary user (self, spouse, child, etc.) |
MRN Auto-Creation
When a policy is created, the server automatically creates one MRN record per covered dependant at the benefit’s provider. Themetadata.dependant_mrn_map field maps each dependant ID to its newly created MRN ID.
For each member without an existing MRN at this provider, the server calls the upstream insurance provider (currently Narayana Health) to look up the patient by name + DOB; if not found, it registers a new patient under the primary user’s phone. The returned external_mrn (temp_number and/or mrn) is persisted on the new MRN row. If a member already has an MRN row at this provider it is reused as-is, with no upstream call.
Error Codes
| Code | HTTP | Description |
|---|---|---|
IP-1000 | 500 | Internal server error |
IP-1001 | 404 | Insurance policy not found |
IP-1002 | 404 | Benefit not found or inactive |
IP-1003 | 400 | Benefit is not of type insurance_policy |
IP-1004 | 400 | Benefit is missing a required insurance field |
IP-1005 | 404 | Dependant not found or inactive |
IP-1006 | 400 | Dependant does not belong to the requesting user |
IP-1007 | 404 | Nominee dependant not found or inactive |
IP-1008 | 409 | User already has an active policy for this benefit |
IP-1009 | 400 | Requested dependant count exceeds benefit’s max_dependants |
IP-1010 | 400 | Validation error |
IP-1011 | 400 | Invalid UUID in request |
IP-1012 | 403 | Forbidden |
IP-1013 | 400 | Member gender is other — Narayana requires male or female |
IP-1014 | 502 | Upstream insurance-provider patient registration failed |