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:
- Reads your API bearer token from an environment variable.
- Asks the ISA underwriting engine whether John Doe qualifies for Final Expense coverage.
- 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
| Requirement | Why |
|---|---|
| Node.js 20 or newer | The SDK uses ES modules and crypto.subtle, available in Node 20 without flags. |
| npm 9+ or pnpm 8+ | Package manager. |
| An ISA API bearer token | See "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_TOKENto 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 tsxAdd a minimal tsconfig.json:
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"outDir": "dist"
},
"include": ["*.ts"]
}
EOF2. Install the SDK
npm install isa-sdkThe 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.tsWhat 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.ts6. 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:
| Field | Meaning |
|---|---|
data.plans | One entry per product — flat plans[] array. |
plan.carrier.name | Carrier display name (e.g., Aetna Accendo). |
plan.product.name | Carrier-formatted product name. |
plan.product.id | Stable prod_<uuid> — use this to carry a selection into a quote or case. |
plan.pricing[].eligibility.category | Closed enum: immediate / graded / rop. |
plan.pricing[].premium.amount.cents | Integer US cents — the canonical numeric value. |
plan.pricing[].premium.amount.display | Verbatim carrier-formatted string (e.g. $87.42). |
plan.pricing[].premium.default_mode | The premium.modes key whose value equals amount. |
plan.pricing[].premium.modes | Full per-mode rate grid (MONTHLY-EFT, ANNUAL, etc.). |
plan.deathBenefit | Face 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
-
Switch the token. Both test and live tokens arrive in the same checkout email:
export ISA_TOKEN="<your isa_live_… token>" -
No code changes needed.
Isa.withBearer()reads the same env var. -
Rate limits differ. Live mode enforces stricter per-second and per-day limits. Review rate limits before launching.
What's next
| Topic | Where to look |
|---|---|
| Prequalify deep dive | Prequalify guide |
| Webhooks | Webhooks guide |
| Token rotation | Authentication guide |
| Full API reference | API Reference |
Updated about 10 hours ago