Echo

Integrate the ISA SDK into an Echo application: dependency injection, middleware, and webhook verification.

Echo

This guide wires the ISA SDK into an Echo v4 application. The *sdk.Isa client is injected through a dependency struct — one instance per process, safe for concurrent use.

Prerequisites

  • Go 1.22+
  • go get github.com/isa-sdk/sdk
  • go get github.com/labstack/echo/v4
  • ISA_TOKEN in your environment

Dependency struct and routes

// internal/handler/handler.go
package handler

import (
    "github.com/isa-sdk/sdk"
    "github.com/labstack/echo/v4"
)

type Handler struct {
    isa *sdk.Isa
}

func New(isa *sdk.Isa) *Handler {
    return &Handler{isa: isa}
}

func (h *Handler) RegisterRoutes(e *echo.Echo) {
    e.POST("/prequalify", h.Prequalify)
    e.POST("/webhooks/isa", h.Webhook)
}

Main setup

// cmd/server/main.go
package main

import (
    "log"
    "os"

    "github.com/isa-sdk/sdk"
    "github.com/isa-sdk/sdk/zyins"
    "github.com/labstack/echo/v4"
    "your-module/internal/handler"
)

func main() {
    isa, err := sdk.WithBearer(os.Getenv("ISA_TOKEN"))
    if err != nil {
        log.Fatalf("isa: %v", err)
    }

    e := echo.New()
    h := handler.New(isa)
    h.RegisterRoutes(e)

    if err := e.Start(":8080"); err != nil {
        log.Fatalf("server: %v", err)
    }
}

Prequalify handler

// internal/handler/prequalify.go
package handler

import (
    "errors"
    "net/http"

    "github.com/isa-sdk/sdk/catalog"
    "github.com/isa-sdk/sdk/zyins"
    "github.com/labstack/echo/v4"
)

type prequalifyRequest struct {
    DOB          string `json:"dob"`
    State        string `json:"state"`
    HeightInches int    `json:"height_inches"`
    WeightPounds int    `json:"weight_pounds"`
}

func (h *Handler) Prequalify(c echo.Context) error {
    var req prequalifyRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
    }

    height, err := zyins.NewHeightInches(req.HeightInches)
    if err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
    }
    weight, err := zyins.NewWeight(req.WeightPounds)
    if err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
    }
    coverage, err := zyins.NewFaceValueCoverage(25_000)
    if err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
    }
    products, err := zyins.NewProductSelectionOf(catalog.Products.Fex.AetnaAccendo())
    if err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
    }

    input := &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,
    }

    result, err := h.isa.Zyins.Prequalify.Run(c.Request().Context(), input)
    if err != nil {
        var apiErr *zyins.Error
        if errors.As(err, &apiErr) {
            return c.JSON(http.StatusBadGateway, map[string]string{
                "code":       string(apiErr.Code),
                "request_id": apiErr.RequestID,
            })
        }
        return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
    }

    type planOut struct {
        Carrier        string `json:"carrier"`
        Product        string `json:"product"`
        PremiumDisplay string `json:"premium_display"` // verbatim carrier string
        PremiumCents   int64  `json:"premium_cents"`   // integer cents
    }
    // The headline premium for one product is its primary pricing row, which
    // zyins.OfferPremium returns; offers with no qualifying row are skipped.
    var plans []planOut
    for _, offer := range result.Plans {
        premium := zyins.OfferPremium(offer)
        if premium == nil {
            continue
        }
        plans = append(plans, planOut{
            Carrier:        offer.Carrier.Name,
            Product:        offer.Product.DisplayName,
            PremiumDisplay: premium.Amount.Display,
            PremiumCents:   premium.Amount.Cents,
        })
    }
    return c.JSON(http.StatusOK, map[string]any{
        "plans":      plans,
        "request_id": result.RequestID,
    })
}

Webhook handler

// 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/labstack/echo/v4"
)

const webhookToleranceSeconds = 300

func (h *Handler) Webhook(c echo.Context) error {
    raw, err := io.ReadAll(c.Request().Body)
    if err != nil {
        return c.NoContent(http.StatusBadRequest)
    }

    sig := c.Request().Header.Get("X-ZyINS-Signature")
    if err := verifyWebhookSignature(raw, sig, os.Getenv("ISA_WEBHOOK_SECRET")); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid signature"})
    }

    var event map[string]any
    if err := json.Unmarshal(raw, &event); err != nil {
        return c.NoContent(http.StatusBadRequest)
    }

    // Dispatch on event["type"]
    return c.NoContent(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