---
title: "Client and Configuration"
canonical: "https://test.abhinandan.one/go-client-configuration"
markdown_url: "https://test.abhinandan.one/go-client-configuration.md"
publisher: "Primitive SDKs"
kind: "concept"
content_type: "reference"
category: "Go SDK"
description: "NewClient, NewClientWithOptions, and NewClientFromAPI build the Go SDK's dual-host client that routes Send/Reply to the attachment-capable host automatically."
keywords: ["NewClient", "NewClientWithOptions", "NewClientFromAPI", "ClientOptions", "dual-host client Go SDK", "DefaultAPIBaseURL1"]
last_modified: "2026-08-11T18:54:51.410187+00:00"
published_at: "2026-08-11T18:54:51.259675+00:00"
source_files:
  - "sdk-go/client.go"
sections:
  - {anchor: "the-dual-host-split", title: "The dual-host split"}
  - {anchor: "constructing-a-client", title: "Constructing a client"}
  - {anchor: "overriding-base-urls", title: "Overriding base URLs"}
  - {anchor: "constructing-from-a-custom-generated-client", title: "Constructing from a custom generated client"}
  - {anchor: "which-constructor-to-use", title: "Which constructor to use"}
  - {anchor: "what-sendapi-is", title: "What `sendAPI` is"}
  - {anchor: "every-network-call-takes-a-contextcontext", title: "Every network call takes a `context.Context`"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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:

```go
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:

```go
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.

```go
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:

```go
client, err := primitive.NewClientWithOptions(apiKey, primitive.ClientOptions{
	APIBaseURL1: "https://staging.primitive.dev/api/v1",
	APIBaseURL2: "https://staging-api.primitive.dev/v1",
})
```

```go
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.

> **Note:** 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:

```go
func NewClientFromAPI(apiClient sendAPI) *Client
```

```go
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.

```go
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](https://test.abhinandan.one/go-context-timeouts.md) for the full pattern, including how `wait: true` sends need a longer deadline.
