FastAPI

Integrate the ISA SDK into a FastAPI application: lifespan singleton, dependency injection, and async webhook verification.

FastAPI

This guide wires the ISA SDK into a FastAPI application using the lifespan pattern for a singleton client, dependency injection for per-route access, and async-safe webhook verification.

Prerequisites

  • Python 3.10+, FastAPI 0.110+
  • pip install isa-sdk fastapi uvicorn
  • ISA_TOKEN in your environment

Singleton via lifespan

The Isa client is thread-safe and intended to be constructed once. FastAPI's lifespan context manager is the correct place.

# main.py
import os
from contextlib import asynccontextmanager
from typing import AsyncGenerator

from fastapi import FastAPI
from isa_sdk.zyins import Isa


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    # Reads ISA_TOKEN from the environment.
    app.state.isa = Isa.with_bearer()
    yield
    # SDK has no async teardown — nothing to do here.


app = FastAPI(lifespan=lifespan)

Dependency injection

# dependencies.py
from fastapi import Request
from isa_sdk.zyins import Isa


def get_isa(request: Request) -> Isa:
    return request.app.state.isa

Prequalify endpoint

# routes/prequalify.py
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel

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

from dependencies import get_isa

router = APIRouter()


class PrequalifyRequest(BaseModel):
    dob: str
    state: str
    height_inches: int
    weight_pounds: int


class PlanOut(BaseModel):
    carrier: str
    product: str
    category: str | None          # 'immediate' | 'graded' | 'rop'
    premium_display: str | None   # verbatim carrier string
    premium_cents: int | None     # integer cents


class PrequalifyResponse(BaseModel):
    plans: list[PlanOut]
    request_id: str


@router.post("/prequalify", response_model=PrequalifyResponse)
def run_prequalify(
    body: PrequalifyRequest,
    isa: Annotated[Isa, Depends(get_isa)],
) -> PrequalifyResponse:
    try:
        # The SDK mints a UUID v4 idempotency key per call automatically.
        result = isa.zyins.prequalify(
            applicant=Applicant(
                dob=body.dob,
                sex=Sex.MALE,
                height_inches=body.height_inches,
                weight_pounds=body.weight_pounds,
                state=body.state,
                nicotine_use=NicotineUsageInput(last_used=NicotineDuration.NEVER),
            ),
            coverage=Coverage.face_value(25_000),
            products=ProductSelection.of(Products.Fex.AetnaAccendo),
        )
    except ValidationError as err:
        raise HTTPException(status_code=400, detail={"param": err.param, "message": str(err)})
    except IsaApiError as err:
        raise HTTPException(status_code=502, detail={"code": err.code, "request_id": err.request_id})

    plans: list[PlanOut] = []
    for offer in result.data.plans:
        primary = next((row for row in offer.pricing if row.primary), None)
        premium = primary.premium if primary else None
        plans.append(PlanOut(
            carrier=offer.carrier.name,
            product=offer.product.name,
            category=primary.eligibility.category if primary else None,
            premium_display=premium.amount.display if premium else None,
            premium_cents=premium.amount.cents if premium else None,
        ))
    return PrequalifyResponse(plans=plans, request_id=result.request_id)

Register the router in main.py:

from routes.prequalify import router as prequalify_router

app.include_router(prequalify_router)

Async note

The isa-sdk client is synchronous. For high-concurrency FastAPI applications, run SDK calls in a thread pool to avoid blocking the event loop:

import asyncio

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


async def run_quote(isa: Isa):
    return await asyncio.to_thread(
        lambda: 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),
        )
    )

Webhook verification

# routes/webhooks.py
import hashlib
import hmac
import json
import os
import time

from fastapi import APIRouter, HTTPException, Request, Response

router = APIRouter()

WEBHOOK_SECRET = os.environ["ISA_WEBHOOK_SECRET"]
TOLERANCE_SECONDS = 300


def _verify(raw_body: bytes, sig_header: str) -> bool:
    # X-ZyINS-Signature is "t=<unix-seconds>,v1=<hex>".
    fields = dict(part.split("=", 1) for part in sig_header.split(",") if "=" in part)
    try:
        ts = int(fields["t"])
        sig = fields["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - ts) > TOLERANCE_SECONDS:
        return False
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        f"{ts}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig)


@router.post("/webhooks/isa")
async def receive_webhook(request: Request) -> Response:
    raw = await request.body()
    sig = request.headers.get("X-ZyINS-Signature", "")
    if not _verify(raw, sig):
        raise HTTPException(status_code=400, detail="invalid signature")

    event = json.loads(raw)
    # Dispatch on event["type"]
    return Response(status_code=200)

See also