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.
go get github.com/status403com/status302-goIt needs Go 1.26 or newer. The REST client uses only the standard library; the stream client adds coder/websocket and vmihailenco/msgpack.
Client
Section titled “Client”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. |
Requests
Section titled “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.
Pagination
Section titled “Pagination”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)}Conditional requests
Section titled “Conditional requests”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): // unchangedcase 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.
Errors
Section titled “Errors”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.APIErrorif 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.
Rate limits and retries
Section titled “Rate limits and retries”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.
Stream
Section titled “Stream”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 aSubscription;Ready(ctx)gives the head seq.Messages(ctx)(orNext(ctx)) delivers*DataMessage(Topic,Seq,TS,Data, withDecode),*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.StreamAuthTicketandGetTicketto connect with tickets instead. - Close codes
4401and4403are final and end the stream with a*StreamClosedError; other closes reconnect.
Binary encoding (MessagePack)
Section titled “Binary encoding (MessagePack)”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.
Webhooks
Section titled “Webhooks”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)}© 2026 status403 Sp. z o.o.Aleja Solidarności 68/121, 00-240 Warsaw, PolandKRS 0001269943NIP 5253108931Share capital PLN 5,000.00
Registry court: District Court for the Capital City of Warsaw, 12th Commercial Division

