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/sdkgo get github.com/labstack/echo/v4ISA_TOKENin 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
Updated about 9 hours ago