NestJS

Integrate the ISA SDK into a NestJS application: module, injectable service, webhook controller.

NestJS

This guide wires the ISA SDK into a NestJS application using a module, an injectable IsaService, and a webhook controller. The SDK shares one Isa instance across your application lifetime via NestJS's singleton scope.

Prerequisites

  • Node.js 20+, NestJS 10+
  • npm install isa-sdk
  • An ISA secret key in ISA_TOKEN (a server-side isa_test_… key while you build, isa_live_… in production). Your keys arrive by email after checkout — to rotate one, contact support.
  • A webhook signing secret in ISA_WEBHOOK_SECRET if you mount the webhook controller.

The SDK authenticates with a bearer token; ISA_TOKEN is the only credential the application needs.

IsaModule

// isa/isa.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { IsaService } from './isa.service';
import { IsaController } from './isa.controller';
import { WebhookController } from './webhook.controller';

@Module({
  imports: [ConfigModule],
  providers: [IsaService],
  controllers: [IsaController, WebhookController],
  exports: [IsaService],
})
export class IsaModule {}

IsaService

onModuleInit builds one Isa instance from your bearer token and pins the prequalify surface to v3. Every call through this service returns the v3 plan shape. Since the instance carries no shared mutable state, the singleton is safe to call concurrently across requests.

// isa/isa.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Isa } from 'isa-sdk';

@Injectable()
export class IsaService implements OnModuleInit {
  private isa!: Isa;

  constructor(private readonly config: ConfigService) {}

  async onModuleInit(): Promise<void> {
    this.isa = await Isa.withBearer(
      { token: this.config.getOrThrow<string>('ISA_TOKEN') },
      undefined,
      { apiVersion: { prequalify: 'v3' } },
    );
  }

  get client(): Isa {
    return this.isa;
  }
}

Prequalify controller

The controller builds an applicant from the request body, runs prequalify, and maps each offer to a small client-facing row.

Each offer carries a pricing[] table with one row per rate class. Use offerPremium to get the best-qualifying premium — this is the single figure a list UI shows per product.

Construct Height and Weight through their factories to avoid raw unitless numbers, and use NicotineDuration for nicotine history. Use ProductSelection.of([...]) to narrow to specific products, or ProductSelection.byTypes([ProductClass.FinalExpense]) to select an entire product family.

// isa/isa.controller.ts
import {
  Body,
  Controller,
  HttpCode,
  HttpException,
  HttpStatus,
  Post,
} from '@nestjs/common';
import { IsaService } from './isa.service';
import {
  Coverage,
  Height,
  IsaApiError,
  IsaValidationError,
  NicotineDuration,
  Products,
  ProductSelection,
  Sex,
  Weight,
  offerPremium,
  type Applicant,
  type V3Offer,
} from 'isa-sdk';

interface PrequalifyBody {
  dob: string;
  state: string;
  heightFeet: number;
  heightInches: number;
  weightPounds: number;
}

@Controller()
export class IsaController {
  constructor(private readonly isaService: IsaService) {}

  @Post('prequalify')
  @HttpCode(HttpStatus.OK)
  async prequalify(@Body() body: PrequalifyBody) {
    const applicant: Applicant = {
      dob: body.dob,
      sex: Sex.Male,
      height: Height.fromFeetInches(body.heightFeet, body.heightInches),
      weight: Weight.fromPounds(body.weightPounds),
      state: body.state,
      nicotineUse: { lastUsed: NicotineDuration.Never },
    };

    try {
      const result = await this.isaService.client.zyins.prequalify({
        applicant,
        coverage: Coverage.faceValue(25_000),
        products: ProductSelection.of([Products.Fex.AetnaAccendo]),
      });
      const plans = result.data.plans as readonly V3Offer[];
      return {
        plans: plans.map((p) => {
          const premium = offerPremium(p);                 // best-qualifying row, or null
          return {
            carrier:        p.carrier.name,
            product:        p.product.name,
            eligible:       p.eligible,                     // qualifies for any rate class
            premiumDisplay: premium?.amount.display ?? null, // server-formatted string
            premiumCents:   premium?.amount.cents ?? null,   // integer cents
          };
        }),
        requestId: result._requestId,
      };
    } catch (err) {
      if (err instanceof IsaValidationError) {
        throw new HttpException(
          { error: err.message, param: err.param },
          HttpStatus.BAD_REQUEST,
        );
      }
      if (err instanceof IsaApiError) {
        throw new HttpException(
          { code: err.code, requestId: err.requestId },
          HttpStatus.BAD_GATEWAY,
        );
      }
      throw err;
    }
  }
}

Webhook controller

ISA signs every webhook with HMAC-SHA256 over <timestamp>.<raw body> and sends it in the X-ZyINS-Signature: t=<timestamp>,v1=<hex> header.

The controller recomputes the signature using your ISA_WEBHOOK_SECRET, compares it in constant time, and rejects any request outside the five-minute timestamp window. Always verify against the raw bytes — parsing the body first changes them and breaks the signature.

See the webhooks guide for the event catalog and retry schedule.

// isa/webhook.controller.ts
import {
  BadRequestException,
  Controller,
  Headers,
  HttpCode,
  HttpStatus,
  Post,
  RawBodyRequest,
  Req,
} from '@nestjs/common';
import { createHmac, timingSafeEqual } from 'node:crypto';
import type { Request } from 'express';

const TOLERANCE_SECONDS = 300;

function verifySignature(rawBody: Buffer, header: string, secret: string): boolean {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=', 2) as [string, string]),
  );
  const { t: ts, v1: sig } = parts;
  if (!ts || !sig) return false;

  const tsInt = parseInt(ts, 10);
  if (Math.abs(Date.now() / 1_000 - tsInt) > TOLERANCE_SECONDS) return false;

  const payload = `${ts}.${rawBody.toString('utf8')}`;
  const expected = createHmac('sha256', secret).update(payload).digest('hex');

  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(sig, 'utf8');
  return a.length === b.length && timingSafeEqual(a, b);
}

@Controller('webhooks')
export class WebhookController {
  @Post('isa')
  @HttpCode(HttpStatus.OK)
  receiveWebhook(
    @Req() req: RawBodyRequest<Request>,
    @Headers('x-zyins-signature') sigHeader: string,
  ): void {
    const secret = process.env['ISA_WEBHOOK_SECRET'] ?? '';
    if (!verifySignature(req.rawBody!, sigHeader, secret)) {
      throw new BadRequestException('invalid signature');
    }

    const event = JSON.parse(req.rawBody!.toString('utf8')) as { type: string };
    // Dispatch on event.type
  }
}

Enable raw body parsing in main.ts:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap(): Promise<void> {
  const app = await NestFactory.create(AppModule, { rawBody: true });
  await app.listen(3000);
}
bootstrap();

See also