Flask

Integrate the ISA SDK into a Flask application: application factory, client helper, and webhook blueprint.

Flask

This guide wires the ISA SDK into a Flask application using the application factory pattern. One Isa client is created per process, stored on the Flask app object, and retrieved through a small helper. The client is thread-safe, so a single instance serves every request a worker handles.

Prerequisites

  • Python 3.10+, Flask 3.0+
  • pip install isa-sdk flask
  • ISA_TOKEN in your environment — your API token (isa_test_… while you build, isa_live_… in production). Tokens arrive by email after checkout; contact support if you need one reissued.

The SDK authenticates with this bearer token alone. There is nothing else to configure.

Application factory

# app/__init__.py
from flask import Flask
from isa_sdk import Isa


def create_app() -> Flask:
    app = Flask(__name__)
    # Reads ISA_TOKEN from the environment and authenticates with it.
    app.isa = Isa.with_bearer()

    from .routes import bp
    app.register_blueprint(bp)

    from .webhooks import wh
    app.register_blueprint(wh, url_prefix="/webhooks")

    return app

Helper to retrieve the client

# app/isa_client.py
from flask import current_app
from isa_sdk import Isa


def get_isa() -> Isa:
    return current_app.isa  # type: ignore[attr-defined]

Prequalify route

A prequalification request has three parts:

  • applicant — demographics (age, sex, height, weight, state).
  • coverage — face amount in cents plus the state.
  • products — which carrier products to evaluate (empty = all qualifying products).

An empty products selection quotes every product the applicant qualifies for, which is the right default for a first integration. Narrow to specific product IDs once you have a shortlist.

Each offer includes a pricing[] table with one row per rate class. The row flagged primary is the best-qualifying class to surface.

# app/routes.py
import uuid

from flask import Blueprint, jsonify, request

from isa_sdk import Coverage, Sex
from isa_sdk.zyins import (
    Applicant,
    IsaApiError,
    NicotineUsage,
    PrequalifyInput,
    ValidationError,
)

from .isa_client import get_isa

bp = Blueprint("isa", __name__)

ALL_PRODUCTS = ""  # empty selection means "quote every product the applicant qualifies for"


@bp.post("/prequalify")
def prequalify():
    isa = get_isa()
    body = request.get_json(force=True)

    input_data = PrequalifyInput(
        applicant=Applicant(
            dob=body["dob"],
            sex=Sex.MALE,
            height_inches=body["height_inches"],
            weight_pounds=body["weight_pounds"],
            state=body["state"],
            nicotine_use=NicotineUsage.NONE,
        ),
        coverage=Coverage.face_value(25_000),
        products=ALL_PRODUCTS,
    )

    try:
        result = isa.zyins.prequalify(
            input_data,
            idempotency_key=str(uuid.uuid4()),
        )
    except ValidationError as err:
        return jsonify({"error": str(err), "param": err.param}), 400
    except IsaApiError as err:
        return jsonify({"error": str(err), "code": err.code}), 502

    plans = []
    for offer in result.data.plans:
        primary = next(row for row in offer.pricing if row.primary)
        plans.append({
            "carrier":         offer.carrier.name,
            "product":         offer.product.name,
            "category":        primary.eligibility.category,
            "premium_display": primary.premium.amount.display,
            "premium_cents":   primary.premium.amount.cents,
        })

    return jsonify({"plans": plans, "request_id": result.request_id})

A request to this route carries only the demographics:

curl -s http://localhost:5000/prequalify \
  -H "Content-Type: application/json" \
  -d '{"dob":"1962-04-18","height_inches":70,"weight_pounds":195,"state":"NC"}'

The raw POST /v3/prequalify body and response shape are documented in the Run a prequalification reference.

Webhook blueprint

ZyINS signs each webhook with a single X-ZyINS-Signature: t=<unix-seconds>,v1=<hex> header. Parse both fields, recompute the HMAC over <t>.<raw_body>, verify in constant time, and reject any request more than five minutes old. The event id lives in the JSON body (id), not a header — deduplicate on it since delivery is at-least-once. The signing secret comes from the ISA_WEBHOOK_SECRET environment variable.

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

from flask import Blueprint, Response, request

wh = Blueprint("webhooks", __name__)

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

_seen_event_ids: set[str] = set()  # swap for a durable store in production


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
    payload = f"{ts}.".encode() + raw_body
    expected = hmac.new(
        WEBHOOK_SECRET.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, sig)


@wh.post("/isa")
def receive():
    raw = request.get_data()
    sig = request.headers.get("X-ZyINS-Signature", "")
    if not _verify(raw, sig):
        return Response("invalid signature", status=400)

    event = json.loads(raw)
    event_id = event.get("id", "")
    if event_id in _seen_event_ids:
        return Response(status=200)  # already processed; acknowledge and skip
    _seen_event_ids.add(event_id)

    _dispatch(event)
    return Response(status=200)


def _dispatch(event: dict) -> None:
    pass  # switch on event["type"]

The shared Webhooks guide documents the full signing protocol, header set, and retry schedule.

Production notes

  • Run with a production WSGI server (gunicorn) and multiple workers. The Isa
    client is process-local and thread-safe — each worker has its own instance.
  • Pass base_url= to Isa.with_bearer(base_url="https://zyins.staging.isaapi.com")
    to target a staging environment.
  • Replace the in-memory _seen_event_ids set with a shared store (Redis, your
    database) so deduplication holds across workers and restarts.

See also