Client and Configuration
Explains how the Go SDK's Client constructors build and configure the internal dual-host setup, and when to reach for NewClient versus NewClientWithOptions or NewClientFromAPI.
The Go SDK's Client talks to two API hosts internally and routes each operation to the correct one, so you never have to think about which host serves which endpoint.
The dual-host split#
Primitive splits its HTTP API across two hosts. Host 1 (DefaultAPIBaseURL1, https://www.primitive.dev/api/v1) serves every operation except attachment-capable message sends. Host 2 (DefaultAPIBaseURL2, https://api.primitive.dev/v1) is a Cloudflare Worker with a larger request body cap, and serves only POST /send-mail and POST /emails/{id}/reply.
Client holds two generated sendAPI clients internally:
type Client struct {
api sendAPI // host 1: everything except attachment-capable sends
apiSend sendAPI // host 2: /send-mail and /emails/{id}/reply
}
Client.Send, Client.Reply, and Client.SemanticSearch call through apiSend, because /send-mail, /emails/{id}/reply, and /v1/semantic-search are all served by the host-2 Worker. Everything else calls through api. The Python and Node SDKs make the same split internally. You never select a host yourself, the method you call decides it for you.
Constructing a client#
Use NewClient for the default, production-pointed client:
package main
import (
"context"
"log"
"os"
primitive "github.com/primitivedotdev/sdks/sdk-go"
)
func main() {
client, err := primitive.NewClient(os.Getenv("PRIMITIVE_API_KEY"))
if err != nil {
log.Fatal(err)
}
_, err = client.Send(context.Background(), primitive.SendParams{
From: "Support <support@example.com>",
To: "alice@example.com",
Subject: "Hello",
BodyText: "Hi there",
})
if err != nil {
log.Fatal(err)
}
}
NewClient turns your API key into a static bearer token source, builds both the host-1 and host-2 generated clients from it, and wires them into a Client. Any variadic primitiveapi.ClientOption you pass (TLS config, a custom http.Client, telemetry middleware) applies to both underlying clients.
func NewClient(apiKey string, opts ...primitiveapi.ClientOption) (*Client, error)
Overriding base URLs#
NewClientWithOptions is the explicit form that lets you override either host's base URL:
client, err := primitive.NewClientWithOptions(apiKey, primitive.ClientOptions{
APIBaseURL1: "https://staging.primitive.dev/api/v1",
APIBaseURL2: "https://staging-api.primitive.dev/v1",
})
type ClientOptions struct {
// APIBaseURL1 overrides the primary API host. Empty = production default.
APIBaseURL1 string
// APIBaseURL2 overrides the attachments-supporting send host. Empty = production default.
APIBaseURL2 string
// Extra options forwarded to both underlying ogen clients (TLS, HTTP
// client, telemetry middleware, etc.).
Extra []primitiveapi.ClientOption
}
An empty string on either field falls back to the production default (primitiveapi.DefaultAPIBaseURL1 / DefaultAPIBaseURL2). NewClient(apiKey, opts...) is exactly NewClientWithOptions(apiKey, ClientOptions{Extra: opts}), a thin convenience wrapper.
These overrides exist for internal staging and local testing. They are not part of the documented, supported SDK surface for application code, leave both fields empty in production.
Constructing from a custom generated client#
NewClientFromAPI wraps a caller-supplied generated client directly, bypassing NewClient's HTTP construction entirely:
func NewClientFromAPI(apiClient sendAPI) *Client
client := primitive.NewClientFromAPI(myMockClient)
The same apiClient instance is used for both api and apiSend. This is a test seam, reach for it when you want full control over the underlying HTTP layer (e.g. a mock server that serves both host-1 and host-2 shapes), not for normal application code, which should use NewClient or NewClientWithOptions.
Which constructor to use#
| Constructor | Use it when |
|---|---|
NewClient(apiKey, opts...) | Default. Production traffic, no base-URL overrides. |
NewClientWithOptions(apiKey, options) | You need to point at a staging or local host. |
NewClientFromAPI(apiClient) | Tests: you already have a sendAPI-shaped client (real or mock) and want to skip HTTP construction. |
What sendAPI is#
sendAPI is the unexported interface both internal clients satisfy: the three generated operations Client's methods call through.
type sendAPI interface {
SendEmail(ctx context.Context, request *primitiveapi.SendMailInput, params primitiveapi.SendEmailParams) (primitiveapi.SendEmailRes, error)
ReplyToEmail(ctx context.Context, request *primitiveapi.ReplyInput, params primitiveapi.ReplyToEmailParams) (primitiveapi.ReplyToEmailRes, error)
SemanticSearch(ctx context.Context, request *primitiveapi.SemanticSearchInput) (primitiveapi.SemanticSearchRes, error)
}
You don't implement this yourself in normal use, the generated sdk-go/api package's client satisfies it, and NewClientFromAPI accepts any type that does (real or a test double).
Every network call takes a context.Context#
Send, Reply, Forward, and SemanticSearch all take ctx context.Context as their first argument, there's no separate RequestOptions struct in the Go SDK. Use the context for per-call timeouts and cancellation; see Context, Timeouts, and Cancellation for the full pattern, including how wait: true sends need a longer deadline.
Next steps#
Use Client.Send to deliver outbound mail, with Wait/WaitTimeoutMs and IdempotencyKey.
Context, Timeouts, and CancellationApply per-call deadlines and cancellation to every network call.
Replying to EmailsUse Client.Reply to respond to inbound mail with server-derived threading.
Go SDK QuickstartInstall the SDK and send your first reply end to end.
Was this page helpful?