Using the ISA SDK with Express

Bootstrap the ISA SDK at server start, share it across Express handlers, and verify webhooks.

Using the ISA SDK with Express

This guide shows you how to integrate the ISA SDK with Express and Node 20.

You'll create a single authenticated client at server startup, use it across all request handlers, run a prequalify, and verify webhook signatures.

Prerequisites: If you haven't run a quote yet, read the TypeScript Quickstart first — it covers the request and response shapes this page assumes.

Install

npm install isa-sdk express

The SDK targets Node 20+. The examples use ESM ("type": "module");
CommonJS works identically with require().

Initialize the client

Create the Isa instance once during server startup, before app.listen(). Use Isa.withBearer() with your bearer token — your isa_live_… (production) or isa_test_… (sandbox) secret, delivered by email after checkout.

By default, withBearer() reads the token from ISA_TOKEN env var. To pull from a secrets manager instead, pass { token } explicitly.

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

const isa = await Isa.withBearer({ token: process.env.ISA_TOKEN! });

const app = express();
app.use(express.json());

// Attach the client to res.locals so handlers don't import it directly.
// Easier to swap in tests; one place changes when the surface changes.
app.use((_req, res, next) => {
  res.locals.isa = isa;
  next();
});

Keep the client at module scope. The SDK has no shared mutable state across calls, so concurrent requests on one instance are safe.

Load reference data

Fetch datasets once at boot and hold them in memory. The picker UIs
your front-end talks to never need a per-request roundtrip.

// src/server.ts (continued)
import express from 'express';
import { Isa, States } from 'isa-sdk';

const app = express();
const isa = await Isa.withBearer();

const datasets = await isa.zyins.datasets.get();
app.use((_req, res, next) => {
  res.locals.datasets = datasets;
  next();
});

// The dataset bundle has no state list — the state catalog ships as the
// `States` constant. Expose whatever slice your pickers need.
app.get('/api/states', (_req, res) => {
  res.json(States);
});

If your service runs for weeks, refresh the data once a day. A cron job that calls isa.zyins.datasets.get() and swaps the cached reference is enough — no restart needed.

Run a quote

This example quotes John Doe — born 1962-04-18, lives in NC, 5'10", 195 lb, no conditions or medications — for Aetna Accendo final expense with a $25,000 face value.

The v3 response returns a flat plans array. Each plan has:

  • A carrier name
  • A product name
  • A pricing array with one row per rate class

One pricing row has primary: true — that's the headline rate. Use offerPremium() to extract its premium. Eligibility (immediate / graded / rop) lives on the pricing row, not on the plan itself.

// src/routes/quote.ts
import { Router } from 'express';
import {
  Isa,
  Sex,
  State,
  Height,
  Weight,
  NicotineDuration,
  Coverage,
  Products,
  ProductSelection,
  offerPremium,
} from 'isa-sdk';

const isa = await Isa.withBearer();

export const quoteRouter = Router();

quoteRouter.post('/quote', async (_req, res, next) => {
  try {
    // prequalify mints a UUID v4 idempotency key per call and echoes it
    // on the envelope; the offers live under `.data.plans`.
    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]),
    });

    res.set('X-Isa-Request-Id', requestId);
    res.json({
      plans: data.plans.map((offer) => {
        const primary = offer.pricing.find((row) => row.primary);
        const premium = offerPremium(offer); // primary row's premium, or null
        return {
          carrier:  offer.carrier.name,
          product:  offer.product.name,
          category: primary?.eligibility.category, // 'immediate' | 'graded' | 'rop'
          premium:  premium?.amount.display,        // verbatim carrier string
          cents:    premium?.amount.cents,          // integer cents
        };
      }),
      requestId,
    });
  } catch (err) {
    next(err);
  }
});

Send result.requestId back to the client in a response header (like X-Isa-Request-Id). This lets integrators correlate errors to support tickets without searching logs.

