Using the ISA SDK with Next.js

Server-only bearer credentials, a Server Action for prequalify, and client-side reference data for the Next.js 14+ App Router.

Using the ISA SDK with Next.js

A Next.js 14+ App Router integration of the ISA SDK.

Your API token stays server-side, prequalify runs in a Server Action, and reference data streams to the client through a typed boundary. The pattern works on the Node.js runtime (see "Edge runtime" below if you need Edge elsewhere).

Install

npm install isa-sdk

The SDK targets Node 20+. Next.js 14+ on the Node runtime is fully
supported; see "Edge runtime" below before adopting Edge.

Initialize the client

The SDK is server-only — your token must never reach the browser bundle.

Authenticate with the bearer token that arrived by email after checkout. Use isa_test_… while you build, and isa_live_… in production. Hold one Isa instance per process inside a server-only module.

// src/lib/isa.server.ts
import 'server-only';
import { Isa } from 'isa-sdk';

let cached: Promise<Isa> | undefined;

export function getIsa(): Promise<Isa> {
  if (!cached) {
    cached = Isa.withBearer({ token: process.env.ISA_TOKEN! });
  }
  return cached;
}

Isa.withBearer returns a Promise<Isa>, so the cache holds the promise itself. This lets concurrent requests share one initialization.

server-only is the official Next.js guard that fails the build if a client component imports this file. Keep the token in ISA_TOKEN, never in a NEXT_PUBLIC_* variable (which Next.js inlines into the browser bundle).

The cache lives at module scope — one instance per server runtime (per Vercel function instance).

Load reference data

Datasets are large and rarely change. Fetch them in a Server Component and stream only the slice the client actually needs. Do not ship the raw SDK client to the browser.

// src/app/quote/page.tsx
import { getIsa } from '@/lib/isa.server';
import { States } from 'isa-sdk';
import { QuoteForm } from './QuoteForm';

export const dynamic = 'force-dynamic';

export default async function QuotePage() {
  const isa = await getIsa();
  await isa.zyins.datasets.get();   // warm the SDK cache
  // States ship as a typed catalog constant, not on the dataset bundle.
  const states = States.entries().map(([code, meta]) => ({
    code,
    name: meta.name,
  }));
  return <QuoteForm states={states} />;
}

The Server Component runs on every request unless you cache with
unstable_cache or revalidate. A 24-hour revalidate is sufficient
for datasets in most apps:

export const revalidate = 86400;

Run a quote

Wrap prequalify in a Server Action. The client posts the applicant form, the server runs the call, and the response streams back.

The canonical happy path:

  • John Doe, DOB 1962-04-18, North Carolina, 5'10", 195 lb
  • No conditions or medications
  • Quoted for $25,000 face amount across every product in the catalog

To narrow to specific products, pass ProductSelection.of([...]) with their prod_<uuid> ids from datasets.get().

// src/app/quote/actions.ts
'use server';

import {
  Sex,
  State,
  Height,
  Weight,
  NicotineDuration,
  Coverage,
  Products,
  ProductSelection,
  offerPremium,
} from 'isa-sdk';
import { getIsa } from '@/lib/isa.server';

export interface PlanRow {
  carrier: string;
  product: string;
  category: 'immediate' | 'graded' | 'rop' | 'other';
  premiumDisplay: string;
  premiumCents: number;
  productId: string;
}

export async function runQuoteAction(): Promise<{
  plans: PlanRow[];
  requestId: string;
}> {
  const isa = await getIsa();
  const { data, requestId } = await isa.zyins.prequalify({
    applicant: {
      dob: '1962-04-18',
      sex: Sex.Male,
      height: Height.fromFeetInches(5, 10),
      weight: Weight.fromPounds(195),
      state: State.NorthCarolina,
      nicotineUse: { lastUsed: NicotineDuration.Never },
    },
    coverage: Coverage.faceValue(25_000),
    products: ProductSelection.of([Products.Fex.AetnaAccendo]),
  });
  return {
    plans: data.plans.map((offer) => {
      const primary = offer.pricing.find((row) => row.primary);
      const premium = offerPremium(offer);
      return {
        carrier:        offer.carrier.name,
        product:        offer.product.name,
        category:       primary?.eligibility.category ?? 'other',
        premiumDisplay: premium?.amount.display ?? '—',
        premiumCents:   premium?.amount.cents ?? 0,
        productId:      offer.product.id,
      };
    }),
    requestId,
  };
}

