Prequalify
Best-offer-per-product prequalification with Money death benefits, flat plans[] with uniform pricing tables, and client-side multi-amount grouping.
Prequalify
Prequalify evaluates an applicant against your product roster and returns the underwriting outcome. You get eligible products with prices, and ineligible products with reasons why.
v3 is the active surface. The body is nested (
applicant/coverage/products), money is integer cents end to end, and condition / medication / nicotine rows are catalog-aware via a{ id?, text? }shape. v2 keeps working for existing integrators — see Migrating from v2 to v3 for the field-by-field mapping.
v3 request
curl https://zyins.isaapi.com/v3/prequalify \
-H "Authorization: Bearer $ISA_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"applicant": {
"sex": "male",
"dob": "1962-04-18",
"height_inches": 70,
"weight_lbs": 195,
"conditions": [
{ "id": "cond_01KSR2WVAGC05ZGR6FA4QYEA8X", "text": "High Blood Pressure",
"was_diagnosed": "5 YEARS AGO", "last_treatment": "2 MONTHS AGO" },
{ "text": "Postural orthostatic tachycardia syndrome",
"was_diagnosed": "1 YEAR AGO", "last_treatment": "1 MONTH AGO" }
],
"medications": [
{ "id": "med_01KSR2WVAGC05ZGR6FA4QYEB12", "text": "Lisinopril",
"use": "High Blood Pressure", "dosage": "20mg",
"first_fill": "5 YEARS AGO", "last_fill": "1 MONTH AGO" },
{ "text": "NewExperimental XR 2026", "use": "Sleep", "dosage": "10mg",
"first_fill": "3 MONTHS AGO", "last_fill": "1 WEEK AGO" }
],
"nicotine": {
"last_used": "within_12_months",
"specificity": [
{ "id": "nic_01KSR2WVAGC05ZGR6FA4QYEA8T", "text": "Marijuana",
"frequency": "few_times_per_week" },
{ "text": "Snus", "frequency": "daily" }
]
}
},
"coverage": { "face_amount_cents": 5000000, "state": "NC" },
"products": ["prod_a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
}'This evaluates John Doe (NC, DOB 1962-04-18, 5'10", 195 lbs) with two conditions, two medications, and two nicotine specificity rows, for $50,000 of face value. products is an array of prod_<uuid> identifiers from the typed catalog constants — see ProductSelection.of([Products.Term.FidelityLifeInstabrainTerm]) for the SDK form.
Catalog IDs and freeform text
Every row in conditions[], medications[], and nicotine.specificity[] takes both an id and a text field — at least one is required.
id — a kind-prefixed ULID from the catalog (cond_<26-char-ULID>, med_<26-char-ULID>, nic_<26-char-ULID>). Opaque to consumers. Stable within a dataset version. Resolved server-side against the catalog before evaluation.
text — the human-typed identifier (drug name, condition phrase, nicotine product). The engine canonicalizes, expands aliases, and corrects spelling.
Both fields present — id wins for catalog lookup; text is preserved on the case for audit and round-trips.
text only — the engine canonicalizes the string. Unknown values fall back to the catalog's OTHER entry engine-side, just like on v2.
Do not persist catalog IDs across dataset versions. Persist
.nameon your business record. Catalog IDs are stable within a dataset version; rebuilds replace them. If you store an ID, also store the dataset version it was issued against — see Reference catalog.
nicotine
nicotine"nicotine": {
"last_used": "within_12_months",
"specificity": [
{ "id": "nic_01KSR2WVAGC05ZGR6FA4QYEA8T", "text": "Marijuana", "frequency": "few_times_per_week" },
{ "text": "Snus", "frequency": "daily" }
]
}last_used— closed enum:never | within_12_months | within_24_months | within_5_years | over_5_years_ago. Required whennicotineis present.specificity[]— zero or more per-product rows. Each carries anidand/ortext(same rule as conditions and medications) plus afrequencyenum.specificity[].frequency—NicotineFrequencyV3:daily | few_times_per_week | few_times_per_month | few_times_per_year.
Omitting nicotine is equivalent to { "last_used": "never" }.
coverage
coverageSingle face amount
"coverage": { "face_amount_cents": 5000000, "state": "NC" }face_amount_cents— integer cents.5_000_000is $50,000 of face value. This is a singular face amount — one prequalify request, one coverage target.state— two-letter US state code, uppercase. Used for licensing and state-availability filtering.
Multiple face amounts
"coverage": {
"state": "NC",
"quote_options": {
"quote_type": "face_amounts",
"amounts": ["10000", "25000", "50000"]
}
}The response is still a flat data.plans[] array — the same product appears once per requested amount, each entry carrying its own death_benefit (period null). Group client-side by death_benefit.amount.cents for a side-by-side comparison table.
Monthly budget (single or multiple)
"coverage": {
"state": "NC",
"quote_options": {
"quote_type": "monthly_budget",
"amounts": ["7500"]
}
}Monthly-budget offers carry a budget Money (period monthly) — the requested budget, distinct from the solved premium — alongside the death_benefit the budget buys. Group client-side by budget.amount.cents for multi-budget requests. Single monthly budget is supported.
products[]
products[]"products": ["prod_a1b2c3d4-e5f6-7890-abcd-ef1234567890"]A list of product ids (prod_<uuid>) to evaluate. The SDK catalog constants carry the correct id for each product — use ProductSelection.of([Products.Term.FidelityLifeInstabrainTerm]) rather than hardcoding the string. Omit products to evaluate every product on the account roster.
Required vs optional
Required at the body level: applicant, coverage. Required inside applicant: sex, dob, height_inches, weight_lbs.
Everything else — conditions, medications, nicotine, products, include_ineligible — is optional. See the POST /v3/prequalify reference for the full field list and the Glossary for domain terms.
Money inputs are integer cents.
coverage.face_amount_cents: 5000000means $50,000 of face value, not 5,000,000¢ × something exotic. Money outputs ship as integer cents plus a verbatim carrier-formatted display string — see Pricing.
Recommended input pipeline (v3)
The v3 SDK ships input adapters that compose into a clean "agent typed text → canonical name" pipeline:
- Autocorrect the user's typing (
isa.zyins.autocorrector.correct(text, { mode })). - Match the corrected text to a canonical
Concept(isa.zyins.conditions.match(text)/medications.match(text)). The handle carriesname(canonical),inputText(verbatim), andid(nullfor anUnknownConcept). - Submit the prequalify request with the canonical concept name on each condition and medication row.
import {
Isa, Sex, Height, Weight, NicotineDuration, Coverage, ProductSelection, Products,
} from 'isa-sdk';
const isa = await Isa.withBearer();
await isa.zyins.datasets.get();
function pickCondition(rawText: string) {
const corrected = isa.zyins.autocorrector.correct(rawText, { mode: 'submit' });
return isa.zyins.conditions.match(corrected);
}
const hbp = pickCondition('hi blod pressur'); // → HIGH BLOOD PRESSURE
const lisinopril = isa.zyins.medications.match('lisinopril');
const { data } = await isa.zyins.prequalify({
applicant: {
sex: Sex.Male,
dob: '1962-04-18',
height: Height.fromFeetInches(5, 10),
weight: Weight.fromPounds(195),
state: 'NC',
nicotineUse: { lastUsed: NicotineDuration.Never },
conditions: [
{ name: hbp.name, wasDiagnosed: '5 YEARS AGO', lastTreatment: '2 MONTHS AGO' },
],
medications: [
{
name: lisinopril.name,
use: 'High Blood Pressure',
firstFill: '5 YEARS AGO',
lastFill: '1 MONTH AGO',
},
],
},
coverage: Coverage.faceValue(25_000),
products: ProductSelection.of([Products.Fex.AetnaAccendo]),
});
void data;How it works:
-
A
Concepthandle exposesname(canonical, or verbatim if unknown),inputText(always verbatim), andid(nullfor unknown). Passconcept.nameto the row; the engine canonicalizes, expands aliases, and corrects spelling server-side. -
The wire body carries the catalog-aware
{ id?, text? }row shape. The SDK builds it for you from typedCondition/Medication. -
For more detail, see Autocorrect, Match, Autocomplete, and Reference matching.
Declined products and alternate tiers
By default, prequalify returns both qualifying and declined results:
- Ineligible rate classes appear as
pricing[]rows witheligibility.eligible: false - Products the applicant didn't qualify for appear as top-level entries with
eligible: false
This default lets you show the full slate and explain declines without a second request. Pass include_ineligible: false to return only qualifying offers.
# Default — qualifying offers plus declined products and declined tiers
curl https://zyins.isaapi.com/v3/prequalify \
-H "Authorization: Bearer $ISA_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"applicant": {
"sex": "male", "dob": "1962-04-18",
"height_inches": 70, "weight_lbs": 195
},
"coverage": { "face_amount_cents": 1500000, "state": "NC" },
"products": ["prod_c8f21a4b-9e3d-4b5c-a1e2-f3d4e5f6a7b8"]
}'# Opt out — return only qualifying offers and qualifying tiers
curl https://zyins.isaapi.com/v3/prequalify \
-H "Authorization: Bearer $ISA_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"applicant": {
"sex": "male", "dob": "1962-04-18",
"height_inches": 70, "weight_lbs": 195
},
"coverage": { "face_amount_cents": 1500000, "state": "NC" },
"products": ["prod_c8f21a4b-9e3d-4b5c-a1e2-f3d4e5f6a7b8"],
"include_ineligible": false
}'On v2? The legacy
/v2/prequalifywire shape (flatdate_of_birth/gender/height/weight+quote_options) continues to serve existing integrators. The published reference shows only the current v3 surface — see Migrating from v2 to v3 for the field-by-field mapping.
Response — flat plans[] with Money primitive
plans[] with Money primitiveThe v3 response is always a flat data.plans[] array — one entry per product per requested amount. Each offer has a death_benefit Money value and a pricing[] table. Group multi-amount responses client-side; see Multi-amount requests below.
The example below shows qualifying rows only. By default the response also includes ineligible rows and declined products — see the full default response further down.
{
"object": "prequalify_result",
"request_id": "req_01HZK2N5GQR9T8X4B6FJW3Y1AS",
"idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
"livemode": true,
"data": {
"plans": [
{
"object": "plan_offer",
"id": "9b7d9b5c-1f3a-5c2b-9a4f-6e1c2d3b4a5e",
"eligible": true,
"carrier": {
"id": "7a3e4b5c-6d7e-5f80-8a9b-0c1d2e3f4a5b",
"name": "American Amicable",
"logo_url": "https://zyins.isaapi.com/v1/logo/American%20Amicable"
},
"product": {
"id": "prod_c8f21a4b-9e3d-4b5c-a1e2-f3d4e5f6a7b8",
"name": "American Amicable Golden Solution",
"class": "fex"
},
"plan_info": [
{ "key": "eapp", "label": "eApp", "values": ["https://www.insuranceapplication.com/cgi/webappmobile/login.aspx?ReturnUrl=/cgi/webappmobile/"] },
{ "key": "telesales", "label": "Telesales", "values": ["Required in all written states"] }
],
"death_benefit": { "amount": { "cents": 1500000, "display": "$15,000" }, "period": null },
"pricing": [
{
"rate_class": "graded",
"primary": true,
"rank": 8,
"eligibility": {
"eligible": true,
"category": "graded",
"coverage_tier": "graded",
"reasons": []
},
"premium": {
"amount": { "cents": 12240, "display": "$122.40" },
"default_mode": "MONTHLY-EFT",
"modes": {
"MONTHLY-EFT": { "cents": 12240, "display": "$122.40" },
"Monthly-ADB": { "cents": 12636, "display": "$126.36" },
"QUARTERLY": { "cents": 36439, "display": "$364.39" },
"SEMI-ANNUAL": { "cents": 72183, "display": "$721.83" },
"ANNUAL": { "cents": 139080, "display": "$1390.80" }
}
}
},
{
"rate_class": "rop",
"primary": false,
"rank": 10,
"eligibility": {
"eligible": true,
"category": "rop",
"coverage_tier": "rop",
"reasons": []
},
"premium": {
"amount": { "cents": 14309, "display": "$143.09" },
"default_mode": "MONTHLY-EFT",
"modes": {
"MONTHLY-EFT": { "cents": 14309, "display": "$143.09" },
"QUARTERLY": { "cents": 42602, "display": "$426.02" },
"SEMI-ANNUAL": { "cents": 84390, "display": "$843.90" },
"ANNUAL": { "cents": 162600, "display": "$1626.00" }
}
}
}
],
"metadata": {}
}
]
}
}Response — with the default include_ineligible: true
include_ineligible: trueBy default, ineligible rate classes appear as additional rows in pricing[] with eligibility.eligible: false, and products the applicant didn't qualify for appear as top-level entries with eligible: false. Pass include_ineligible: false to drop both.
{
"data": {
"plans": [
{
"object": "plan_offer",
"id": "9b7d9b5c-1f3a-5c2b-9a4f-6e1c2d3b4a5e",
"eligible": true,
"carrier": { "name": "American Amicable" },
"product": { "id": "prod_c8f21a4b-9e3d-4b5c-a1e2-f3d4e5f6a7b8", "name": "American Amicable Golden Solution", "class": "fex" },
"death_benefit": { "amount": { "cents": 1500000, "display": "$15,000" }, "period": null },
"pricing": [
{
"rate_class": "graded",
"primary": true,
"rank": 8,
"eligibility": { "eligible": true, "category": "graded", "reasons": [] },
"premium": { "amount": { "cents": 12240, "display": "$122.40" }, "default_mode": "MONTHLY-EFT", "modes": {...} }
},
{
"rate_class": "rop",
"primary": false,
"rank": 10,
"eligibility": { "eligible": true, "category": "rop", "reasons": [] },
"premium": { "amount": { "cents": 14309, "display": "$143.09" }, "default_mode": "MONTHLY-EFT", "modes": {...} }
},
{
"rate_class": "immediate",
"primary": false,
"rank": null,
"eligibility": {
"eligible": false,
"category": "immediate",
"coverage_tier": null,
"reasons": ["Diabetes diagnosed within 12 months"]
}
}
]
},
{
"object": "plan_offer",
"id": "0e1c2a3b-4d5e-5f6a-7b8c-9d0e1f2a3b4c",
"eligible": false,
"carrier": { "name": "Sample Carrier" },
"product": { "id": "prod_b9e34c5d-2f1a-4b6c-9d8e-7f0a1b2c3d4e", "name": "Sample Carrier Final Expense", "class": "fex" },
"death_benefit": { "amount": { "cents": 1500000, "display": "$15,000" }, "period": null },
"pricing": [
{
"rate_class": "default",
"primary": false,
"rank": null,
"eligibility": {
"eligible": false,
"category": null,
"coverage_tier": null,
"reasons": ["Due to age", "Diabetes diagnosed within 12 months"]
}
}
],
"metadata": {}
}
]
}
}Field contract
One row per (product × requested_amount). The response is a flat data.plans[]. In multi-amount requests, the same product appears once per amount with its own death_benefit (or budget). The offer id is amount-specific — to compare the same product across amounts, match on product.id (the prod_<uuid>), not offer id.
pricing[] is uniform. Every rate class for a product is a sibling row. The best qualifying class has primary: true. Ineligible classes have eligibility.eligible: false and no premium.
Money shape: { amount: {cents, display}, period }
death_benefithasperiod: null(one-time lump sum)centsis the integer value for math;displayis the carrier-formatted string- Monthly-budget offers also carry a
budgetMoney withperiod: "monthly"
premium: {amount, default_mode, modes} (no period).
premium.amount({cents, display}) is the default-mode headline — the guaranteed apples-to-apples comparison value across rowspremium.default_modenames whichmodeskey equalsamountpremium.modesis the full carrier grid keyed by carrier-supplied mode strings- The mode string itself carries the cadence, so premium needs no
period
eligibility.category — closed enum immediate | graded | rop | other on qualifying rows. null when eligible: false.
eligibility.coverage_tier — the carrier rate-class string verbatim. null when the carrier doesn't segment by rate class, or when the row is ineligible.
rank — null when ineligible. Qualifying rows have a server-assigned rank; lower is better.
product.class — canonical product family: fex, term, medsup, preneed, annuity. Match defensively; new families ship without a version bump.
product.id — the stable prod_<uuid> that identifies the product across renames. Use this to carry a product selection into a quote or case creation.
plan_info — an ordered, typed array Array<{ key: string, label: string, values: string[] }>. As of 2026-05-26 this replaced the previous Map<string, string[]> shape.
key — machine-readable snake_case identifier, stable across carriers. Switch on key for icons, hide-show logic, and dispatch. Known keys: eapp, nicotine, payment_options, phone_number, replacement_policy, riders, social_security_number, submitting_applications, telephone_interview, telesales. Match defensively; carriers add new keys without a version bump.
label — display-ready Title Case heading. ALL-CAPS carrier input is title-cased server-side (PHONE NUMBER → Phone Number). Mixed-case carrier input is preserved (eApp stays eApp). Acronyms stay uppercase (SSN, EFT, URL, ACH, PDF, API). Render verbatim.
values — list of guidance strings for this entry. Order is the carrier-authored order. URLs are URL-decoded (no percent encoding).
Order — items prefixed 1., 2., … sort by integer ascending; unnumbered items follow in deterministic source-key order. The numeric prefix is stripped from both key and label.
"plan_info": [
{ "key": "phone_number", "label": "Phone Number", "values": ["866-272-6630"] },
{ "key": "submitting_applications", "label": "Submitting Applications", "values": ["eApp", "Paper App"] },
{ "key": "payment_options", "label": "Payment Options", "values": ["Bank Draft/EFT"] },
{ "key": "telephone_interview", "label": "Telephone Interview", "values": ["Not required"] },
{ "key": "social_security_number", "label": "SSN Requirements", "values": ["Required"] },
{ "key": "telesales", "label": "Telesales", "values": ["Required in all written states"] }
]Multi-amount requests: client-side grouping
The response is always a flat data.plans[] — single amount or many. On a multi-amount request (Coverage.faceValues([…]) or Coverage.monthlyBudgets([…])), the same product appears once per requested amount. Use the byAmount helper to group client-side: face-amount offers key off deathBenefit.amount.cents, monthly-budget offers off budget.amount.cents.
import {
Isa, Sex, Height, Weight, NicotineDuration, Coverage, ProductSelection, Products,
} from 'isa-sdk';
import { byAmount } from 'isa-sdk/zyins';
const isa = await Isa.withBearer();
const { data } = await isa.zyins.prequalify({
applicant: {
sex: Sex.Male,
dob: '1962-04-18',
height: Height.fromFeetInches(5, 10),
weight: Weight.fromPounds(195),
state: 'NC',
nicotineUse: { lastUsed: NicotineDuration.Never },
},
coverage: Coverage.faceValues([10_000, 25_000, 50_000]),
products: ProductSelection.of([Products.Term.FidelityLifeInstabrainTerm]),
});
for (const [cents, offers] of byAmount(data.plans)) {
console.log(`Amount ${offers[0].deathBenefit?.amount.display} (${cents}¢): ${offers.length} offers`);
for (const offer of offers) {
const headline = offer.pricing.find(r => r.primary);
console.log(` ${offer.carrier.name}: ${headline?.premium?.amount.display}`);
}
}The byAmount helper automatically picks the grouping dimension: death_benefit.amount.cents for face-amount responses, budget.amount.cents for budget responses. The shape is always a flat plans[], regardless of amount count.
Advanced: custom sorting, grouping, and matching
The flat plans[] shape lets you customize without hitting the API again. The server sets rank (lower is better) and byAmount covers the common grouping. Everything else is client-side transforms on the array you already have.
Sort by your own key
plans[] arrives in server rank order. To reorder — by price, carrier name, or your own score — sort the array. Extract the headline premium from each offer's primary pricing row and sort on cents (never on display, which is a formatted string):
import type { V3Offer } from 'isa-sdk/zyins';
function sortOffers(plans: readonly V3Offer[]) {
const headlineCents = (offer: V3Offer): number =>
offer.pricing.find(row => row.primary)?.premium?.amount.cents ?? Number.MAX_SAFE_INTEGER;
// Cheapest qualifying offer first.
const byPrice = [...plans].sort((a, b) => headlineCents(a) - headlineCents(b));
// Carrier name A→Z.
const byCarrier = [...plans].sort((a, b) => a.carrier.name.localeCompare(b.carrier.name));
return { byPrice, byCarrier };
}
void sortOffers;Copy the array before sorting ([...plans]) so the original server order stays available for an "as ranked" toggle.
Group by your own dimension
byAmount groups by coverage dimension. To group by something else — carrier, product family, eligibility category — fold the flat array into your own Map:
import type { V3Offer } from 'isa-sdk/zyins';
function groupBy<K>(offers: readonly V3Offer[], key: (o: V3Offer) => K): Map<K, V3Offer[]> {
const out = new Map<K, V3Offer[]>();
for (const offer of offers) {
const bucket = out.get(key(offer)) ?? [];
bucket.push(offer);
out.set(key(offer), bucket);
}
return out;
}
const groupByFamily = (plans: readonly V3Offer[]) => groupBy(plans, o => o.product.type);
const groupByCarrier = (plans: readonly V3Offer[]) => groupBy(plans, o => o.carrier.name);
void groupByFamily; void groupByCarrier;Resolve freeform input with medications.match / conditions.match
medications.match / conditions.matchWhen you collect conditions or medications as freeform text, resolve them to canonical concepts before submitting. Both isa.zyins.medications.match() and isa.zyins.reference.medications.match() work — use whichever reads clearer. The same dual access is available for conditions. match returns a Concept whose name you pass to the prequalify row. The engine still canonicalizes server-side, so unknown strings never reject:
import { Isa } from 'isa-sdk';
const isa = await Isa.withBearer();
await isa.zyins.datasets.get();
const lisinopril = isa.zyins.medications.match('lisinopril');
console.log(lisinopril.name, lisinopril.id);Both-sort: Most-Common vs Alphabetical
A matched concept exposes related concepts in the order you choose. Sort.MostCommonFirst (default) ranks by prescription frequency — "what most applicants pick." Sort.Alphabetical emits the same set A→Z for a scannable picker. The same toggle works on autocomplete:
import { Isa } from 'isa-sdk';
const isa = await Isa.withBearer();
await isa.zyins.datasets.get();
const lisinopril = isa.zyins.medications.match('lisinopril');
// Conditions this drug treats, most-prescribed first (the default).
const common = lisinopril.conditions(isa.zyins.reference.Sort.MostCommonFirst);
// Same set, A→Z — for an alphabetical picker toggle.
const alpha = lisinopril.conditions(isa.zyins.reference.Sort.Alphabetical);
// Autocomplete honors the same axis.
const azSuggestions = await isa.zyins.medications.autocomplete('lisi', {
limit: 5,
sort: isa.zyins.reference.Sort.Alphabetical,
});
void common; void alpha; void azSuggestions;For full details, see Reference matching, Match, and Autocomplete.
Idempotency
Send a UUID v4 in Idempotency-Key on every call — see Idempotency. The response envelope echoes the key back at idempotency_key so you can correlate without parsing headers.
Errors
All errors are RFC 7807 ProblemDetails on application/problem+json. See Errors for the full code catalog.
See also
- Pricing — reading
death_benefit,budget, andpricing[]premium values. POST /v3/quote— identical response shape, broader product set.- Glossary —
plan_offer,eligibility category,rank,rate_class,product.class, FEX, ROP, medsup, term, preneed. - Authentication
- Idempotency
Updated about 10 hours ago