eusend
SDKs

Go SDK

Official Go SDK for the eusend API. Zero dependencies, context-aware, and shaped to mirror resend-go.

Official Go SDK for the eusend API. Its shape mirrors resend-go, so migrating from Resend is largely a resendeusend rename. Zero dependencies (standard library only), context.Context-aware, and safe for concurrent use. Requires Go 1.21+.

Installation

go get github.com/eusend-dev/eusend-go

Getting started

Create a client with your API key. Pass an empty string to read EUSEND_API_KEY from the environment.

import eusend "github.com/eusend-dev/eusend-go"

client := eusend.NewClient("eu_live_...") // or NewClient("") to read EUSEND_API_KEY

Every method has a WithContext variant that takes a context.Context as its first argument (e.g. client.Emails.SendWithContext(ctx, params)). The context-free forms use context.Background(). Optional pointer fields have helpers: eusend.Bool(true), eusend.String("x").

Sending an email

From and To are required; provide at least one of Html, Text, or TemplateId.

sent, err := client.Emails.Send(&eusend.SendEmailRequest{
	// From accepts a bare email or a display-name form: "Acme <you@yourdomain.com>"
	From:    "hello@yourdomain.com",
	To:      []string{"customer@example.com"},
	Subject: "Your order is confirmed",
	Html:    "<p>Thanks for your order!</p>",
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(sent.Id) // 9a8b7c6d-...

Send options

FieldTypeDescription
FromstringSender address — bare email or "Name <email>"
To Cc Bcc ReplyTo[]stringMax 50 each
SubjectstringEmail subject
Html / TextstringHTML / plain-text body
TemplateIdstringID of a saved template
Variablesmap[string]anyTemplate variable substitutions (HTML-escaped)
Headersmap[string]stringCustom headers; no line breaks in names or values
TrackOpens / TrackClicks*boolDefault true; eusend.Bool(false) to disable
Attachments[]*AttachmentUp to 20, 10 MB combined. See below.
ScheduledAtstringSchedule for a future time, at most 30 days out

Attachments

Provide Content (raw bytes, base64-encoded on the wire) or Path (a public URL fetched at send time). Set ContentId for an inline <img src="cid:...">.

pdf, _ := os.ReadFile("invoice.pdf")

client.Emails.Send(&eusend.SendEmailRequest{
	From:    "you@yourdomain.com",
	To:      []string{"customer@example.com"},
	Subject: "Your invoice",
	Html:    "<p>Attached.</p>",
	Attachments: []*eusend.Attachment{
		{Filename: "invoice.pdf", Content: pdf, ContentType: "application/pdf"},
	},
})

Idempotent sends

client.Emails.SendWithOptions(ctx, params, &eusend.SendEmailOptions{
	IdempotencyKey: "receipt-" + orderID,
})

Retrying with the same key never sends a duplicate and returns the original ID.

Batch send

Send up to 100 emails in a single request. Attachments and scheduling are stripped (not supported on the batch endpoint). Results map positionally to the input: queued items carry Id, rejected items carry Error and Code.

res, _ := client.Batch.Send([]*eusend.SendEmailRequest{
	{From: "you@yourdomain.com", To: []string{"alice@example.com"}, Subject: "Hi", Html: "<p>Hi</p>"},
	{From: "you@yourdomain.com", To: []string{"bob@example.com"},   Subject: "Hi", Html: "<p>Hi</p>"},
})
for _, r := range res.Data {
	if r.Id != "" {
		fmt.Println("queued", r.Id)
	} else {
		fmt.Printf("failed: %s (%s)\n", r.Error, r.Code)
	}
}

Scheduled sends

ScheduledAt accepts an ISO 8601 string or natural language like "in 1 hour" (at most 30 days out), parsed server-side in UTC.

sent, _ := client.Emails.Send(&eusend.SendEmailRequest{
	From: "you@yourdomain.com", To: []string{"customer@example.com"},
	Subject: "Your trial ends tomorrow", Html: "<p>Reminder.</p>",
	ScheduledAt: "in 1 hour",
})

client.Emails.Update(&eusend.UpdateEmailRequest{Id: sent.Id, ScheduledAt: "2026-07-04T09:00:00Z"})
client.Emails.Cancel(sent.Id)

Retrieve & list emails

email, _ := client.Emails.Get("9a8b7c6d-...")
fmt.Println(email.Status, email.Events[0].Type)

page, _ := client.Emails.List(&eusend.ListEmailsOptions{Status: "delivered", Limit: 50})
for _, e := range page.Data {
	fmt.Println(e.Id, e.Subject)
}
// page.NextCursor -> pass as ListEmailsOptions{Cursor: ...} for the next page

Domains

created, _ := client.Domains.Create(&eusend.CreateDomainRequest{Name: "yourdomain.com"})
fmt.Println(created.Dkim.Name, created.Dkim.Value) // DNS records to add
fmt.Println(created.Spf, created.Dmarc)

client.Domains.Verify(created.Id) // after publishing the records
client.Domains.List()
client.Domains.Get(created.Id)
client.Domains.Remove(created.Id)

API keys

key, _ := client.ApiKeys.Create(&eusend.CreateApiKeyRequest{Name: "Production"})
fmt.Println(key.Key) // eu_live_... — returned only once

client.ApiKeys.Create(&eusend.CreateApiKeyRequest{Name: "Sandbox", TestMode: true}) // eu_test_... key
client.ApiKeys.List()                                                               // prefixes only
client.ApiKeys.Remove(key.Id)

Templates

{{variable}} placeholders are substituted at send time; values are HTML-escaped.

tpl, _ := client.Templates.Create(&eusend.CreateTemplateRequest{
	Name:    "Welcome email",
	Subject: "Welcome, {{name}}!",
	Html:    "<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>",
})

client.Emails.Send(&eusend.SendEmailRequest{
	From: "you@yourdomain.com", To: []string{"customer@example.com"},
	TemplateId: tpl.Id,
	Variables:  map[string]any{"name": "Jane", "product": "Acme"},
})

client.Templates.List()
client.Templates.Get(tpl.Id)
client.Templates.Update(tpl.Id, &eusend.UpdateTemplateRequest{Subject: eusend.String("New subject")})
client.Templates.Remove(tpl.Id)

Audiences & contacts

Contact operations are grouped under Audiences (they live under a specific audience).

audience, _ := client.Audiences.Create(&eusend.CreateAudienceRequest{Name: "Newsletter"})

client.Audiences.CreateContact(audience.Id, &eusend.CreateContactRequest{
	Email: "user@example.com", FirstName: "Jane",
})

// Bulk upsert (up to 1,000)
client.Audiences.BatchCreateContacts(audience.Id, []*eusend.CreateContactRequest{
	{Email: "alice@example.com", FirstName: "Alice"},
	{Email: "bob@example.com", FirstName: "Bob"},
})

page, _ := client.Audiences.ListContacts(audience.Id, &eusend.ListContactsOptions{
	Subscribed: eusend.Bool(true), Search: "gmail.com",
})
contact := page.Data[0]

client.Audiences.UpdateContact(audience.Id, contact.Id, &eusend.UpdateContactRequest{
	Unsubscribed: eusend.Bool(true),
})
client.Audiences.GetContact(audience.Id, contact.Id)
client.Audiences.RemoveContact(audience.Id, contact.Id)

client.Audiences.List()
client.Audiences.Remove(audience.Id)

Broadcasts

{{first_name}}, {{last_name}}, {{full_name}}, and {{email}} are available per recipient, and RFC 8058 one-click unsubscribe headers are added automatically.

bc, _ := client.Broadcasts.Create(&eusend.CreateBroadcastRequest{
	Name:       "May newsletter",
	AudienceId: audience.Id,
	From:       "Sivert <hello@yourdomain.com>",
	Subject:    "May update",
	Html:       "<p>Hi {{first_name}}, your monthly update is here...</p>",
})

client.Broadcasts.Send(bc.Id, nil)                                                              // send now
client.Broadcasts.Send(bc.Id, &eusend.SendBroadcastRequest{ScheduledAt: "2026-06-01T09:00:00Z"}) // or schedule
client.Broadcasts.Cancel(bc.Id)

client.Broadcasts.List()
client.Broadcasts.Get(bc.Id) // includes delivery stats
client.Broadcasts.Update(bc.Id, &eusend.UpdateBroadcastRequest{Subject: eusend.String("Updated")})
client.Broadcasts.Remove(bc.Id)

Webhooks

hook, _ := client.Webhooks.Create(&eusend.CreateWebhookRequest{
	Url:    "https://yourapp.com/webhooks/eusend",
	Events: []string{"email.sent", "email.delivered", "email.bounced", "email.complained"},
	// or: Events: []string{"*"} to receive everything
})
fmt.Println(hook.Secret) // signing secret — store it securely, only returned once

client.Webhooks.List()
client.Webhooks.Get(hook.Id) // includes recent deliveries
client.Webhooks.Update(hook.Id, &eusend.UpdateWebhookRequest{Events: []string{"email.bounced"}})
client.Webhooks.Remove(hook.Id)

Verifying webhook signatures

Every delivery is signed with HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{body}:

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
)

func verify(r *http.Request, body []byte, secret string) bool {
	signed := r.Header.Get("webhook-id") + "." + r.Header.Get("webhook-timestamp") + "." + string(body)
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(signed))
	expected := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(r.Header.Get("webhook-signature")), []byte(expected))
}

