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 to dependant_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:
  1. Preview — compute the premium breakdown without creating a record
  2. Create — purchase the policy; status starts as pending
At creation time, the server also auto-creates MRN records for each covered dependant at the benefit’s provider. The resulting mapping is stored in the policy’s 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

EndpointJWT userAdmin keyNotes
POST /users/{id}/insurance_policies/previewDependants must belong to the token user
POST /users/{id}/insurance_policies/preview_enrollment_formRead-only — returns application/pdf. Zero DB writes, no Narayana calls
POST /users/{id}/insurance_policiesOne active policy per user+benefit
GET /users/{id}/insurance_policies/{pid}Returns 404 for wrong user
GET /users/{id}/insurance_policies/{pid}/detailsWrapper response including the linked benefit summary
GET /users/{id}/insurance_policiesFilters by status, benefit_id
GET /insurance_policiesFilter by user, benefit, status, time range
PATCH /insurance_policies/{pid}Set status, external_policy_id, dates

Key Concepts

Plan code derivation

The server derives a plan_code for each preview/purchase request from the resolved member list (SELF + selected dependants):
  • Adult vs child cutoff: members with age < 25 count as Children, age ≥ 25 as Adults.
  • Code format: {adult_count}A when 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).
The benefit’s 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

Each PlanVariant carries:
FieldTypeDescription
descriptionstring (optional)Variant copy (e.g. “Self + Spouse + 1 Child”)
daily_premium_amountinteger (minor units)Daily premium
annual_premium_amountinteger (minor units)Annual premium (replaces the old monthly_premium_amount)
coverage_amountinteger (minor units)Sum insured
currencystringISO 4217 (currently INR)
grace_period_daysinteger (optional)Days after expiry during which renewal is still possible
The persisted policy carries a single 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

pending → active → requires_customer_renewal_in_grace

         requires_policy_reissuance

         requires_customer_renewal

              expired

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 a PREVIEW — 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.
What happens server-side
  • Loads the benefit + provider, asserts it is Active and of type InsurancePolicy.
  • Resolves the caller-supplied dependant ids (ownership-checked) and auto-includes the SELF dependant at the head of the member list.
  • Derives the plan_code from the member list (adults/children) and looks up the matching PlanVariant in the benefit. Unsupported combinations return 400 IP-1009 PlanVariantNotAvailable.
  • Validates the nominee per plan: required when plan_code != "1A". Missing returns 400 IP-1015 NomineeRequiredForPlan. When present, it must still be the policyholder’s spouse.
  • Reads daily_premium_amount / annual_premium_amount / coverage_amount / currency / grace_period_days from the matched variant (no on-the-fly bracket calc).
  • Renders the enrollment-form PDF directly from the resolved data.
  • No INSERT / UPDATE against insurance_policies or mrns. No Narayana find_or_register_patients call.
Request body
benefit_id
string (uuid)
required
Benefit to preview against. Must be Active and of type InsurancePolicy.
dependant_ids
string (uuid)[]
required
Caller-supplied dependants to cover. SELF is auto-included if absent — you may pass an empty array.
nominee_details
object
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.
Example
curl -X POST http://localhost:8080/users/USER_ID/insurance_policies/preview_enrollment_form \
  -H 'Authorization: Bearer eyJhbGci...' \
  -H 'Content-Type: application/json' \
  --output enrollment_form_preview.pdf \
  -d '{
    "benefit_id": "018f4c2a-1b3e-7d8f-9a0b-2c3d4e5f6a7b",
    "dependant_ids": [],
    "nominee_details": {
      "type": "external",
      "salutation": "Mrs",
      "name": "Spouse Name",
      "date_of_birth": "1992-04-15",
      "relationship": "spouse",
      "gender": "female",
      "phone": "+919999999999"
    }
  }'
Response 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

curl -X POST http://localhost:8080/users/USER_ID/insurance_policies/preview \
  -H 'Authorization: Bearer eyJhbGci...' \
  -H 'Content-Type: application/json' \
  -d '{
    "benefit_id": "018f4c2a-1b3e-7d8f-9a0b-2c3d4e5f6a7b",
    "dependant_ids": ["01926b3a-7c2e-7d4f-a1b2-c3d4e5f60001"]
  }'
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.
The legacy fields primary_user_id and dependant_ids are still returned on every response for backwards compatibility with existing SDK/mobile clients, but are deprecated and will be removed in a future release. New integrations should rely on primary_member and dependants exclusively. The deprecated fields exactly mirror their structured counterparts (same id, same order).
FieldTypeNotes
primary_member.idstringPolicyholder’s user id.
primary_member.first_namestringAlways populated — purchase requires a completed onboarding.
primary_member.last_namestringAlways populated — purchase requires a completed onboarding.
primary_member.genderenumMALE, FEMALE, OTHER. Always populated post-onboarding.
dependants[].iduuidDependant id.
dependants[].first_namestringRequired (mirrors the dependants table).
dependants[].last_namestringRequired.
dependants[].salutationenumMR, MRS, MS, MASTER. Required.
dependants[].relationshipenumSELF, SPOUSE, MOTHER, FATHER, CHILD, FATHER_IN_LAW, MOTHER_IN_LAW, SIBLING. Required.
dependants[].genderenumMALE, 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:
VariantFieldsDescription
dependanttype, dependant_idNominee is an existing dependant on the user’s profile
externaltype, name, relationship, date_of_birth, gender, phoneNominee 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:
FieldTypeDescription
dependant_iduuidThe dependant’s internal ID
first_namestringFirst name
last_namestringLast name
salutationstringTitle (mr, mrs, ms, master)
ageintegerAge computed from date of birth
genderstringmale or female
relationshipstringRelationship 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. The metadata.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.
Narayana only accepts Male / Female for patient gender. Members with gender = other cause policy creation to fail with 400 IP-1013 — update the dependant’s gender before retrying. Upstream registration failures (timeout, 5xx) surface as 502 IP-1014 and the entire policy creation is rolled back.

Error Codes

CodeHTTPDescription
IP-1000500Internal server error
IP-1001404Insurance policy not found
IP-1002404Benefit not found or inactive
IP-1003400Benefit is not of type insurance_policy
IP-1004400Benefit is missing a required insurance field
IP-1005404Dependant not found or inactive
IP-1006400Dependant does not belong to the requesting user
IP-1007404Nominee dependant not found or inactive
IP-1008409User already has an active policy for this benefit
IP-1009400Requested dependant count exceeds benefit’s max_dependants
IP-1010400Validation error
IP-1011400Invalid UUID in request
IP-1012403Forbidden
IP-1013400Member gender is other — Narayana requires male or female
IP-1014502Upstream insurance-provider patient registration failed