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/http method-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 form isa_test_… (test mode) or isa_live_… (live mode); to rotate one, contact support. Authentication is bearer-token only — the SDK attaches Authorization: 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