Send with your stack

Send emails with Go

Send email from Go with the official senddart-go module — typed request structs, context variants, and zero third-party dependencies.

The official Go module is the fastest way to send email from Go. It depends only on the standard library, sets the required User-Agent header for you, and retries 429/503 responses automatically.

The SendDart 1.0.0 module path is github.com/shekhu10/senddart-sdks/senddart-go, without a major-version suffix. The package name is senddart.

Prerequisites

  1. A verified domain for your from address. (guide)
  2. An API key (mb_...), read from SENDDART_API_KEY. (Authentication)
  3. Go 1.22 or newer — that is the floor declared in the module go.mod.

Install

go get github.com/shekhu10/senddart-sdks/senddart-go

Send an email

Construct the client with senddart.NewClient and call client.Emails.Send. delivered@test.senddart.com is the delivery simulator — safe to send to while you wire things up. Do not use example.com: it is a blocked recipient domain and the send comes back 422.

main.go
package main

import (
	"errors"
	"fmt"
	"log"
	"os"

	senddart "github.com/shekhu10/senddart-sdks/senddart-go"
)

func main() {
	client := senddart.NewClient(os.Getenv("SENDDART_API_KEY"))

	sent, err := client.Emails.Send(&senddart.SendEmailRequest{
		From:    "Acme <hello@yourdomain.com>",
		To:      []string{"delivered@test.senddart.com"},
		Subject: "Hello from Go",
		Html:    "<p>Sent with the official Go module 🐹</p>",
	})
	if err != nil {
		var apiErr *senddart.SendDartError
		if errors.As(err, &apiErr) {
			log.Fatalf("SendDart %d %s: %s", apiErr.StatusCode, apiErr.Name, apiErr.Message)
		}
		log.Fatal(err)
	}

	fmt.Println("Sent email", sent.Id)
}

Handling the response

A successful send returns *senddart.CreateEmailResponse, whose only field is Id. Keep it to retrieve the email later or to correlate webhook events.

{
  "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794"
}

Any non-2xx answer comes back as a *senddart.SendDartError carrying the { statusCode, name, message } envelope. Branch on Name together with StatusCode, never on Message — message text is scrubbed server-side and is not a stable contract. Plan and quota rejections additionally populate Limit, and reputation gates populate Reputation; both are nil on an ordinary error.

var apiErr *senddart.SendDartError
if errors.As(err, &apiErr) {
	switch apiErr.Name {
	case "validation_error":
		// 422 — a bad field, or 403 when the User-Agent header is missing.
	case "daily_quota_exceeded":
		if l := apiErr.Limit; l != nil {
			fmt.Printf("%s cap hit: %d/%d\n", l.Kind, l.Used, l.Limit)
		}
	}
}

Context and tuning

Every method has a context-aware variant with a WithContext suffix, so a send can inherit the deadline of the request that triggered it.

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

sent, err := client.Emails.SendWithContext(ctx, params)

The exported client fields may be overridden before first use. Timeout bounds each attempt (default senddart.DefaultTimeout, 30s) and MaxRetries is the retry budget for 429/503 only (default senddart.DefaultMaxRetries, 2) — no other status, network error, or timeout is retried, so a send is never silently duplicated.

client := senddart.NewClient(os.Getenv("SENDDART_API_KEY"))
client.Timeout = 10 * time.Second
client.MaxRetries = 3
The module always sends a non-empty User-Agent (senddart-go/1.0.0), which the API requires on every route — a request without one is rejected with 403 validation_error before it is even authenticated. Setting client.UserAgent = "" falls back to that default rather than producing a client that 403s on every call.
Read the mb_ API key from the environment (or your secrets manager) and keep it server-side. Never compile it into a binary you ship to users — the key can send email as your account.

Next steps