Go: net/http
Integrate the ISA SDK with the Go standard library net/http: ServeMux, handler functions, and webhook verification.
Go: net/http
This guide wires the ISA SDK directly into Go's net/http standard library — no third-party router required. One *sdk.Isa client is built at startup and injected through a dependency struct, so handlers stay testable and the client is reused across every request.
Prerequisites
- Go 1.22+ (this guide uses the
net/httpmethod-and-path routing patterns introduced in 1.22). go get github.com/isa-sdk/[email protected]- Your bearer token in
ISA_TOKEN. Tokens arrive by email after checkout in the formisa_test_…(test mode) orisa_live_…(live mode); to rotate one, contact support. Authentication is bearer-token only — the SDK attachesAuthorization: Bearer <token>to every request.
Confirm your token first
Before wiring the SDK, confirm the token reaches the API. This curl prequalifies John Doe — male, born 1962-04-18, in NC, 70 in, 195 lbs, no nicotine — for $25,000 of coverage on one final-expense product, and returns the v3 envelope (object, request_id, livemode, data.plans). Swap prod_007e74bf-671c-41cc-be27-28cfd75fd5d2 for any product ID from GET /v3/datasets?include=products.
curl https://zyins.isaapi.com/v3/prequalify \
-H "Authorization: Bearer $ISA_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"applicant": {
"sex": "male",
"dob": "1962-04-18",
"height_inches": 70,
"weight_lbs": 195,
"nicotine": { "last_used": "never" }
},
"coverage": { "face_amount_cents": 2500000, "state": "NC" },
"products": ["prod_007e74bf-671c-41cc-be27-28cfd75fd5d2"]
}'
Dependency struct
// internal/handler/handler.go
package handler
import (
"net/http"
"github.com/isa-sdk/sdk"
)
type Handler struct {
isa *sdk.Isa
mux *http.ServeMux
}
func New(isa *sdk.Isa) *Handler {
h := &Handler{
isa: isa,
mux: http.NewServeMux(),
}
h.mux.HandleFunc("POST /prequalify", h.Prequalify)
h.mux.HandleFunc("POST /webhooks/isa", h.Webhook)
return h
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(w, r)
}
Main setup
The SDK bundles a per-surface version map that already defaults prequalify to v3, so the unversioned Prequalify.Run below returns the v3 flat-plans response with no extra configuration. Passing APIVersion: map[string]string{"prequalify": "v3"} simply makes that pin explicit and auditable — record the contract version in code rather than inheriting it from the SDK release.
// cmd/server/main.go
package main
import (
"log"
"net/http"
"os"
"github.com/isa-sdk/sdk"
"your-module/internal/handler"
)
func main() {
isa, err := sdk.WithBearerOptions(sdk.BearerOptions{
Token: os.Getenv("ISA_TOKEN"), // isa_test_… or isa_live_…
APIVersion: map[string]string{"prequalify": "v3"},
})
if err != nil {
log.Fatalf("isa: %v", err)
}
h := handler.New(isa)
log.Fatal(http.ListenAndServe(":8080", h))
}
Prequalify handler
Your browser posts the applicant's facts to this handler. It converts them to a zyins.PrequalifyRequest, runs the API call, and transforms each offer into a result the frontend needs.
The v3 response is a flat Plans slice. The headline premium comes from the primary pricing row; use zyins.OfferPremium to fetch it.
The Products field tells the API which carrier products to quote. Use product IDs (prod_<uuid>) from GET /v3/datasets?include=products. The example below quotes one final-expense product.
// internal/handler/prequalify.go
package handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/isa-sdk/sdk/catalog"
"github.com/isa-sdk/sdk/zyins"
)
type prequalifyRequest struct {
DOB string `json:"dob"`
State string `json:"state"`
HeightInches int `json:"height_inches"`
WeightPounds int `json:"weight_lbs"`
FaceAmountCents int `json:"face_amount_cents"`
ProductIDs []string `json:"product_ids"`
}
// resolveProducts maps the wire product IDs (prod_<uuid>) posted by the
// browser to the typed catalog.Product values NewProductSelectionOf takes.
// An unknown ID is a client error, surfaced rather than silently dropped.
func resolveProducts(ids []string) ([]catalog.Product, error) {
products := make([]catalog.Product, 0, len(ids))
for _, id := range ids {
product, ok := catalog.ByID(id)
if !ok {
return nil, fmt.Errorf("unknown product id %q", id)
}
products = append(products, product)
}
return products, nil
}
func (h *Handler) Prequalify(w http.ResponseWriter, r *http.Request) {
var req prequalifyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
height, err := zyins.NewHeightInches(req.HeightInches)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
weight, err := zyins.NewWeight(req.WeightPounds)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
coverage, err := zyins.NewFaceValueCoverage(req.FaceAmountCents / 100)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
selected, err := resolveProducts(req.ProductIDs)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
products, err := zyins.NewProductSelectionOf(selected...)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
result, err := h.isa.Zyins.Prequalify.Run(r.Context(), &zyins.PrequalifyRequest{
Applicant: zyins.Applicant{
DOB: req.DOB,
Sex: zyins.SexMale,
Height: height,
Weight: weight,
State: zyins.State(req.State),
NicotineUse: zyins.NicotineUsageInput{LastUsed: zyins.NicotineNever},
},
Coverage: coverage,
Products: products,
})
if err != nil {
var apiErr *zyins.Error
if errors.As(err, &apiErr) {
writeJSON(w, http.StatusBadGateway, map[string]string{
"code": string(apiErr.Code),
"request_id": apiErr.RequestID,
})
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"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 {
premium := zyins.OfferPremium(offer)
if premium == nil {
continue // every pricing row was ineligible
}
out := planOut{
Carrier: offer.Carrier.Name,
Product: offer.Product.Name,
PremiumDisplay: premium.Amount.Display,
PremiumCents: premium.Amount.Cents,
}
for _, row := range offer.Pricing {
if row.Primary && row.Eligibility.Category != nil {
out.Category = string(*row.Eligibility.Category)
}
}
plans = append(plans, out)
}
writeJSON(w, http.StatusOK, map[string]any{
"plans": plans,
"request_id": result.RequestID,
})
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
Quoting every product
The v3 contract reserves an empty products array to mean "quote every product the token can see." That is the call a brand-new integrator reaches for first:
products, _ := zyins.NewProductSelectionOf() // intended: quote all products
Until that lands, pass explicit product IDs from GET /v3/datasets?include=products as shown above.
Webhook handler
Webhooks arrive with a single X-ZyINS-Signature header in the form t=<unix-seconds>,v1=<hex>. Parse both fields from that one header, recompute the HMAC-SHA256 over <t>.<raw body> keyed by your signing secret, compare in constant time, and reject any timestamp outside a five-minute window so a captured request cannot be replayed. See the Webhooks guide for the full signing protocol, retry schedule, and event.id de-duplication contract.
// 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"
)
const webhookToleranceSeconds = 300
func (h *Handler) Webhook(w http.ResponseWriter, r *http.Request) {
raw, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
sig := r.Header.Get("X-ZyINS-Signature")
if err := verifyWebhookSignature(raw, sig, os.Getenv("ISA_WEBHOOK_SECRET")); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid signature"})
return
}
var event map[string]any
if err := json.Unmarshal(raw, &event); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// Dispatch on event["type"]
w.WriteHeader(http.StatusOK)
}
// verifyWebhookSignature parses the "t=<unix>,v1=<hex>" X-ZyINS-Signature
// header, recomputes HMAC-SHA256 over "<t>.<body>", and compares in
// constant time. There is no separate timestamp header.
func verifyWebhookSignature(body []byte, signatureHeader, secret string) error {
var tsField, v1Field string
for _, part := range strings.Split(signatureHeader, ",") {
key, value, found := strings.Cut(part, "=")
if !found {
continue
}
switch key {
case "t":
tsField = value
case "v1":
v1Field = value
}
}
tsInt, err := strconv.ParseInt(tsField, 10, 64)
if err != nil {
return fmt.Errorf("missing t field: %w", err)
}
if diff := time.Now().Unix() - tsInt; diff > webhookToleranceSeconds || diff < -webhookToleranceSeconds {
return fmt.Errorf("timestamp out of tolerance")
}
payload := fmt.Sprintf("%s.%s", tsField, body)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(payload))
expected := hex.EncodeToString(mac.Sum(nil))
if subtle.ConstantTimeCompare([]byte(expected), []byte(v1Field)) != 1 {
return fmt.Errorf("signature mismatch")
}
return nil
}
See also
Updated about 9 hours ago