Skip to content

Go SDK

github.com/status403com/status302-go is the official Go client (API documentation on pkg.go.dev). It has one typed method per public operation, with structs and enums generated from the same OpenAPI spec as the API reference; releases share the major and minor version of the spec they were generated from.

Terminal window
go get github.com/status403com/status302-go

It needs Go 1.26 or newer. The REST client uses only the standard library; the stream client adds coder/websocket and vmihailenco/msgpack.

import "github.com/status403com/status302-go"
client := status302.NewClient(os.Getenv("STATUS302_API_KEY"))

A Client is safe for concurrent use; create one and share it.

Option Default
WithBaseURL https://api.status302.com Another environment’s API origin.
WithMaxRetries 3 Retries after 429, 5xx (idempotent methods) and connection errors.
WithMaxRetryAfter 60 * time.Second The longest Retry-After the client waits out before returning the 429.
WithTimeout 30 * time.Second Timeout per attempt; the call’s context.Context bounds the whole call.
WithHTTPClient http.DefaultClient Your own *http.Client, for proxies or instrumentation.
WithETagCache An ETagCache such as NewMemoryETagCache(1000): revalidates GETs and returns the stored body on 304. See conditional requests.

Methods are named after the operation ids: ListEvents, GetEventMetrics, ListSalesFeed, CreateSavedList, and so on. Path parameters are positional, query parameters go in a <Operation>Params struct (nil for none) and request bodies are pointers to the generated structs. Optional fields are pointers, set with new(value):

page, err := client.ListEvents(ctx, &status302.ListEventsParams{Country: new("US"), Limit: new(50)})
if err != nil {
return err
}
metrics, err := client.GetEventMetrics(ctx, page.Items[0].ID)
if err != nil {
return err
}
for _, m := range metrics.Items {
fmt.Println(m.Scope, *m.Floor, *m.Currency) // money is integer minor units
}

Optional properties that may also be null are Nullable[T]: status302.Value(v), status302.Null[T](), or unset (omitted from the JSON), so a PATCH can tell “clear” from “leave as is”. CSV and binary downloads return []byte.

Per-call options: WithHeader(key, value), WithIfNoneMatch(etag) and WithResponseInto(&resp), which fills a Response with the status, headers, ETag and the parsed RateLimit.

Every paginated operation has two iterators: …All yields every item, …Pages whole pages. They follow nextCursor as cursor (or nextBefore as before for ListAlertRules and ListAlertEvents) and stop after the first error they yield.

for event, err := range client.ListEventsAll(ctx, &status302.ListEventsParams{Country: new("US"), Limit: new(200)}) {
if err != nil {
return err
}
fmt.Println(event.ID, event.Name)
}

The operations that accept If-None-Match say so in their doc comment. With WithIfNoneMatch(etag), while the data still has that ETag the API answers 304 and the call returns a *NotModifiedError (with ETag, Header and RateLimit) matching ErrNotModified:

var first status302.Response
_, err := client.GetEventMetrics(ctx, 4102, status302.WithResponseInto(&first))
// …
next, err := client.GetEventMetrics(ctx, 4102, status302.WithIfNoneMatch(first.ETag))
switch {
case errors.Is(err, status302.ErrNotModified):
// unchanged
case err == nil:
fmt.Println("changed", next.Items)
}

With WithETagCache the client does this for every GET: it stores bodies by URL with their ETag, revalidates them and returns the stored body on 304 (StatusCode 304 in the Response). MemoryETagCache is concurrency-safe and keeps the most recently used entries; implement ETagCache (Get/Set of a CachedResponse) to use your own store. See conditional requests.

Non-2xx responses return an *APIError carrying the problem as Problem, with StatusCode, Header, RequestID and RateLimit. Match the status with errors.Is:

Sentinel Status
ErrBadRequest 400, 422
ErrAuthentication 401
ErrPlanRequired 402, with RequiredPlan(), CurrentPlan(), Feature() (and Limit() for count limits)
ErrPermission 403, with RequiredScope() for a missing scope
ErrNotFound 404
ErrConflict 409
ErrRateLimited 429, with RetryAfter, RateLimit and Limit()
ErrServer 5xx
_, err := client.ListMovers(ctx, &status302.ListMoversParams{Window: new(status302.ListMoversWindow24h)})
var apiErr *status302.APIError
if errors.Is(err, status302.ErrPlanRequired) && errors.As(err, &apiErr) {
fmt.Println("needs plan", apiErr.RequiredPlan())
}

The message ends with (request <id>) when the response names its request; quote it to support. Network failures return a *ConnectionError wrapping the cause.

429 responses are retried for every method after Retry-After; 5xx responses and connection errors are retried with exponential backoff for GET, PUT and DELETE only. Each response’s RateLimit headers are parsed into Response.RateLimit (and APIError.RateLimit). See rate limits.

client.Stream(options) returns a stream client. It connects on the first Subscribe, reconnects with backoff, resubscribes every topic with since set to the last seq it delivered, drops duplicate seqs, and delivers a ResyncMessage when a gap couldn’t be replayed; FetchSnapshot loads the REST snapshot for it.

stream := client.Stream(status302.StreamOptions{})
defer stream.Close()
sales := stream.Subscribe("sales", status302.SubscribeOptions{Filter: map[string]any{"country": []string{"US"}}})
if _, err := sales.Ready(ctx); err != nil {
return err
}
for msg, err := range stream.Messages(ctx) {
if err != nil {
return err
}
switch m := msg.(type) {
case *status302.DataMessage:
fmt.Println(m.Topic, m.Seq, string(m.Data))
case *status302.ResyncMessage:
snapshot, err := stream.FetchSnapshot(ctx, m)
// …
}
}
  • stream.Subscribe(topic, SubscribeOptions{Filter, Since}) returns a Subscription; Ready(ctx) gives the head seq.
  • Messages(ctx) (or Next(ctx)) delivers *DataMessage (Topic, Seq, TS, Data, with Decode), *ResyncMessage (Topic, Reason, Snapshot) and *ErrorMessage (Code, Message) when the server refuses a subscription.
  • The client sends your key on the upgrade. Pass Auth: status302.StreamAuthTicket and GetTicket to connect with tickets instead.
  • Close codes 4401 and 4403 are final and end the stream with a *StreamClosedError; other closes reconnect.

Pass Encoding: status302.StreamEncodingMsgpack to receive every server message as one binary MessagePack frame instead of JSON text (the client adds encoding=msgpack to the URL). Messages and Data payloads decode into the same values as with JSON; the client’s own messages stay JSON. The default is JSON.

VerifyWebhook(body, signatureHeader, secrets...) checks a delivery’s signature in constant time and its timestamp against a 5-minute tolerance (VerifyWebhookWith and VerifyOptions.Tolerance to change it), and returns the parsed envelope. Pass several secrets while rotating. Every failure matches ErrWebhookVerification; answer it with a 4xx. SignWebhook(body, secret, time.Now()) produces a valid header for tests.

func handler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
event, err := status302.VerifyWebhook(body, r.Header.Get("Status302-Signature"), os.Getenv("STATUS302_WEBHOOK_SECRET"))
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
fmt.Println(event.Type, event.ID)
w.WriteHeader(http.StatusNoContent)
}