TypeScript/Node: Zero to first 200

A Node.js script that prequalifies John Doe for Final Expense coverage. Under 60 minutes for a backend developer with no insurance domain knowledge.

TypeScript/Node: Zero to first 200

Target: A Node.js script that prequalifies John Doe (NC, 64, male, no conditions) for Final Expense coverage and prints every qualifying plan with its monthly premium. Time budget: under 60 minutes from a blank directory.

Language requirement: Node.js 20+ with npm or pnpm. No React, no browser.


What you'll build

A single TypeScript script — prequalify.ts — that:

  1. Reads your API bearer token from an environment variable.
  2. Asks the ISA underwriting engine whether John Doe qualifies for Final Expense coverage.
  3. Prints each qualifying plan with carrier name, tier, monthly premium, and face value.

What "prequalify" means

Prequalify is a fast eligibility screen. You send an applicant's demographics and a coverage request. The engine returns every plan the applicant qualifies for with a bucketed monthly premium.

It is not a binding quote. Think of it as: "Will this person likely get coverage, and roughly what will it cost?" You run it before showing the applicant a full application form.

What "Final Expense" is

Final Expense (FEX) is a small face-value whole-life product designed to cover end-of-life costs. Typical death benefit: $5,000–$25,000.


Prerequisites

RequirementWhy
Node.js 20 or newerThe SDK uses ES modules and crypto.subtle, available in Node 20 without flags.
npm 9+ or pnpm 8+Package manager.
An ISA API bearer tokenSee "Getting your test token" below.

Getting your test token

Your ISA API token arrives by email within minutes of completing checkout at checkout.isaapi.com. The email contains both an isa_test_… and an isa_live_… token. Use the test token throughout this tutorial.

Test mode vs. live mode. Every call you make with a test token runs against the real engine with real carrier rules; pricing and audit are sandboxed. Switch ISA_TOKEN to your live token to promote.


1. Create a project directory

mkdir isa-ts-demo && cd isa-ts-demo
npm init -y
npm install --save-dev typescript @types/node tsx

Add a minimal tsconfig.json:

cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "dist"
  },
  "include": ["*.ts"]
}
EOF

2. Install the SDK

npm install isa-sdk

The SDK targets Node 20+ and ships TypeScript declarations.


3. Set your token as an environment variable

Never hardcode credentials. Export your test token in your shell:

export ISA_TOKEN="<paste your isa_test_… key here>"

Verify it is set:

echo "Token prefix: ${ISA_TOKEN:0:9}"
# Expected: isa_test_

4. Bootstrap the client

Create prequalify.ts:

import { Isa, IsaConfigError } from 'isa-sdk';

// Isa.withBearer() reads ISA_TOKEN from the environment.
// If it is missing it throws IsaConfigError naming the variable.
let isa: Isa;
try {
  isa = await Isa.withBearer();
} catch (err) {
  if (err instanceof IsaConfigError) {
    console.error('Configuration error:', err.message);
    console.error('Set ISA_TOKEN in your environment.');
    process.exit(1);
  }
  throw err;
}

console.log('Client created. Ready to prequalify.');

Run it:

npx tsx prequalify.ts

What you should see:

Client created. Ready to prequalify.

5. Run a prequalify for John Doe NC

Replace the body of prequalify.ts with the full script below.

import {
  Isa,
  IsaConfigError,
  Sex,
  Height,
  Weight,
  NicotineDuration,
  Coverage,
  ProductSelection,
  Products,
} from 'isa-sdk';

// ── 1. Build the client (reads ISA_TOKEN from env) ───────────────────────────
let isa: Isa;
try {
  isa = await Isa.withBearer();
} catch (err) {
  if (err instanceof IsaConfigError) {
    console.error('Configuration error:', err.message);
    process.exit(1);
  }
  throw err;
}

// ── 2. Run prequalify ────────────────────────────────────────────────────────
//
// dob:         ISO date string (YYYY-MM-DD). The engine uses this to
//              compute age and apply age-band rules.
// sex:         Sex.Male or Sex.Female. Biological sex affects underwriting
//              tables for most carriers.
// height:      Use Height.fromFeetInches(feet, inches) — never pass a raw
//              number; the value object prevents passing 70 when you mean 5'10".
// weight:      Weight.fromPounds(number) — keeps the unit explicit.
// state:       Two-letter US postal code. State determines which carriers
//              are licensed to write business there.
// nicotineUse: { lastUsed: NicotineDuration.Never/... } — the single biggest
//              underwriting factor after age.
//
// Products.Fex.AetnaAccendo carries the stable prod_<uuid> used on the wire.
// Always use catalog constants — never hardcode prod_ strings or slugs.
const { data } = await isa.zyins.prequalify({
  applicant: {
    dob: '1962-04-18',
    sex: Sex.Male,
    height: Height.fromFeetInches(5, 10),
    weight: Weight.fromPounds(195),
    state: 'NC',
    nicotineUse: { lastUsed: NicotineDuration.Never },
  },
  coverage: Coverage.faceValue(25_000),
  products: ProductSelection.of([Products.Fex.AetnaAccendo]),
});

// ── 3. Print the results ─────────────────────────────────────────────────────
console.log(`Plans found: ${data.plans.length}`);
console.log('');

