Docs › SDKs
Go SDK
github.com/handset-hq/handset-go — generated from the
same OpenAPI spec as the API and the other SDKs. One typed
…WithResponse method per endpoint over net/http, no
runtime dependencies beyond the generated client. Go 1.24+.
Install & configure
go get github.com/handset-hq/handset-go
One constructor, authenticated with your API key. A test key
(hs_test_…) hits the simulated carrier, a live key
(hs_live_…) the real network — the prefix picks the mode, the base
URL is the same.
import handset "github.com/handset-hq/handset-go" client, err := handset.New(os.Getenv("HANDSET_API_KEY")) if err != nil { log.Fatal(err) }
Send a message
Every operation has a …WithResponse method. Optional fields are
pointers — handset.Ptr wraps a value inline. Header and query
params ride in a per-operation …Params struct; pass
nil for none.
resp, err := client.SendMessageWithResponse(ctx, nil, handset.SendMessageJSONRequestBody{ From: "num_01kzv…", To: "+14155550134", Body: handset.Ptr("Your technician is 15 minutes out."), }) if err != nil { return err // transport failure (DNS, connection, timeout) } fmt.Println(resp.JSON202.Id, resp.JSON202.Status) // msg_…, "queued"
Errors & status codes
Methods don't return an error for a non-2xx — err is only a
transport failure. Each documented status is a typed field on the response
(JSON202, JSON200, …); when the one you expect is
nil, read JSONDefault.Error:
resp, err := client.SendMessageWithResponse(ctx, nil, body) if err != nil { return err } if resp.JSON202 == nil { e := resp.JSONDefault.Error // e.Code e.g. "recipient_opted_out"; resp.StatusCode() for the HTTP status return fmt.Errorf("%s: %s", e.Code, e.Message) }
Codes worth branching on: recipient_opted_out (they sent STOP —
do not retry), campaign_not_approved (10DLC in review),
rate_limited, tenant_not_found.
Idempotent sends
Attach an Idempotency-Key through the params struct — a retry
with the same key within 24h returns the original result instead of sending
twice:
key := handset.IdempotencyKey("order-4417-confirm")
client.SendMessageWithResponse(ctx,
&handset.SendMessageParams{IdempotencyKey: &key}, body)The core loop
// 1. Your customer, as a tenant tenant, _ := client.CreateTenantWithResponse(ctx, handset.CreateTenantJSONRequestBody{ Name: "Bayview Dental", Timezone: handset.Ptr("America/Los_Angeles"), }) // 2. A number in their area code, live on arrival found, _ := client.SearchAvailableNumbersWithResponse(ctx, &handset.SearchAvailableNumbersParams{AreaCode: handset.Ptr("415")}) number, _ := client.PurchaseNumberWithResponse(ctx, handset.PurchaseNumberJSONRequestBody{ TenantId: tenant.JSON201.Id, PhoneNumber: found.JSON200.Data[0].PhoneNumber, }) // 3. Send — threading, opt-outs, 10DLC all happen server-side client.SendMessageWithResponse(ctx, nil, handset.SendMessageJSONRequestBody{ From: number.JSON201.Id, To: "+14155550123", Body: handset.Ptr("Welcome aboard!"), })
Pagination
var after *string for { page, err := client.ListMessagesWithResponse(ctx, &handset.ListMessagesParams{ Limit: handset.Ptr(100), After: after, }) if err != nil { return err } for _, m := range page.JSON200.Data { handle(m) } if !page.JSON200.HasMore { break } after = page.JSON200.NextCursor }
Types & webhooks
Every request and response shape is an exported type, so the package doubles
as your model library — handset.Message,
handset.Tenant, handset.Voicemail. Inbound webhook
deliveries are the handset.EventEnvelope type (each event name also
has a named alias, handset.MessageReceivedJSONRequestBody and the
rest), so a verified payload unmarshals into a typed value.
User-Agent: handset-go/<version> on every request, so Go
traffic is visible in your API logs. Prefer TypeScript or Python? Same spec,
same objects: TypeScript ·
Python.