Context, Timeouts, and Cancellation
Every network call on the Go SDK client takes a context.Context as its first argument, so you control per-call deadlines and cancellation with the standard library instead of a custom options struct.
Every Client method in github.com/primitivedotdev/sdks/sdk-go that performs a network request takes ctx context.Context as its first argument. There is no separate RequestOptions struct like the Node and Python SDKs use: per-call deadlines, cancellation, and request-scoped values all go through the standard library's context package.
You need this whenever a call might hang longer than you want to wait, or when an upstream cancellation (a client disconnect, a shutdown signal) should abort an in-flight Send, Reply, or Forward.
Per-call timeout#
Wrap the call in context.WithTimeout and always defer cancel().
package main
import (
"context"
"log"
"time"
primitive "github.com/primitivedotdev/sdks/sdk-go"
)
func main() {
client, err := primitive.NewClient("prim_test")
if err != nil {
log.Fatal(err)
}
// Cancel after 15 seconds.
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_, err = client.Send(ctx, primitive.SendParams{
From: "Support <support@example.com>",
To: "alice@example.com",
Subject: "Hello",
BodyText: "Hi there",
})
if err != nil {
log.Printf("send failed: %v", err)
}
}
For wait mode (Wait: true on Send or Reply), give the context a deadline long enough for the downstream SMTP transaction to settle, typically 30-60 seconds. WaitTimeoutMs (default 30000 ms, and validated to be between 1000 and 30000 ms when set) governs how long Primitive itself waits for a delivery outcome; your context deadline must be at least that long or you'll cancel the request before Primitive replies.
Per-call cancellation#
Use context.WithCancel and call cancel() from wherever the abort signal arrives, for example a user closing a connection or your own shutdown handler.
// Requires: context, primitive "github.com/primitivedotdev/sdks/sdk-go"
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-userBailoutSignal
cancel()
}()
_, err := client.Send(ctx, primitive.SendParams{
From: "Support <support@example.com>",
To: "alice@example.com",
Subject: "Hello",
BodyText: "Hi there",
})
Distinguishing client-side aborts from API errors#
A canceled or timed-out context surfaces as a standard library sentinel error, while a server response surfaces as *primitive.APIError, so errors.Is and errors.As tell the two apart:
| Cause | Error |
|---|---|
| Context deadline passed | context.DeadlineExceeded |
cancel() called | context.Canceled |
| Non-2xx or malformed API response | *primitive.APIError |
- 1
Call the method with a context that can time out or be canceled#
// Requires: context, time, primitive "github.com/primitivedotdev/sdks/sdk-go" ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := client.Send(ctx, primitive.SendParams{ From: "Support <support@example.com>", To: "alice@example.com", Subject: "Hello", BodyText: "Hi there", }) - 2
Branch on the error with errors.Is and errors.As#
// Requires: errors, log, context, primitive "github.com/primitivedotdev/sdks/sdk-go" switch { case errors.Is(err, context.DeadlineExceeded): log.Println("client-side timeout: the request did not complete in time") case errors.Is(err, context.Canceled): log.Println("client-side cancellation: caller aborted the request") default: var apiErr *primitive.APIError if errors.As(err, &apiErr) { log.Printf("API error: status=%d code=%s message=%s", apiErr.StatusCode, apiErr.Code, apiErr.Message) } else if err != nil { log.Printf("unexpected error: %v", err) } } - 3
Verify the outcome#
A
context.DeadlineExceededorcontext.Cancelederror means the request may or may not have reached the server, treat the send as indeterminate and retry with anIdempotencyKeyrather than assuming it failed. A*primitive.APIErrormeans the server responded, so itsStatusCodeandCodefields tell you exactly what went wrong.
The one exception: PayEmailChallenge#
X402Client.PayEmailChallenge signs a payment locally with no I/O, so it takes no context.Context argument. Every other x402 and email client method, including Charge, Pay, Send, Reply, and Forward, follows the standard ctx pattern above.
Idempotency keys (IdempotencyKey on SendParams or ForwardParams) are what make it safe to retry after a canceled or timed-out context. Retrying the same logical send with the same key replays the original response instead of sending duplicate mail, reach for it before you build your own retry loop around context deadlines.
Next steps#
Control Wait/WaitTimeoutMs delivery semantics and dedupe retries with IdempotencyKey.
Client and ConfigurationUnderstand how NewClient and NewClientWithOptions construct the dual-host Go client.
Error HandlingInspect APIError and the webhook/x402 error types to handle failures precisely.
x402 ErrorsInterpret X402Error status codes and indeterminate-outcome cases, including a Status: 0 Pay() failure.
Was this page helpful?