Each offer carries a pricing[] table — one row per rate class. The row with primary: true is the headline tier.

Use offerPremium(offer) to get that row's premium directly. Check primary.eligibility.category to see whether the applicant qualifies at the immediate, graded, or return-of-premium tier.

Money is always integer cents plus a server-formatted display string — never parse the display.

// src/app/quote/QuoteForm.tsx
'use client';

import { useState, useTransition } from 'react';
import { runQuoteAction, type PlanRow } from './actions';

export function QuoteForm({ states }: { states: { code: string; name: string }[] }) {
  const [plans, setPlans] = useState<PlanRow[]>([]);
  const [pending, startTransition] = useTransition();

  function onSubmit() {
    // Await the Server Action first, then mark the state update as a
    // transition — React 18's startTransition callback must be synchronous.
    runQuoteAction().then(({ plans: next, requestId }) => {
      console.log('request', requestId);
      startTransition(() => setPlans(next));
    });
  }

  return (
    <>
      <button onClick={onSubmit} disabled={pending}>
        {pending ? 'Running…' : 'Run quote'}
      </button>
      <ul>
        {plans.map((p) => (
          <li key={p.productId + p.category}>
            {p.carrier} — {p.product} — {p.category} — {p.premiumDisplay}
          </li>
        ))}
      </ul>
    </>
  );
}

The Server Action carries the response envelope's requestId back as a
plain string. Log it on the server too — that req_… value is the support
ticket's anchor.

Handle errors

Server Actions throw to the client as plain errors. Catch SDK exceptions
inside the action, translate to a typed result, and let the client render
the appropriate UI.

// src/app/quote/actions.ts (refined)
import {
  IsaApiError,
  IsaIdempotencyConflictError,
  type PrequalifyRequest,
} from 'isa-sdk';
import { getIsa } from '@/lib/isa.server';

// The request builder from the prior section.
declare function buildRequest(): PrequalifyRequest;

interface PlanRow {
  carrier: string;
  product: string;
  premiumDisplay: string;
}

export type QuoteResult =
  | { ok: true; plans: PlanRow[]; requestId: string }
  | { ok: false; message: string; requestId?: string };

export async function runQuoteAction(): Promise<QuoteResult> {
  try {
    const isa = await getIsa();
    const { data, requestId } = await isa.zyins.prequalify(buildRequest());
    const plans = data.plans.map((offer) => ({
      carrier: offer.carrier.name,
      product: offer.product.name,
      premiumDisplay: offer.pricing.find((row) => row.primary)?.premium?.amount.display ?? '—',
    }));
    return { ok: true, plans, requestId };
  } catch (err) {
    if (err instanceof IsaIdempotencyConflictError) {
      return { ok: false, message: 'Duplicate request with a different body.' };
    }
    if (err instanceof IsaApiError) {
      console.error('isa api', err.code, err.requestId);
      return { ok: false, message: err.message, requestId: err.requestId };
    }
    throw err; // unexpected — let Next.js render the error boundary
  }
}

Match on err.code (a stable string enum), never on the human-readable
err.message. The Errors reference lists every code
value and its remediation.

Edge runtime

The SDK signs requests and reads process.env through Node APIs, so run your Server Actions and Route Handlers that call ISA on the Node.js runtime, not Edge.

The default App Router runtime is Node.js — only routes that opt in with export const runtime = 'edge' are affected. Keep getIsa() callers on the default runtime and stream the resulting data to Edge-rendered pages if you need Edge elsewhere.

Production checklist

  • ISA_TOKEN set in your hosting platform's server env (Vercel project
    env, etc.), not in a NEXT_PUBLIC_* variable.
  • Use the isa_test_… token while you build; swap to isa_live_… for
    production. The response's livemode field tells you which one ran.
  • The SDK imported only from files marked 'server-only'. The build fails
    fast if a client component imports it.
  • ISA calls run on the Node.js runtime, not Edge.
  • Idempotency keys are UUID v4 (the SDK mints one per call by default).
  • result.requestId logged on every error (req_01HZK2N5GQR9T8X4B6FJW3Y1AS).
  • Datasets revalidated daily, not on every request.

What's next