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-sdkThe 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 bothcents(integer minor units) anddisplay(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.423. 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.plansReuse 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)
raiseThe five you see first:
| Error | When it fires | What to do |
|---|---|---|
IsaConfigError | ISA_TOKEN missing | Set the env var |
ValidationError | 400 from the API | Inspect err.param; fix the input |
IsaApiError | Any other 4xx/5xx | Match on err.code (stable enum) |
IsaIdempotencyConflictError | Same Idempotency-Key, different body | Treat as a bug |
RateLimitError | 429 | Sleep err.retry_after_seconds, retry |
7. Configuration
The defaults match what most consumers need:
| Default | Value | When to override |
|---|---|---|
| Base URL | Production | Set ISA_BASE_URL for staging or self-hosted |
| Timeout | 30s per request | Pass timeout=... to the constructor |
| Retries | 3 attempts, exponential backoff, 5xx + 429 only | Constructor-level transport injection |
Idempotency-Key | UUID v4 minted per call | Pass idempotency_key=... for cross-process replays |
| Transport | httpx | Inject 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
- Prequalify guide — full envelope, eligibility categories, premium mode grid, and field contract
- Custom sorting and grouping — override the default order, group by your own dimension
- Authentication guide — bearer tokens, test vs. live
- Idempotency guide — how the SDK mints keys, when to bring your own
- Error catalog — every
codevalue, status mapping, remediation - Webhooks guide — verifying signed webhook deliveries with
hmac.compare_digest - API Reference — every endpoint, every parameter, every response field
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.
Updated about 10 hours ago