To quote an entire product family instead of a single carrier, use ProductSelection.byTypes([ProductClass.FinalExpense]). To narrow further, pass specific product IDs from isa.zyins.datasets.get().

Webhooks

Verification needs the raw request body (not parsed JSON) so the HMAC matches the bytes the server signed. No SDK ships a ZyINS webhook verifier yet, so recompute the HMAC by hand.

Important: Mount the webhook route BEFORE express.json() middleware, and use express.raw({ type: 'application/json' }) on that route only. If you parse the body first, HMAC verification fails.

The signature arrives in one header, X-ZyINS-Signature: t=<unix-seconds>,v1=<hex>. Parse both fields, recompute HMAC-SHA256(secret, "<t>.<raw body>"), and compare to v1 with crypto.timingSafeEqual — never write your own comparator, as it's vulnerable to timing attacks.

// src/routes/webhooks.ts
import crypto from 'node:crypto';
import { Router, raw } from 'express';

const TOLERANCE_SECONDS = 5 * 60;

export const webhookRouter = Router();

webhookRouter.post(
  '/webhooks/rapidsign',
  raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.header('X-ZyINS-Signature') ?? '';
    const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
    const ts = parseInt(parts.t ?? '', 10);
    const receivedHex = parts.v1 ?? '';
    if (!ts || !receivedHex || Math.abs(Date.now() / 1000 - ts) > TOLERANCE_SECONDS) {
      res.status(400).end();
      return;
    }

    const signed = `${ts}.${(req.body as Buffer).toString('utf8')}`;
    const expected = crypto.createHmac('sha256', process.env.ISA_WEBHOOK_SECRET!).update(signed).digest();
    const received = Buffer.from(receivedHex, 'hex');
    if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
      res.status(400).end();
      return;
    }

    const event = JSON.parse((req.body as Buffer).toString('utf8'));
    console.log('webhook', event.type, event.id);
    // Process the event. Persist by event.id for at-least-once dedupe.
    res.status(204).end();
  },
);

See the Webhooks guide for the full signature format and retry schedule.

Handle errors

Centralize all SDK error translation in an Express error-handler middleware. This keeps route handlers clean and gives you one place to log and respond consistently.

// src/middleware/isaErrors.ts
import type { ErrorRequestHandler } from 'express';
import {
  IsaApiError,
  IsaConfigError,
  IsaIdempotencyConflictError,
} from 'isa-sdk';

export const isaErrorHandler: ErrorRequestHandler = (err, _req, res, next) => {
  if (err instanceof IsaIdempotencyConflictError) {
    return res.status(409).json({
      error: { code: 'idempotency_conflict', key: err.key },
    });
  }
  if (err instanceof IsaApiError) {
    return res.status(err.status ?? 502).json({
      error: { code: err.code, requestId: err.requestId, message: err.message },
    });
  }
  if (err instanceof IsaConfigError) {
    return res.status(500).json({ error: { code: 'sdk_misconfigured' } });
  }
  return next(err);
};

// Mount last:
// app.use(isaErrorHandler);

Always log err.requestId — that's the support ticket's anchor.

Production checklist

  • Secrets: Set ISA_TOKEN and ISA_WEBHOOK_SECRET per environment. Never commit them. Use isa_test_… in staging and isa_live_… in production — the token alone decides test vs. live mode.
  • Idempotency keys: The SDK auto-mints UUID v4 keys (e.g. 550e8400-e29b-41d4-a716-446655440000) and echoes them as idempotencyKey on the response envelope. No per-call setup needed.
  • Request ID: Echo requestId (format req_01HZK2N5GQR9T8X4B6FJW3Y1AS) in the response header X-Isa-Request-Id on every response.
  • Webhook routing: Mount webhook routes before express.json(), using express.raw({ type: 'application/json' }). Parsing JSON first breaks HMAC verification.
  • Logging: Use a structured logger (pino, winston) that includes requestId on every log line for tracing.

What's next