Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /go-sending-emails.md.
Verified · 8/11/2026

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).

Send a basic email#

  1. 1

    Construct the client#

    Build a Client with your API key:

    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. 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 for how Go SDK methods use context.Context instead of a separate request-options type.

    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. 3

    Read the result#

    Send returns a primitive.SendResult:

    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.

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:

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 for the full wait-mode and delivery-status contract shared across every SDK. The terminal DeliveryStatus values are:

StatusMeaning
deliveredAccepted by the receiving MTA
bouncedRejected by the receiving MTA (the HTTP response is still 200 OK)
deferredTemporary failure; the receiving MTA may retry
wait_timeoutNo outcome observed in time, treat as "outcome unknown," the send may still complete after the response returns
Tip

Forward (see Forwarding Emails) 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:

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:

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.

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 for the full type and how to inspect it with errors.As.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer