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 resend → eusend rename. Zero dependencies (standard library only),
context.Context-aware, and safe for concurrent use. Requires Go 1.21+.
⬡ Node.js / TypeScript
@eusend_dev/sdk
🐍 Python
pip install eusend
🐹 Go
go get github.com/eusend-dev/eusend-go
Installation
go get github.com/eusend-dev/eusend-goGetting 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_KEYEvery 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
| Field | Type | Description |
|---|---|---|
From | string | Sender address — bare email or "Name <email>" |
To Cc Bcc ReplyTo | []string | Max 50 each |
Subject | string | Email subject |
Html / Text | string | HTML / plain-text body |
TemplateId | string | ID of a saved template |
Variables | map[string]any | Template variable substitutions (HTML-escaped) |
Headers | map[string]string | Custom headers; 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. See below. |
ScheduledAt | string | Schedule 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 pageDomains
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 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) |
View the full source and changelog on GitHub.