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
carriername - A
productname - A
pricingarray 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_TOKENandISA_WEBHOOK_SECRETper environment. Never commit them. Useisa_test_…in staging andisa_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 asidempotencyKeyon the response envelope. No per-call setup needed. - Request ID: Echo
requestId(formatreq_01HZK2N5GQR9T8X4B6FJW3Y1AS) in the response headerX-Isa-Request-Idon every response. - Webhook routing: Mount webhook routes before
express.json(), usingexpress.raw({ type: 'application/json' }). Parsing JSON first breaks HMAC verification. - Logging: Use a structured logger (
pino,winston) that includesrequestIdon every log line for tracing.
What's next
- Authentication — bearer token rotation,
test vs. live - Idempotency — how the SDK mints keys, when to
bring your own - Webhooks — full signature format, retry schedule,
dedupe strategy - TypeScript Quickstart — the underlying
SDK shapes
Updated about 9 hours ago