Gin
Integrate the ISA SDK into a Gin application: one client on a dependency struct, a prequalify handler, and a signed webhook receiver.
Gin
This guide wires the ISA SDK into a Gin application. One *sdk.Isa client lives on a dependency struct and is injected into handlers, so there is no global state and no init(). The client is constructed once at startup; its namespaces share transport resources and are safe for concurrent use across Gin's request goroutines.
The shortest working call is the prequalify handler below: it reads a bearer token from the environment, runs the v3 prequalify engine for the John Doe persona (NC, DOB 1962-04-18, 5'10", 195 lb, no medications), and returns the qualified plans as JSON.
Prerequisites
- Go 1.22 or newer.
go get github.com/isa-sdk/sdkandgo get github.com/gin-gonic/gin.- A bearer token in
ISA_TOKEN. Use a test token (isa_test_…) in development and a live token (isa_live_…) in production; both return the same response shape, and the token alone selects test or live mode. Tokens are emailed to you after checkout — contact support to rotate one.
Dependency struct
// internal/handler/handler.go
package handler
import (
"github.com/gin-gonic/gin"
"github.com/isa-sdk/sdk"
)
// Handler holds all application-wide dependencies.
type Handler struct {
isa *sdk.Isa
}
func New(isa *sdk.Isa) *Handler {
return &Handler{isa: isa}
}
func (h *Handler) RegisterRoutes(r *gin.Engine) {
r.POST("/prequalify", h.Prequalify)
r.POST("/webhooks/isa", h.Webhook)
}Main setup
Construct the client once and hand it to the handler. Authentication is a single bearer token read from ISA_TOKEN; pinning the prequalify surface to v3 opts this client into the uniform v3 offer shape returned below.
// cmd/server/main.go
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/isa-sdk/sdk"
"your-module/internal/handler"
)
func main() {
// ISA_TOKEN is read from the environment when Token is empty.
isa, err := sdk.WithBearerOptions(sdk.BearerOptions{
APIVersion: map[string]string{"prequalify": "v3"},
})
if err != nil {
log.Fatalf("isa: %v", err)
}
r := gin.Default()
h := handler.New(isa)
h.RegisterRoutes(r)
if err := r.Run(":8080"); err != nil {
log.Fatalf("server: %v", err)
}
}Prequalify handler
The handler binds the request body, runs prequalify for the applicant, and maps each offer to a compact JSON row. Each offer's headline premium and rate class come from the primary pricing row; zyins.OfferPremium returns that row's premium directly so you do not walk Pricing by hand.
Narrow the product roster by product id (prod_<uuid>), not by carrier slug. The id comes from Get the v3 datasets on the products row, and it selects the whole product family. The example pins one final-expense product; pass several ids to compare carriers in one call.
// internal/handler/prequalify.go
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/isa-sdk/sdk"
"github.com/isa-sdk/sdk/catalog"
"github.com/isa-sdk/sdk/zyins"
)
// newbridgeFinalExpenseID is one product id from GET /v3/datasets?include=products.
// Fetch ids at startup rather than hard-coding them in production.
const newbridgeFinalExpenseID = "prod_007e74bf-671c-41cc-be27-28cfd75fd5d2"
type prequalifyRequest struct {
DOB string `json:"dob" binding:"required"`
State string `json:"state" binding:"required"`
HeightInches int `json:"height_inches" binding:"required"`
WeightPounds int `json:"weight_pounds" binding:"required"`
FaceAmount int `json:"face_amount" binding:"required"`
}
func (h *Handler) Prequalify(c *gin.Context) {
var req prequalifyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
height, _ := zyins.NewHeightInches(req.HeightInches)
weight, _ := zyins.NewWeight(req.WeightPounds)
coverage, _ := zyins.NewFaceValueCoverage(req.FaceAmount)
product, ok := catalog.ByID(newbridgeFinalExpenseID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "unknown product id"})
return
}
products, _ := zyins.NewProductSelectionOf(product)
input := zyins.PrequalifyRequest{
Applicant: zyins.Applicant{
DOB: req.DOB,
Sex: zyins.SexMale,
Height: height,
Weight: weight,
State: catalog.State(req.State),
NicotineUse: zyins.NicotineUsageInput{LastUsed: zyins.NicotineNever},
},
Coverage: coverage,
Products: products,
}
result, err := h.isa.Zyins.Prequalify.Run(c.Request.Context(), &input)
if err != nil {
var apiErr *zyins.Error
if errors.As(err, &apiErr) {
// 4xx errors are the caller's to fix; surface the code and request id.
c.JSON(http.StatusBadGateway, gin.H{
"code": apiErr.Code,
"request_id": apiErr.RequestID,
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
type planOut struct {
Carrier string `json:"carrier"`
Product string `json:"product"`
Category string `json:"category"`
PremiumDisplay string `json:"premium_display"`
PremiumCents int64 `json:"premium_cents"`
}
plans := make([]planOut, 0, len(result.Plans))
for _, offer := range result.Plans {
prem := zyins.OfferPremium(offer)
if prem == nil {
continue // no qualifying, priceable rate class for this applicant
}
var category string
for _, row := range offer.Pricing {
if row.Primary && row.Eligibility.Category != nil {
category = string(*row.Eligibility.Category)
}
}
plans = append(plans, planOut{
Carrier: offer.Carrier.Name,
Product: offer.Product.Name,
Category: category,
PremiumDisplay: prem.Amount.Display,
PremiumCents: prem.Amount.Cents,
})
}
c.JSON(http.StatusOK, gin.H{
"plans": plans,
"request_id": result.RequestID,
})
}Webhook handler
ISA signs every webhook with an X-ZyINS-Signature header in the form t=<unix-seconds>,v1=<hex-hmac-sha256>. The signing string is <timestamp>.<raw-body>, so you must read the raw bytes before any JSON decode and compare them with a constant-time function.
Reject deliveries with timestamps outside the five-minute tolerance window. De-duplicate on the event's id because ISA delivers webhooks at least once. See the Webhooks guide for the retry schedule and full signature format.
// internal/handler/webhook.go
package handler
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
const (
signatureHeader = "X-ZyINS-Signature"
webhookToleranceSeconds = 300
)
func (h *Handler) Webhook(c *gin.Context) {
raw, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
sig := c.GetHeader(signatureHeader)
if err := verifyWebhookSignature(raw, sig, os.Getenv("ISA_WEBHOOK_SECRET")); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
var event struct {
ID string `json:"id"`
Type string `json:"type"`
}
if err := json.Unmarshal(raw, &event); err != nil {
c.Status(http.StatusBadRequest)
return
}
// De-dupe on event.ID, then dispatch on event.Type.
c.Status(http.StatusOK)
}
func verifyWebhookSignature(body []byte, header, secret string) error {
timestamp, macHex, err := parseSignatureHeader(header)
if err != nil {
return err
}
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return fmt.Errorf("parse webhook timestamp %q: %w", timestamp, err)
}
if delta := time.Now().Unix() - ts; delta > webhookToleranceSeconds || delta < -webhookToleranceSeconds {
return fmt.Errorf("webhook timestamp %d is outside the %ds tolerance", ts, webhookToleranceSeconds)
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%s.%s", timestamp, body)
expected := hex.EncodeToString(mac.Sum(nil))
if subtle.ConstantTimeCompare([]byte(expected), []byte(macHex)) != 1 {
return fmt.Errorf("webhook signature mismatch")
}
return nil
}
// parseSignatureHeader splits "t=<ts>,v1=<hex>" into its timestamp and hex parts.
func parseSignatureHeader(header string) (timestamp, macHex string, err error) {
for _, part := range strings.Split(header, ",") {
key, value, ok := strings.Cut(strings.TrimSpace(part), "=")
if !ok {
continue
}
switch key {
case "t":
timestamp = value
case "v1":
macHex = value
}
}
if timestamp == "" || macHex == "" {
return "", "", fmt.Errorf("malformed %s header: %q", signatureHeader, header)
}
return timestamp, macHex, nil
}See also
- Go quickstart — install, authenticate, and run your first quote.
- Authentication — bearer tokens, test versus live mode.
- Webhooks — signature format, timestamp tolerance, and the retry schedule.
- Error catalog — every
code, its HTTP status, and how to remediate it.
Updated about 9 hours ago