Quickstart: Python

Get a prequalify decision from a fresh Python project in under five minutes.

Python Quickstart

Get a prequalify decision from a fresh Python project in under five minutes.

No boilerplate: pydantic v2 models for inputs, typed exceptions for failures, one flat result.data.plans list on the way out.

1. Install

pip install isa-sdk

The SDK targets Python 3.10+ and ships type stubs. Works with mypy strict mode.

2. Hello world

Set the bearer token and run prequalify:

export ISA_TOKEN="<paste your isa_test_… key here>"
from isa_sdk import Isa
from isa_sdk.zyins import (
    Applicant, NicotineDuration, NicotineUsageInput, Sex, Coverage, ProductSelection,
)
from isa_sdk.catalog import Products

isa = Isa.with_bearer()

result = isa.zyins.prequalify(
    applicant=Applicant(
        dob="1962-04-18",
        sex=Sex.MALE,
        height_inches=70,
        weight_pounds=195,
        state="NC",
        nicotine_use=NicotineUsageInput(last_used=NicotineDuration.NEVER),
    ),
    coverage=Coverage.face_value(25_000),
    products=ProductSelection.of(Products.Fex.AetnaAccendo),
)

# result.data.plans is a flat list — one entry per product.
for offer in result.data.plans:
    headline = next((row for row in offer.pricing if row.primary), None)
    if headline is not None and headline.premium is not None:
        print(offer.carrier.name, offer.product.name, headline.premium.amount.display)

Isa.with_bearer() reads ISA_TOKEN from the environment. No transport setup, no request plumbing.

Money inputs are integer dollars. Coverage.face_value(25_000) means $25,000 of death benefit, not 25,000 cents. Outputs always carry both cents (integer minor units) and display (the verbatim carrier string), so you never have to decide formatting on the way out.

Run it. The expected output is one line per qualifying offer:

Aetna Accendo Aetna Accendo Final Expense $87.42

3. A real example

A full prequalify sequence for the project's canonical applicant: John Doe, born 1962-04-18, North Carolina, 5'10", 195 lb, no medications, evaluated against Aetna Accendo final expense at $25,000 of face value.

from isa_sdk import Isa
from isa_sdk.zyins import (
    Applicant, NicotineDuration, NicotineUsageInput, Sex, Coverage, ProductSelection,
)
from isa_sdk.catalog import Products

isa = Isa.with_bearer()

john = Applicant(
    dob="1962-04-18",
    sex=Sex.MALE,
    height_inches=70,
    weight_pounds=195,
    state="NC",
    nicotine_use=NicotineUsageInput(last_used=NicotineDuration.NEVER),
)

result = isa.zyins.prequalify(
    applicant=john,
    coverage=Coverage.face_value(25_000),
    products=ProductSelection.of(Products.Fex.AetnaAccendo),
)

for offer in result.data.plans:
    print((offer.death_benefit.amount.display if offer.death_benefit else "n/a (medsup)"))  # "$25,000.00"

    # Eligibility and premium live on each pricing[] row, not the offer.
    headline = next((row for row in offer.pricing if row.primary), None)
    if headline is not None and headline.premium is not None:
        print(
            offer.carrier.name,
            headline.rate_class,
            headline.premium.amount.display,      # verbatim carrier string
            headline.premium.amount.cents,        # integer cents for arithmetic
        )

    # Alternate qualifying tiers (e.g. graded when best is immediate) are sibling rows.
    for row in offer.pricing:
        if row.primary or not row.eligibility.eligible or row.premium is None:
            continue
        print("  also:", row.rate_class, row.premium.amount.display, row.eligibility.category)

Every call returns an Envelope: the offers live at result.data.plans, with result.request_id, result.idempotency_key, and result.livemode alongside as provenance. Persist request_id and idempotency_key next to your business records — they correlate your call to the server-side trace.

Async

The SDK is sync by default. For asyncio applications, run each call in a worker thread so the event loop stays responsive:

import asyncio

from isa_sdk import Isa
from isa_sdk.zyins import Applicant, Coverage, ProductSelection

isa = Isa.with_bearer()

async def prequalify_one(applicant: Applicant, products: ProductSelection):
    result = await asyncio.to_thread(
        lambda: isa.zyins.prequalify(
            applicant=applicant,
            coverage=Coverage.face_value(25_000),
            products=products,
        )
    )
    return result.data.plans

Reuse the same Isa instance across asyncio.gather(...) calls.

4. Multi-amount face-value quote

Request several coverage amounts in one call with Coverage.face_values([…]). The response is a flat result.data.plans list where the same product appears once per requested amount.

Use the by_amount helper to group the results client-side.

from isa_sdk import Isa
from isa_sdk.zyins import (
    Applicant, NicotineDuration, NicotineUsageInput, Sex, Coverage, ProductSelection, by_amount,
)
from isa_sdk.catalog import Products