Error handling

Every method returns (result, error). Any non-2xx response, and any network failure, is an *eusend.Error. On a 429, RetryAfter, RateLimitReset, and RateLimitRemaining are populated from the response headers.

sent, err := client.Emails.Send(params)
if err != nil {
	var apiErr *eusend.Error
	if errors.As(err, &apiErr) {
		fmt.Println(apiErr.Code)       // "MONTHLY_LIMIT_EXCEEDED"
		fmt.Println(apiErr.Message)    // "Monthly send limit exceeded"
		fmt.Println(apiErr.StatusCode) // 429 (0 for a network failure)

		if apiErr.Code == eusend.CodeMonthlyLimitExceeded {
			// back off and retry later
		}
	}
	return
}
Code constantWire valueStatus
CodeUnauthorizedUNAUTHORIZED401
CodeForbiddenFORBIDDEN403
CodeNotFoundNOT_FOUND404
CodeValidationErrorVALIDATION_ERROR400
CodeBadRequestBAD_REQUEST400
CodeConflictCONFLICT409
CodeRateLimitedRATE_LIMITED429
CodeMonthlyLimitExceededMONTHLY_LIMIT_EXCEEDED429
CodeDailyLimitExceededDAILY_LIMIT_EXCEEDED429
CodePlanLimitExceededPLAN_LIMIT_EXCEEDED403
CodeDomainNotVerifiedDOMAIN_NOT_VERIFIED403
CodeSendingSuspendedSENDING_SUSPENDED403
CodeAllSuppressedALL_SUPPRESSED422
CodeServicePausedSERVICE_PAUSED503
CodeInternalErrorINTERNAL_ERROR500
CodeApplicationErrorapplication_error— (network failure)

View the full source and changelog on GitHub.