---
title: "Sending Emails (Go SDK)"
canonical: "https://test.abhinandan.one/go-sending-emails"
markdown_url: "https://test.abhinandan.one/go-sending-emails.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Go SDK"
description: "Client.Send in the Go SDK dispatches outbound mail synchronously by default, with an optional Wait flag for SMTP delivery confirmation."
keywords: ["Client.Send", "SendParams", "Go SDK send email", "Wait WaitTimeoutMs", "IdempotencyKey", "delivery status Go"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:54:51.56226+00:00"
source_files:
  - "sdk-go/client.go"
  - "sdk-go/README.md"
sections:
  - {anchor: "send-a-basic-email", title: "Send a basic email"}
  - {anchor: "step-construct-the-client", title: "Construct the client"}
  - {anchor: "step-call-clientsend-with-a-context-deadline", title: "Call Client.Send with a context deadline"}
  - {anchor: "step-read-the-result", title: "Read the result"}
  - {anchor: "wait-for-delivery-confirmation", title: "Wait for delivery confirmation"}
  - {anchor: "dedupe-retries-with-idempotencykey", title: "Dedupe retries with IdempotencyKey"}
  - {anchor: "threading-a-new-send", title: "Threading a new send"}
  - {anchor: "errors", title: "Errors"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Sending Emails (Go SDK)

Send outbound emails with Client.Send, choosing between the default fast-return behavior and wait mode for delivery confirmation, with idempotent retries via IdempotencyKey.

`Client.Send` sends a new outbound email through the Primitive API. Use it whenever your Go service needs to originate mail rather than reply to an inbound message (for replies, see [Replying to Emails](https://test.abhinandan.one/go-replying-to-emails.md)).

## Send a basic email

### 1. Construct the client

Build a [`Client`](https://test.abhinandan.one/go-client-configuration.md) with your API key:

```go
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)
	}
	_ = client
}
```

### 2. Call Client.Send with a context deadline

`Send` takes `ctx context.Context` as its first argument and a `primitive.SendParams` struct. Give the context a deadline: see [Context, Timeouts, and Cancellation](https://test.abhinandan.one/go-context-timeouts.md) for how Go SDK methods use `context.Context` instead of a separate request-options type.

```go
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

result, err := client.Send(ctx, primitive.SendParams{
	From:     "Support <support@example.com>",
	To:       "alice@example.com",
	Subject:  "Hello",
	BodyText: "Hi there",
})
if err != nil {
	log.Fatal(err)
}

log.Println(result.ID, result.Status, result.Accepted)
```

### 3. Read the result

`Send` returns a `primitive.SendResult`:

```go
type SendResult struct {
	ID                   string
	Status               primitiveapi.SentEmailStatus
	QueueID              primitiveapi.NilString
	Accepted             []string
	Rejected             []string
	ClientIdempotencyKey string
	RequestID            string
	ContentHash          string
	IdempotentReplay     bool
	DeliveryStatus       primitiveapi.OptDeliveryStatus
	SMTPResponseCode     primitiveapi.OptNilInt
	SMTPResponseText     primitiveapi.OptString
}
```

`IdempotentReplay` is `true` when the response replays a previously-recorded send keyed by `ClientIdempotencyKey` (same key, same canonical payload), false on a fresh send and on gate-denied responses. `DeliveryStatus`, `SMTPResponseCode`, and `SMTPResponseText` are only populated when you use [wait mode](#wait-for-delivery-confirmation).

`SendParams` requires `From`, `To`, `Subject`, and at least one of `BodyText` or `BodyHTML`. Validation runs client-side before any network call:

- `From` and `To` must be 3-998 and 3-320 characters respectively.
- `To` must parse as a valid email address (bare or `Display Name <addr>` form).
- `Subject` must be non-empty.
- `WaitTimeoutMs`, if set, must be between 1000 and 30000.

A validation failure returns a Go `error` immediately, with no request sent.

## Wait for delivery confirmation

By default, `Send` returns as soon as Primitive accepts the message, it does not wait for the receiving mail server to respond. Pass `Wait: &wait` (a `*bool`, since `false` and unset are distinct in the generated request) to hold the request open until the first downstream SMTP delivery outcome:

```go
wait := true

result, err := client.Send(ctx, primitive.SendParams{
	From:          "Support <support@example.com>",
	To:            "alice@example.com",
	Subject:       "Hello",
	BodyText:      "Hi there",
	Wait:          &wait,
	WaitTimeoutMs: 5000,
})
if err != nil {
	log.Fatal(err)
}

log.Println(result.DeliveryStatus, result.SMTPResponseCode)
```

`WaitTimeoutMs` bounds how long the server waits for an outcome before returning `wait_timeout`; it defaults to 30000 ms when omitted and must be between 1000 and 30000 ms. With `Wait` set, `Send` and `Reply` keep the HTTP request open until Primitive's downstream SMTP transaction completes, so give the request a `context.Context` deadline long enough for SMTP delivery.

See [Inbound and Outbound Email Model](https://test.abhinandan.one/email-model.md) for the full wait-mode and delivery-status contract shared across every SDK. The terminal `DeliveryStatus` values are:

| Status | Meaning |
|---|---|
| `delivered` | Accepted by the receiving MTA |
| `bounced` | Rejected by the receiving MTA (the HTTP response is still 200 OK) |
| `deferred` | Temporary failure; the receiving MTA may retry |
| `wait_timeout` | No outcome observed in time, treat as "outcome unknown," the send may still complete after the response returns |

> **Tip:** `Forward` (see [Forwarding Emails](https://test.abhinandan.one/go-forwarding-emails.md)) has no `Wait` option and always returns as soon as the API accepts the message.

## Dedupe retries with IdempotencyKey

Set `IdempotencyKey` on `SendParams` to make retries safe. Use a unique key per logical send, reusing a key returns the original response from the first send instead of sending a duplicate:

```go
result, err := client.Send(ctx, primitive.SendParams{
	From:           "Support <support@example.com>",
	To:             "alice@example.com",
	Subject:        "Hello",
	BodyText:       "Hi there",
	IdempotencyKey: "customer-key-abc123",
})
```

Check `result.IdempotentReplay` to tell a fresh send apart from a replayed one. `ForwardParams` also accepts `IdempotencyKey` for the same reason.

> **Warning:** A `WaitTimeoutMs` outside 1000-30000 fails client-side validation before any request is sent. Set it only alongside `Wait: &wait`; it has no effect otherwise.

## Threading a new send

Pass `Thread` to link a new outbound email into an existing conversation via `In-Reply-To` and `References` headers:

```go
result, err := client.Send(ctx, primitive.SendParams{
	From:     "Support <support@example.com>",
	To:       "alice@example.com",
	Subject:  "Re: Hello",
	BodyText: "Following up",
	Thread: &primitive.SendThread{
		InReplyTo:  "<parent-message-id@example.com>",
		References: []string{"<root@example.com>", "<parent-message-id@example.com>"},
	},
})
```

For replying to an inbound `ReceivedEmail` directly (server-derived recipients, subject, and threading), use `Client.Reply` instead, see [Replying to Emails](https://test.abhinandan.one/go-replying-to-emails.md).

## Errors

A non-2xx response or a mapping failure returns a `*primitive.APIError`, carrying `StatusCode`, `Code`, `Message`, `RetryAfter`, `Gates`, `RequestID`, and `Details`. See [Error Handling](https://test.abhinandan.one/go-error-handling.md) for the full type and how to inspect it with `errors.As`.