isa = Isa.with_bearer()

result = isa.zyins.prequalify(
    applicant=Applicant(
        dob="1962-04-18",
        sex=Sex.MALE,
        height_inches=70,
        weight_pounds=195,
        state="NC",
        nicotine_use=NicotineUsageInput(last_used=NicotineDuration.NEVER),
    ),
    coverage=Coverage.face_values([10_000, 25_000, 50_000]),
    products=ProductSelection.of(Products.Term.FidelityLifeInstabrainTerm),
)

# Flat plans, grouped client-side by death_benefit.amount.cents.
for cents, offers in by_amount(result.data.plans).items():
    amount_disp = offers[0].death_benefit.amount.display if offers[0].death_benefit else 'n/a'
    print(f"Amount {amount_disp} ({cents}c):")
    for offer in offers:
        headline = next((row for row in offer.pricing if row.primary), None)
        if headline is not None and headline.premium is not None:
            print(f"  {offer.carrier.name}: {headline.premium.amount.display}")

5. Monthly-budget prequalify

Solve for coverage given a monthly budget. Use Coverage.monthly_budget(75) where 75 is dollars per month.

Each offer includes both budget (the monthly cost) and death_benefit (the coverage it buys).

from isa_sdk import Isa
from isa_sdk.zyins import (
    Applicant, NicotineDuration, NicotineUsageInput, Sex, Coverage, ProductSelection,
)
from isa_sdk.catalog import Products

isa = Isa.with_bearer()

result = isa.zyins.prequalify(
    applicant=Applicant(
        dob="1962-04-18",
        sex=Sex.MALE,
        height_inches=70,
        weight_pounds=195,
        state="NC",
        nicotine_use=NicotineUsageInput(last_used=NicotineDuration.NEVER),
    ),
    coverage=Coverage.monthly_budget(75),  # $75/mo budget
    products=ProductSelection.of(Products.Fex.AetnaAccendo),
)

for offer in result.data.plans:
    headline = next((row for row in offer.pricing if row.primary), None)
    if offer.budget is not None and headline is not None and headline.premium is not None:
        coverage_disp = offer.death_benefit.amount.display if offer.death_benefit else 'n/a (medsup)'
        print(f"Budget: {offer.budget.amount.display} -> Coverage: {coverage_disp}")
        print(f"  Premium: {headline.premium.amount.display} ({headline.rate_class})")

6. Errors

Every SDK exception inherits from ISAError. Match on the specific subclass — don't parse str(err):

import logging
import time

from isa_sdk import Isa
from isa_sdk.zyins import (
    IsaApiError,
    IsaIdempotencyConflictError,
    IsaConfigError,
    PrequalifyRequest,
    RateLimitError,
    ValidationError,
)

log = logging.getLogger(__name__)
isa = Isa.with_bearer()
request: PrequalifyRequest

try:
    result = isa.zyins.prequalify(request)
except IsaIdempotencyConflictError as err:
    # Replayed key with a different body. Almost always a bug — log and bail.
    log.error("idempotency conflict: %s first seen %s", err.key, err.first_seen_at)
    raise
except ValidationError as err:
    # 400 from the API. err.param names the offending JSON pointer when known.
    log.error("%s: %s", err.param, err)
    raise
except RateLimitError as err:
    # 429. retry_after_seconds is populated when the server provides Retry-After.
    time.sleep(err.retry_after_seconds or 1)
except IsaApiError as err:
    # Any other 4xx / 5xx. Match on err.code (stable enum), log err.request_id.
    log.error("%s: %s (request %s)", err.code, err, err.request_id)
    raise
except IsaConfigError as err:
    # Raised synchronously when ISA_TOKEN is missing.
    log.error("config error: %s", err)
    raise

The five you see first:

ErrorWhen it firesWhat to do
IsaConfigErrorISA_TOKEN missingSet the env var
ValidationError400 from the APIInspect err.param; fix the input
IsaApiErrorAny other 4xx/5xxMatch on err.code (stable enum)
IsaIdempotencyConflictErrorSame Idempotency-Key, different bodyTreat as a bug
RateLimitError429Sleep err.retry_after_seconds, retry

7. Configuration

The defaults match what most consumers need:

DefaultValueWhen to override
Base URLProductionSet ISA_BASE_URL for staging or self-hosted
Timeout30s per requestPass timeout=... to the constructor
Retries3 attempts, exponential backoff, 5xx + 429 onlyConstructor-level transport injection
Idempotency-KeyUUID v4 minted per callPass idempotency_key=... for cross-process replays
TransporthttpxInject your own for proxies, mocking, telemetry

If you find yourself overriding more than one of these in normal usage, file an issue — the default is probably wrong.

8. What's next

Need raw HTTP details (status, headers, request ID)? Pass idempotency_key=... and read the envelope fields on the result — result.request_id and result.idempotency_key carry the same provenance the response headers do.