Official Go client for the Sendara email API — transactional and marketing email, broadcasts, contacts, lists, templates, domains, signed inbound webhook events, and billing.
Zero non-stdlib dependencies. Typed request/response structs, a typed error type
with classification helpers, automatic retries with backoff (honoring
Retry-After), idempotency-key auto-generation, a cursor-based message
iterator, multipart image uploads, and an outbound-webhook signature verifier.
go get github.com/sendaramail/sendara-go@v0.4.0import sendara "github.com/sendaramail/sendara-go"package main
import (
"context"
"fmt"
"log"
sendara "github.com/sendaramail/sendara-go"
)
func main() {
client := sendara.New("sk_live_...")
resp, err := client.Emails.Send(context.Background(), sendara.EmailSendParams{
From: "hello@yourdomain.com",
To: "user@example.com",
Subject: "Welcome",
HTML: "<h1>Hi there</h1>",
})
if err != nil {
log.Fatal(err)
}
fmt.Println("queued:", resp.ID)
}The client takes functional options:
client := sendara.New(
"sk_live_...",
sendara.WithBaseURL("https://api.sendara.dev"),
sendara.WithTimeout(15 * time.Second),
sendara.WithMaxRetries(5),
sendara.WithHTTPClient(myHTTPClient),
)| Option | Description |
|---|---|
WithBaseURL(url) |
Override the API base URL. |
WithTimeout(d) |
Per-request timeout (default 30s; no-op with a custom HTTP client). |
WithMaxRetries(n) |
Bound automatic retries (default 3; 0 disables). |
WithHTTPClient(c) |
Inject a custom HTTP client / transport. |
WithUserAgent(s) |
Append a token to the SDK User-Agent. |
Every non-2xx response returns a *sendara.Error exposing Status, Code,
Message, and RetryAfter. Branch on the failure class with helpers:
resp, err := client.Emails.Send(ctx, params)
if err != nil {
var apiErr *sendara.Error
if errors.As(err, &apiErr) {
switch {
case apiErr.IsRateLimited():
time.Sleep(apiErr.RetryAfter)
case apiErr.IsAuthentication():
log.Fatal("bad API key")
case apiErr.IsValidation():
log.Printf("invalid request: %s", apiErr.Message)
}
}
}Classifiers: IsAuthentication (401), IsPermission (403), IsNotFound (404),
IsConflict (409), IsValidation (400/422), IsRateLimited (429), IsServer
(5xx), IsTransport (network error).
GETs and idempotent calls (sends, deletes, and PUT updates) are retried
automatically on 429, 5xx, and transport errors, with exponential backoff +
full jitter, honoring a server Retry-After. Non-idempotent POSTs (create
contact/list/template, API-key rotation, webhook-secret rotation, etc.) are
never retried.
Sends accept an IdempotencyKey; when omitted the SDK auto-generates a UUID so
the call can be retried safely. Override it to dedupe across process restarts.
EmailSendParams also supports TemplateID, TemplateVars, TestSend,
ScheduledAt, and ValidateRecipient; scheduled sends need at least one minute
of lead time to enter the scheduler.
Messages.Iterator transparently follows the opaque cursor:
it := client.Messages.Iterator(sendara.ListMessagesParams{Channel: "email"})
for it.Next(ctx) {
fmt.Println(it.Message().ID)
}
if err := it.Err(); err != nil {
log.Fatal(err)
}Or grab a single page with Messages.List (which also accepts a Search
filter), or collect everything with it.All(ctx).
Fetch one message with its full event timeline, by ID or by the idempotency key from the original send:
msg, err := client.Messages.Get(ctx, sendara.MessageQuery{ID: "msg_123"})
msg, err := client.Messages.Get(ctx, sendara.MessageQuery{IdempotencyKey: "order-42"})Checkout accepts the Starter, Pro, Growth, and Scale plans with monthly or yearly billing. The result covers both a new hosted checkout and an in-place change to an existing subscription:
checkout, err := client.Billing.Checkout(ctx, sendara.CheckoutParams{
Plan: sendara.BillingPlanGrowth,
Period: sendara.BillingPeriodYear,
})
if err != nil {
log.Fatal(err)
}
if checkout.Updated {
fmt.Println("subscription changed to", checkout.Plan)
} else {
fmt.Println("redirect to", checkout.URL)
}Verify the signature of inbound webhook deliveries (HMAC-SHA256 of
"<timestamp>.<rawBody>", hex-encoded):
func handler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
event, err := sendara.VerifyWebhookRequest(
webhookSigningSecret, r.Header, body, 5*time.Minute,
)
if err != nil {
http.Error(w, "invalid signature", http.StatusForbidden)
return
}
fmt.Println("event:", event.EventType, event.MessageID)
w.WriteHeader(http.StatusOK)
}VerifyWebhook(secret, timestamp, signature, body, tolerance) is the lower-level
primitive. Pass tolerance == 0 to skip the replay-window check.
| Namespace | Methods |
|---|---|
client.Emails |
Send |
client.Send |
Send, Batch |
client.Broadcasts |
Create, List, Get, Send, Cancel, Delete, BulkSend |
client.Messages |
List, Get, Iterator (+ Next/Message/Err/All) |
client.Suppressions |
List, Create, Delete |
client.Domains |
List, Create, Get, Verify |
client.APIKeys |
List, Create, Rotate, Revoke |
client.Usage |
Get, SetSpendCap |
client.Billing |
Get, Checkout, Portal |
client.Templates |
Create, List, Get, Update, Delete, Render |
client.Contacts |
Create, List, Get, Update, Delete, Import |
client.Contacts.Lists |
Create, List, Get, Update, Delete, AddMember, RemoveMember, Members |
client.Webhooks |
Create, List, Get, Update, Delete, Deliveries, RotateSecret |
client.Uploads |
Upload, UploadBytes |
client.TestRecipients |
List, Create, Resend, Delete |
Package-level helpers: VerifyWebhook, VerifyWebhookRequest, and the optional
pointer constructors String, Bool, Int64, NullableString for building
update params.
cd sdk/go
go build ./...
go test ./...MIT