Replying to Emails
Use Client.Reply to send a threaded reply to an inbound email, with server-derived recipients and subject, an optional From override, and inline attachments.
Client.Reply sends an outbound reply to an inbound ReceivedEmail, letting the server derive the recipient, Re: subject, and threading headers instead of you rebuilding them by hand. Reach for it any time a handler needs to respond to the email it just received.
What the server derives for you#
The server derives the recipients, the Re: <parent> subject, and the In-Reply-To / References threading headers from the inbound row; you supply only the body and a few optional fields. Reply calls POST /emails/{id}/reply using the inbound email's ID. Recipients, the Re: <parent> subject, and the In-Reply-To / References threading headers are all derived server-side from that inbound row, you only control the body, an optional From override, optional attachments, and the Wait flag.
ReplyParams intentionally has no Subject field. Gmail's Conversation View needs both a References match and a normalized-subject match to thread correctly; a custom subject silently breaks threading for part of the recipient population. Use Client.Send if you need full subject control instead.
- 1
Receive and normalize the inbound email#
Verify and normalize the inbound webhook into a
ReceivedEmailwithprimitive.Receive, covered in Receiving and Verifying Webhooks.import primitive "github.com/primitivedotdev/sdks/sdk-go" email, err := primitive.Receive(primitive.HandleWebhookOptions{ Body: body, Headers: headers, Secret: "whsec_...", }) if err != nil { log.Printf("invalid webhook: %v", err) return } - 2
Construct a client#
client, err := primitive.NewClient("prim_test") if err != nil { log.Fatal(err) }See Client and Configuration for
NewClientWithOptionsand base-URL overrides. - 3
Call Client.Reply#
ctx := context.Background() result, err := client.Reply(ctx, email, primitive.ReplyParams{ BodyText: "Thank you for your email.", }) if err != nil { log.Printf("reply failed: %v", err) return } log.Println(result.ID, result.Status)Replyrequires eitherBodyTextorBodyHTML(or both); an empty body returns an error before any request is made.
ReplyParams fields#
ReplyParams carries five fields: BodyText, BodyHTML, From, Attachments, and Wait.
type ReplyParams struct {
BodyText string
BodyHTML string
From string
Attachments []SendAttachment
Wait *bool
}
| Field | Purpose |
|---|---|
BodyText / BodyHTML | The reply body. Provide at least one; both are accepted as siblings. |
From | Overrides the From header. Defaults server-side to the address that received the inbound email. |
Attachments | Inline SendAttachment values (base64 content). |
Wait | Pointer to a bool. When true, mirrors wait mode on Send. |
Reply from a different address#
Set ReplyParams.From to override the From header. Reply otherwise defaults it to the inbound recipient, the address that received the email. When your verified outbound domain differs from your inbound domain, pass From explicitly:
_, err = client.Reply(ctx, email, primitive.ReplyParams{
BodyText: "Thanks for your email.",
From: "notifications@outbound.example.com",
})
HTML replies, attachments, and waiting for delivery#
Set BodyHTML alongside BodyText, pass base64 Attachments, and point Wait at true to hold the response open for the delivery outcome, all on the same ReplyParams:
wait := true
_, err = client.Reply(ctx, email, primitive.ReplyParams{
BodyText: "Thanks for your email.",
BodyHTML: "<p>Thanks for your email.</p>",
Attachments: []primitive.SendAttachment{
{
Filename: "report.txt",
ContentBase64: "aGVsbG8=",
},
},
Wait: &wait,
})
Wait is a *bool (not a bare bool) so the SDK can distinguish "unset" from "explicitly false." When Wait points to true, the call holds the HTTP response open until the first downstream SMTP delivery status (delivered, bounced, deferred, or wait_timeout), or WaitTimeoutMs (default 30000ms) elapses. Set a context.Context deadline of 30-60 seconds when waiting, see Context, Timeouts, and Cancellation.
Pitfalls#
If the inbound row isn't in a repliable state, the email was rejected, its content was discarded, or no recipient was recorded, the API returns inbound_not_repliable (HTTP 422) and Reply returns an error. A missing Message-Id does not block the reply; it only omits the threading headers on the outbound message.
Passing a Subject is not supported. If you need to control the subject line, use Client.Send directly instead of Reply.
Return value and errors#
Reply returns a SendResult and an error; non-2xx responses come back as a *primitive.APIError. The SendResult shape is identical to Send's: ID, Status, QueueID, Accepted, Rejected, ClientIdempotencyKey, RequestID, ContentHash, IdempotentReplay, and (when Wait was used) DeliveryStatus, SMTPResponseCode, SMTPResponseText.
Non-2xx responses map to a *primitive.APIError carrying StatusCode, Code, Message, RequestID, Gates, and RetryAfter (on 429). See Error Handling for the full type and how to distinguish it from a context.Canceled / context.DeadlineExceeded client-side abort.
Next steps#
Send an inbound email to a new recipient with Client.Forward.
Sending EmailsSend new outbound mail with Client.Send and full subject control.
Receiving and Verifying WebhooksNormalize an inbound webhook into a ReceivedEmail before replying.
Error HandlingInspect APIError and distinguish it from client-side context errors.
Was this page helpful?