for (const plan of data.plans) {
  const headline = plan.pricing.find(row => row.primary);
  console.log(
    `  ${plan.carrier.name.padEnd(24)} ${plan.product.name.padEnd(40)} ` +
    `eligible=${String(plan.eligible).padEnd(6)} ` +
    `death_benefit=${(plan.deathBenefit?.amount.display ?? '—').padEnd(10)}`,
  );
  if (headline?.premium) {
    console.log(`    ✓ ${headline.eligibility.category} ${headline.premium.amount.display}`);
  }
}

Run it:

npx tsx prequalify.ts

6. Inspect the response

What you should see:

Plans found: 12

  Aetna Accendo            Aetna Accendo Final Expense              eligible=true   death_benefit=$25,000.00
    ✓ immediate $87.42
  ...

The exact plans and premiums vary by which carriers are enabled on your account.

What the response fields mean:

FieldMeaning
data.plansOne entry per product — flat plans[] array.
plan.carrier.nameCarrier display name (e.g., Aetna Accendo).
plan.product.nameCarrier-formatted product name.
plan.product.idStable prod_<uuid> — use this to carry a selection into a quote or case.
plan.pricing[].eligibility.categoryClosed enum: immediate / graded / rop.
plan.pricing[].premium.amount.centsInteger US cents — the canonical numeric value.
plan.pricing[].premium.amount.displayVerbatim carrier-formatted string (e.g. $87.42).
plan.pricing[].premium.default_modeThe premium.modes key whose value equals amount.
plan.pricing[].premium.modesFull per-mode rate grid (MONTHLY-EFT, ANNUAL, etc.).
plan.deathBenefitFace value as a Money object {cents, display}.

If you get zero plans: The engine returned a valid response, but no carrier accepted the applicant. Try Coverage.faceValue(10_000) or verify the state supports the product.


7. Handle errors

The SDK throws a hierarchy of typed errors. Match on the specific subclass:

import {
  Isa,
  IsaApiError,
  IsaConfigError,
  IsaIdempotencyConflictError,
  IsaRateLimitError,
  IsaValidationError,
  Sex, Height, Weight, NicotineDuration, Coverage, ProductSelection, Products,
} from 'isa-sdk';

const isa = await Isa.withBearer();

try {
  const { data } = await isa.zyins.prequalify({
    applicant: {
      dob: '1962-04-18', sex: Sex.Male,
      height: Height.fromFeetInches(5, 10), weight: Weight.fromPounds(195),
      state: 'NC', nicotineUse: { lastUsed: NicotineDuration.Never },
    },
    coverage: Coverage.faceValue(25_000),
    products: ProductSelection.of([Products.Fex.AetnaAccendo]),
  });
  // happy path
  console.log(data.plans.length, 'plans');
} catch (err) {
  if (err instanceof IsaIdempotencyConflictError) {
    // 409 — you sent two different request bodies under the same idempotency key.
    // This is always a code bug. Log and alert; do NOT automatically retry.
    console.error(`Idempotency conflict on key: ${err.key}`);
  } else if (err instanceof IsaRateLimitError) {
    // 429 — rate limited. Honor Retry-After.
    const wait = err.retryAfterSeconds ?? 60;
    console.error(`Rate limited. Wait ${wait}s before retrying.`);
  } else if (err instanceof IsaValidationError) {
    // 400 — fix your request; do not retry.
    console.error(`Validation error [${err.code}]: ${err.message}`);
  } else if (err instanceof IsaApiError) {
    // Any other API error. Stable `code`, machine-readable.
    console.error(`API error [${err.code}]: ${err.message}`);
    if (err.requestId) console.error(`Request ID: ${err.requestId}`);
  } else if (err instanceof IsaConfigError) {
    console.error('Config error:', err.message);
  } else {
    throw err;
  }
}

8. Add retry with backoff

The SDK retries 5xx and 429 automatically: 3 attempts with exponential backoff, same idempotency key so retries are safe.

For cross-process replay safety (e.g., a job queue that might re-enqueue), mint the idempotency key at dispatch time and pass it explicitly:

import {
  type Isa,
  type PrequalifyRequest,
  type Envelope,
  type PrequalifyV3Result,
  IsaApiError,
  IsaRateLimitError,
  IsaIdempotencyConflictError,
} from 'isa-sdk';

async function prequalifyWithRetry(
  isa: Isa,
  request: PrequalifyRequest,
  maxAttempts = 3,
): Promise<Envelope<PrequalifyV3Result>> {
  // The SDK mints one Idempotency-Key per call and reuses it across its
  // own automatic 5xx/429 retries, so this outer loop is replay-safe.
  let delayMs = 1_000;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await isa.zyins.prequalify(request);
    } catch (err) {
      if (err instanceof IsaApiError && err.code === 'validation_error') throw err;
      if (err instanceof IsaIdempotencyConflictError) throw err;
      if (attempt === maxAttempts) throw err;

      if (err instanceof IsaRateLimitError && err.retryAfterSeconds !== undefined) {
        delayMs = err.retryAfterSeconds * 1_000;
      } else {
        delayMs = Math.min(delayMs * 2, 30_000);
      }
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }
  throw new Error('prequalifyWithRetry: exhausted all attempts');
}

9. Move to live mode

  1. Switch the token. Both test and live tokens arrive in the same checkout email:

    export ISA_TOKEN="<your isa_live_… token>"
  2. No code changes needed. Isa.withBearer() reads the same env var.

  3. Rate limits differ. Live mode enforces stricter per-second and per-day limits. Review rate limits before launching.


What's next

TopicWhere to look
Prequalify deep divePrequalify guide
WebhooksWebhooks guide
Token rotationAuthentication guide
Full API referenceAPI Reference