Sendkit TeamHow to send email with Go
Send email from Go applications using the Sendkit SDK. Covers installation, HTML email, MIME sending, error handling, and SMTP as a fallback.

Go's standard library has net/smtp, and it does work. But it feels like writing email in 2006. You build MIME messages by hand, manage TLS connections yourself, and parse cryptic SMTP reply codes when something goes wrong. For a language that prides itself on getting out of your way, the email story is surprisingly tedious.
An email API fixes this. You make an HTTP call, get back a message ID, and move on. This guide covers sending email from Go with the Sendkit email API, starting with basic sends and working through HTML, MIME, error handling, and SMTP for legacy setups.
Install the Go SDK
go get github.com/sendkitdev/sendkit-goImport it in your code:
import sendkit "github.com/sendkitdev/sendkit-go"Create a client with your API key from the Sendkit dashboard:
client, err := sendkit.NewClient("sk_live_your_api_key")
if err != nil {
log.Fatal(err)
}Don't hardcode the key. Pass an empty string and the SDK reads the SENDKIT_API_KEY environment variable instead:
client, err := sendkit.NewClient("")
if err != nil {
log.Fatal(err)
}The client constructor also accepts functional options if you need to customize things:
client, err := sendkit.NewClient("",
sendkit.WithBaseURL("https://api.sendkit.dev/v1"),
sendkit.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}),
)Most people won't need those. The defaults are fine.
Send a plain text email
resp, err := client.Emails.Send(context.Background(), &sendkit.SendEmailParams{
From: "[email protected]",
To: []string{"[email protected]"},
Subject: "Your order has shipped",
Text: "Order #4821 shipped via FedEx. Tracking number: 7489201384.",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.ID) // msg_8f2a...The From address must be on a domain you've verified in Sendkit. If you haven't done that yet, check the guide on setting up DMARC, DKIM, and SPF first.
To is a string slice, so you can send to multiple recipients in one call. More on that below.
Send HTML email
Most transactional email needs some formatting. Pass both Text and HTML so clients that can't render HTML still show something readable.
func sendWelcomeEmail(client *sendkit.Client, email, name string) (string, error) {
html := fmt.Sprintf(`
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Welcome, %s</h2>
<p>Your account is set up and ready to go.</p>
<p>A few things to try first:</p>
<ul>
<li>Complete your profile</li>
<li>Connect your first integration</li>
<li>Invite your team</li>
</ul>
<a href="https://app.yoursite.com/getting-started"
style="background: #0066ff; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 4px;
display: inline-block; margin-top: 16px;">
Get started
</a>
</div>`, name)
resp, err := client.Emails.Send(context.Background(), &sendkit.SendEmailParams{
From: "[email protected]",
To: []string{email},
Subject: fmt.Sprintf("Welcome, %s", name),
Text: fmt.Sprintf("Hey %s, thanks for signing up. Your account is ready.", name),
HTML: html,
})
if err != nil {
return "", err
}
return resp.ID, nil
}Use inline CSS in your HTML. Email clients strip <style> blocks and ignore external stylesheets, so inline styles are the only thing that works across Gmail, Outlook, and Apple Mail.

Send to multiple recipients
Since To takes a slice, sending to several addresses is straightforward:
resp, err := client.Emails.Send(context.Background(), &sendkit.SendEmailParams{
From: "[email protected]",
To: []string{"[email protected]", "[email protected]"},
Subject: "[ALERT] Database connection pool exhausted",
Text: "Connection pool hit 100% at 14:32 UTC. Auto-scaling triggered.",
})If each recipient needs different content, loop and send individually. The API docs cover templates if you want server-side personalization.
Before sending to a list of addresses, consider validating them first. Sending to invalid addresses tanks your sender reputation, and once that's damaged it affects everything you send. Sendkit has built-in email validation that catches typos, disposable addresses, and dead mailboxes. There's a separate guide on validating email addresses before sending if you want the details.
Error handling
Go makes you deal with errors, which is actually a good thing here. If you skip error handling on email sends, you'll find out when your password reset flow silently stops working.
The SDK returns *sendkit.APIError for API-level failures. Use errors.As to inspect it:
import (
"context"
"errors"
"fmt"
"log"
sendkit "github.com/sendkitdev/sendkit-go"
)
func sendEmailSafely(client *sendkit.Client, params *sendkit.SendEmailParams) (string, error) {
resp, err := client.Emails.Send(context.Background(), params)
if err != nil {
var apiErr *sendkit.APIError
if errors.As(err, &apiErr) {
switch apiErr.StatusCode {
case 422:
// Bad input: invalid address, missing field
return "", fmt.Errorf("validation error: %s", apiErr.Message)
case 429:
// Rate limited
return "", fmt.Errorf("rate limited, retry later")
default:
if apiErr.StatusCode >= 500 {
return "", fmt.Errorf("server error (%d): %s", apiErr.StatusCode, apiErr.Message)
}
return "", fmt.Errorf("api error (%s): %s", apiErr.Name, apiErr.Message)
}
}
// Network error, DNS failure, timeout
return "", fmt.Errorf("network error: %w", err)
}
return resp.ID, nil
}For production, add retry logic with backoff. Something like this works fine without pulling in a dependency:
func sendWithRetry(client *sendkit.Client, params *sendkit.SendEmailParams, maxRetries int) (string, error) {
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
id, err := sendEmailSafely(client, params)
if err == nil {
return id, nil
}
lastErr = err
// Don't retry validation errors
var apiErr *sendkit.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 422 {
return "", err
}
delay := time.Duration(1<<uint(attempt)) * time.Second
if delay > 30*time.Second {
delay = 30 * time.Second
}
log.Printf("Attempt %d failed: %v. Retrying in %v...", attempt, err, delay)
time.Sleep(delay)
}
return "", fmt.Errorf("failed after %d attempts: %w", maxRetries, lastErr)
}Three retries handles most transient problems. If things are still broken after that, push the email into a queue rather than blocking the request.
Sending raw MIME messages
If you already build MIME messages (maybe you're migrating from net/smtp and have existing MIME construction code), the SDK supports that directly:
mimeMessage := "From: [email protected]\r\n" +
"To: [email protected]\r\n" +
"Subject: Password reset\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"\r\n" +
"Click here to reset your password: https://app.yoursite.com/reset?token=abc123"
resp, err := client.Emails.SendMime(context.Background(), &sendkit.SendMimeEmailParams{
EnvelopeFrom: "[email protected]",
EnvelopeTo: "[email protected]",
RawMessage: mimeMessage,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.ID)Most people should use the regular Send method. MIME sending is there for compatibility when you already have raw messages and don't want to parse them apart.

Using SMTP instead of the API
If your codebase already uses net/smtp and you'd rather not rewrite it, Sendkit provides an SMTP relay you can point at:
import (
"net/smtp"
"strings"
)
func sendViaSMTP() error {
auth := smtp.PlainAuth("", "sendkit", "sk_live_your_api_key", "smtp.sendkit.com")
to := []string{"[email protected]"}
msg := strings.Join([]string{
"From: [email protected]",
"To: [email protected]",
"Subject: Password reset",
"",
"Click here to reset your password: https://app.yoursite.com/reset?token=abc123",
}, "\r\n")
return smtp.SendMail(
"smtp.sendkit.com:587",
auth,
"[email protected]",
to,
[]byte(msg),
)
}Your API key is your SMTP password. No extra credentials.
The API is still better for new code. No SMTP handshake overhead, you get a message ID back immediately, and errors come as structured JSON instead of SMTP reply codes. But SMTP works when you need to plug into something that already exists.
After the email leaves your app
Sending the email is the easy part. Whether it actually lands in the inbox is a different question.
First, set up domain authentication. DKIM, SPF, and DMARC are table stakes now. Without them, Gmail and Outlook will either spam-folder your messages or reject them outright. The DMARC/DKIM/SPF setup guide walks through the DNS records.
Second, watch your bounces. Sending to addresses that don't exist will damage your sender reputation, and that damage affects every email you send, not just the bad ones. Sendkit auto-suppresses bounced addresses, but keep an eye on your own bounce rates too.
Third, validate addresses before sending. If you accept user-submitted emails, a quick validation call catches typos like gmial.com and disposable addresses that will never engage.
The Sendkit docs have more on webhooks, templates, and batch sending.
Wrapping up
Install sendkit-go, call client.Emails.Send(), handle errors with errors.As, and you have working email in your Go app. The whole thing is maybe 20 lines of code.
The ongoing work is deliverability: authenticate your domain, don't ignore bounces, validate addresses. The sending code is the small part. Keeping a good sender reputation is what takes real effort.
Share this article