Official Go SDK for the Eusend API — the EU-native transactional email platform.
Its shape mirrors resend-go, so migrating from Resend is largely a resend → eusend rename.
NewClient+ service methods —client.Emails.Send(...), withWithContextvariants.- Zero dependencies — standard library only.
- Concurrency-safe — create one
*Clientand share it.
go get github.com/eusend-dev/eusend-goRequires Go 1.21+.
package main
import (
"fmt"
"log"
eusend "github.com/eusend-dev/eusend-go"
)
func main() {
client := eusend.NewClient("eu_live_...") // or NewClient("") to read EUSEND_API_KEY
sent, err := client.Emails.Send(&eusend.SendEmailRequest{
// From accepts a bare email or a display-name form: "Acme <you@yourdomain.com>"
From: "Acme <you@yourdomain.com>",
To: []string{"user@example.com"},
Subject: "Hello",
Html: "<p>Hello world</p>",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(sent.Id) // 9a8b7c6d-... (UUID)
}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 (*bool, *string) have helpers: eusend.Bool(true), eusend.String("x").
From and To are required; provide at least one of Html, Text, or TemplateId.
| Field | Type | Notes |
|---|---|---|
From |
string |
Verified domain; bare or display-name form. |
To Cc Bcc ReplyTo |
[]string |
Max 50 each. |
Subject |
string |
|
Html / Text |
string |
|
TemplateId |
string |
Saved template. |
Variables |
map[string]any |
Template substitutions (HTML-escaped). |
Headers |
map[string]string |
No line breaks in names or values. |
TrackOpens / TrackClicks |
*bool |
Default true; eusend.Bool(false) to disable. |
Attachments |
[]*Attachment |
Up to 20, 10 MB combined. |
ScheduledAt |
string |
Future send, ≤ 30 days. |
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{"user@example.com"},
Subject: "Your invoice",
Html: "<p>Attached.</p>",
Attachments: []*eusend.Attachment{
{Filename: "invoice.pdf", Content: pdf, ContentType: "application/pdf"},
},
})client.Emails.SendWithOptions(ctx, params, &eusend.SendEmailOptions{
IdempotencyKey: "receipt-" + orderID,
})Retrying with the same key never sends a duplicate and returns the original ID.
ScheduledAt accepts an ISO 8601 string or natural language ("in 1 hour",
"tomorrow at 9am"), parsed server-side in UTC.
sent, _ := client.Emails.Send(&eusend.SendEmailRequest{
From: "you@yourdomain.com", To: []string{"user@example.com"},
Subject: "Reminder", Html: "<p>Soon.</p>",
ScheduledAt: "in 1 hour",
})
client.Emails.Update(&eusend.UpdateEmailRequest{Id: sent.Id, ScheduledAt: "in 2 hours"})
client.Emails.Cancel(sent.Id)Up to 100 emails in one 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)
}
}email, _ := client.Emails.Get("9a8b7c6d-...")
fmt.Println(email.Status, email.Events[0].Type)
page, _ := client.Emails.List(&eusend.ListEmailsOptions{Limit: 20, Status: "delivered"})
for _, e := range page.Data {
fmt.Println(e.Id, e.Subject)
}
if page.NextCursor != "" {
page, _ = client.Emails.List(&eusend.ListEmailsOptions{Cursor: page.NextCursor})
}Statuses: queued scheduled sending sent delivered bounced complained suppressed failed.
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 DNS records
client.Domains.List()
client.Domains.Get(created.Id)
client.Domains.Remove(created.Id)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)Emails sent with a test key are accepted and tracked but never delivered.
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){{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{"user@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)hook, _ := client.Webhooks.Create(&eusend.CreateWebhookRequest{
Url: "https://yourapp.com/webhooks/eusend",
Events: []string{"email.delivered", "email.bounced", "email.complained"}, // or []string{"*"}
})
fmt.Println(hook.Secret) // signing secret — returned only 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)Events: email.sent email.delivered email.bounced email.complained
email.opened email.clicked. The endpoint must be a public http(s) URL
returning 2xx directly (redirects count as failures).
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))
}Send one email to every contact in an audience. {{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)Calling Send on a paused broadcast resumes it from where it stopped.
Every method returns (result, error). Any non-2xx response, and any network
failure, is an *eusend.Error:
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
}On a 429, apiErr.RetryAfter, RateLimitReset, and RateLimitRemaining are
populated from the response headers.
| Code constant | Wire value | Status |
|---|---|---|
CodeUnauthorized |
UNAUTHORIZED |
401 |
CodeForbidden |
FORBIDDEN |
403 |
CodeNotFound |
NOT_FOUND |
404 |
CodeValidationError |
VALIDATION_ERROR |
400 |
CodeBadRequest |
BAD_REQUEST |
400 |
CodeConflict |
CONFLICT |
409 |
CodeRateLimited |
RATE_LIMITED |
429 |
CodeMonthlyLimitExceeded |
MONTHLY_LIMIT_EXCEEDED |
429 |
CodeDailyLimitExceeded |
DAILY_LIMIT_EXCEEDED |
429 |
CodePlanLimitExceeded |
PLAN_LIMIT_EXCEEDED |
403 |
CodeDomainNotVerified |
DOMAIN_NOT_VERIFIED |
403 |
CodeSendingSuspended |
SENDING_SUSPENDED |
403 |
CodeAllSuppressed |
ALL_SUPPRESSED |
422 |
CodeServicePaused |
SERVICE_PAUSED |
503 |
CodeInternalError |
INTERNAL_ERROR |
500 |
CodeApplicationError |
application_error |
— (network failure) |
// Custom http.Client (timeouts, proxies, ...):
client := eusend.NewCustomClient(&http.Client{Timeout: 60 * time.Second}, "eu_live_...")
// Override the base URL (e.g. for testing) after construction:
client.BaseURL, _ = url.Parse("https://api.eusend.dev/")