{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/go-client-configuration","markdown_url":"https://test.abhinandan.one/go-client-configuration.md","article":{"id":"45d00bde-e3be-4891-83e3-fc80031a5477","article_slug":"go-client-configuration","parent_article_slug":null,"parent_article_title":null,"kind":"concept","published_at":"2026-08-11T18:54:51.259675+00:00","keywords":["NewClient","NewClientWithOptions","NewClientFromAPI","ClientOptions","dual-host client Go SDK","DefaultAPIBaseURL1"],"meta_description":"NewClient, NewClientWithOptions, and NewClientFromAPI build the Go SDK's dual-host client that routes Send/Reply to the attachment-capable host automatically.","og_image_url":null,"source_file_paths":["sdk-go/client.go"],"recording_id":null,"replayable":false,"task_name":"Client and Configuration","category":"Go SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"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.\n\n## The dual-host split\n\nPrimitive 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`.\n\n`Client` holds two generated `sendAPI` clients internally:\n\n```go\ntype Client struct {\n\tapi     sendAPI // host 1: everything except attachment-capable sends\n\tapiSend sendAPI // host 2: /send-mail and /emails/{id}/reply\n}\n```\n\n`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.\n\n## Constructing a client\n\nUse `NewClient` for the default, production-pointed client:\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\nfunc main() {\n\tclient, err := primitive.NewClient(os.Getenv(\"PRIMITIVE_API_KEY\"))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t_, err = client.Send(context.Background(), primitive.SendParams{\n\t\tFrom:     \"Support <support@example.com>\",\n\t\tTo:       \"alice@example.com\",\n\t\tSubject:  \"Hello\",\n\t\tBodyText: \"Hi there\",\n\t})\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n```\n\n`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.\n\n```go\nfunc NewClient(apiKey string, opts ...primitiveapi.ClientOption) (*Client, error)\n```\n\n## Overriding base URLs\n\n`NewClientWithOptions` is the explicit form that lets you override either host's base URL:\n\n```go\nclient, err := primitive.NewClientWithOptions(apiKey, primitive.ClientOptions{\n\tAPIBaseURL1: \"https://staging.primitive.dev/api/v1\",\n\tAPIBaseURL2: \"https://staging-api.primitive.dev/v1\",\n})\n```\n\n```go\ntype ClientOptions struct {\n\t// APIBaseURL1 overrides the primary API host. Empty = production default.\n\tAPIBaseURL1 string\n\t// APIBaseURL2 overrides the attachments-supporting send host. Empty = production default.\n\tAPIBaseURL2 string\n\t// Extra options forwarded to both underlying ogen clients (TLS, HTTP\n\t// client, telemetry middleware, etc.).\n\tExtra []primitiveapi.ClientOption\n}\n```\n\nAn 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.\n\n<Note>\n\nThese 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.\n\n</Note>\n\n## Constructing from a custom generated client\n\n`NewClientFromAPI` wraps a caller-supplied generated client directly, bypassing `NewClient`'s HTTP construction entirely:\n\n```go\nfunc NewClientFromAPI(apiClient sendAPI) *Client\n```\n\n```go\nclient := primitive.NewClientFromAPI(myMockClient)\n```\n\nThe 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`.\n\n## Which constructor to use\n\n| Constructor | Use it when |\n| --- | --- |\n| `NewClient(apiKey, opts...)` | Default. Production traffic, no base-URL overrides. |\n| `NewClientWithOptions(apiKey, options)` | You need to point at a staging or local host. |\n| `NewClientFromAPI(apiClient)` | Tests: you already have a `sendAPI`-shaped client (real or mock) and want to skip HTTP construction. |\n\n## What `sendAPI` is\n\n`sendAPI` is the unexported interface both internal clients satisfy: the three generated operations `Client`'s methods call through.\n\n```go\ntype sendAPI interface {\n\tSendEmail(ctx context.Context, request *primitiveapi.SendMailInput, params primitiveapi.SendEmailParams) (primitiveapi.SendEmailRes, error)\n\tReplyToEmail(ctx context.Context, request *primitiveapi.ReplyInput, params primitiveapi.ReplyToEmailParams) (primitiveapi.ReplyToEmailRes, error)\n\tSemanticSearch(ctx context.Context, request *primitiveapi.SemanticSearchInput) (primitiveapi.SemanticSearchRes, error)\n}\n```\n\nYou 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).\n\n## Every network call takes a `context.Context`\n\n`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](go-context-timeouts) for the full pattern, including how `wait: true` sends need a longer deadline.\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Sending Emails\" href=\"go-sending-emails\">\n\nUse Client.Send to deliver outbound mail, with Wait/WaitTimeoutMs and IdempotencyKey.\n\n</Card>\n\n<Card title=\"Context, Timeouts, and Cancellation\" href=\"go-context-timeouts\">\n\nApply per-call deadlines and cancellation to every network call.\n\n</Card>\n\n<Card title=\"Replying to Emails\" href=\"go-replying-to-emails\">\n\nUse Client.Reply to respond to inbound mail with server-derived threading.\n\n</Card>\n\n<Card title=\"Go SDK Quickstart\" href=\"go-sdk-quickstart\">\n\nInstall the SDK and send your first reply end to end.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-go/client.go","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Client+and+Configuration&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fgo-client-configuration","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}