Help us shape the future of Go! Take the 2026 Go Developer Survey

vibemail

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 11 Imported by: 0

README

VibeMail SDK for Go

Official SDK for the VibeMail transactional email API. No dependencies beyond the standard library.

go get github.com/vibemailai/vibemail-go

Send an email

package main

import (
	"context"
	"log"
	"os"

	vibemail "github.com/vibemailai/vibemail-go"
)

func main() {
	client := vibemail.New(os.Getenv("VIBEMAIL_API_KEY"))

	res, err := client.Send(context.Background(), vibemail.SendRequest{
		From:    "hello@yourdomain.com",
		To:      "ada@example.com",
		Subject: "Welcome",
		HTML:    "<p>Glad you're here.</p>",
		Text:    "Glad you're here.",
	})
	if err != nil {
		log.Fatal(err)
	}
	log.Println(res.ID, res.Status)
}

Send returns once the API has accepted the message, not once it has been delivered. Use Get to follow it.

Always send Text alongside HTML. Mail with no plain-text part is measurably more likely to be spam-foldered.

Every call takes a context.Context. Cancelling it stops the request and any pending retry.

Recipients

One message goes to one recipient. To reach several people, use Batch, it sends a separate message to each, which is also what stops your recipients from seeing one another's addresses:

res, err := client.Batch(ctx, []vibemail.SendRequest{
	{To: "ada@example.com", Subject: "Welcome", Text: "Hi Ada"},
	{To: "alan@example.com", Subject: "Welcome", Text: "Hi Alan"},
})

for _, entry := range res.Data {
	if entry.Error != "" {
		log.Printf("rejected: %s", entry.Error)
	}
}

Each entry succeeds or fails on its own. A comma-joined To returns ErrTooManyRecipients rather than delivering to just one of the addresses.

Retries and idempotency

Pass an idempotency key on anything you might retry. A repeat carrying the same key returns the original result instead of sending a second copy, which matters because a client that times out has no way of knowing whether the message went out.

res, err := client.Send(ctx, req, vibemail.WithIdempotencyKey("receipt-"+orderID))

The client retries 429 and 5xx on its own (twice by default, honouring Retry-After) and never retries a request the server rejected on its merits. Use vibemail.WithMaxRetries(0) to handle that yourself.

Scheduling

req := vibemail.SendRequest{To: "ada@example.com", Subject: "Reminder", Text: "Standup soon."}
req.ScheduleAt(time.Now().Add(2 * time.Hour))   // or req.ScheduledAt = "in 2 hours"

res, _ := client.Send(ctx, req)
client.Cancel(ctx, res.ID)                      // while it is still scheduled

Up to 30 days ahead. Once the dispatcher has claimed a message it is on its way out and can no longer be withdrawn.

Templates

templates, err := client.Templates(ctx)

client.Send(ctx, vibemail.SendRequest{
	To:        "ada@example.com",
	Template:  "welcome",
	Variables: map[string]string{"name": "Ada"},
})

Anything you set explicitly - Subject, HTML, Text, overrides the stored template.

Tracking flags

TrackOpens and TrackClicks are *bool so that an explicit false is distinguishable from "unset". Use the Bool helper:

req.TrackOpens = vibemail.Bool(false)

A plain bool would make "off" and "not specified" the same value, and the off would be silently dropped.

Domains

Mail sent from your own domain needs that domain added and verified. Adding one returns the DNS records to publish; it stays unverified until they resolve.

domain, err := client.CreateDomain(ctx, "yourdomain.com")
if err != nil {
	return err
}

for purpose, r := range domain.DNSRecords {
	log.Printf("%s %s %s %s", purpose, r.Type, r.Host, r.Value)
}

dns_records is keyed by purpose, not a list: verification, mx, spf and dmarc. Each carries type, host, value, and priority where the type takes one. Publish all four; the domain verifies once they resolve.

Come back for the same records at any time, along with whether verification has gone through:

d, err := client.Domain(ctx, strconv.FormatInt(domain.ID, 10))
log.Println("verified:", d.IsVerified)

Listing is paged. Total counts every domain on the account, not just the page you asked for:

page, err := client.Domains(ctx, vibemail.ListOptions{Limit: 25})
log.Printf("%d of %d", len(page.Data), page.Total)

_, err = client.DeleteDomain(ctx, "7")

Removing a domain does not affect mail already sent from it.

Contacts

_, err := client.CreateContact(ctx, "ada@example.com", "Ada Lovelace", "met at a talk")

page, err := client.Contacts(ctx, vibemail.ListOptions{Search: "ada", Limit: 50})

_, err = client.DeleteContact(ctx, strconv.FormatInt(page.Data[0].ID, 10))

Search matches against address and name. An empty name or notes is left off the wire entirely rather than sent as "".

Suppressions

Addresses that will not be sent to: hard bounces, spam complaints, and anything blocked by hand. A send to a suppressed address is dropped rather than attempted, which is what keeps a bad list from taking your sending reputation with it.

page, err := client.Suppressions(ctx, vibemail.ListOptions{Limit: 100})

for _, entry := range page.Data {
	log.Println(entry.Email, entry.Reason)   // "manual", or "hard bounce: ..."
}

reason is free text, not a fixed set: manual for anything added by hand or through the API, and hard bounce: ... carrying the remote server's own wording when the queue gave up on an address.

Paging

Domains, Contacts and Suppressions all return *Page[T]:

type Page[T any] struct {
	Object string
	Total  int64
	Limit  int64
	Offset int64
	Data   []T
}

Limit defaults to 50 and is capped at 100. The zero ListOptions{} sends no query at all and takes the server defaults. Walk the whole set by stepping Offset until you have Total:

var all []vibemail.Contact
for offset := 0; ; offset += 100 {
	page, err := client.Contacts(ctx, vibemail.ListOptions{Limit: 100, Offset: offset})
	if err != nil {
		return err
	}
	all = append(all, page.Data...)
	if int64(len(all)) >= page.Total {
		break
	}
}

Errors

res, err := client.Send(ctx, req)

var apiErr *vibemail.Error
if errors.As(err, &apiErr) {
	log.Println(apiErr.StatusCode, apiErr.Detail)
	if apiErr.Retryable() {
		// 429 or 5xx, already retried up to WithMaxRetries
	}
}
Status Means
400 The request was malformed, such as a domain that is not a domain.
401 The API key is missing, wrong, or revoked.
402 A plan limit was reached. Detail says which. Not retried.
404 No such record, or not yours.
409 Already exists, such as a domain someone has registered.
422 Required fields missing, or a schedule more than 30 days out.
429 Rate limited. Retried automatically, honouring Retry-After.
5xx Our fault. Retried automatically.

Every call takes a context.Context. Cancelling it stops the request and any retry still to come.

Options

client := vibemail.New("vm_live_...",
	vibemail.WithBaseURL("https://vibemail.ai"),
	vibemail.WithMaxRetries(2),
	vibemail.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
)
Option Default
WithBaseURL https://vibemail.ai Point at a self-hosted deployment.
WithMaxRetries 2 Applies to 429 and 5xx only.
WithHTTPClient http.Client with a 30s timeout Supply your own for proxying or tracing.

API

Send(ctx, req, opts...) Send one message.
Batch(ctx, reqs, opts...) Send many, one recipient each.
Get(ctx, id) Status of a send.
Cancel(ctx, id) Withdraw a scheduled send.
Templates(ctx) Stored templates.
Domains(ctx, opts) A page of sending domains.
Domain(ctx, id) One domain, with the records that verify it.
CreateDomain(ctx, domain) Add a domain.
DeleteDomain(ctx, id) Remove a domain.
Contacts(ctx, opts) A page of contacts.
CreateContact(ctx, email, name, notes) Store a contact.
DeleteContact(ctx, id) Forget a contact.
Suppressions(ctx, opts) Addresses that will not be sent to.

WithIdempotencyKey(key) is a SendOption for Send and Batch: a retry carrying the same key returns the original result instead of sending a second copy.

SendRequest fields
Field
To Recipient. One per message; use Batch for many.
From Sender. Must be an address your account owns. Defaults to the account address.
Subject
Text Plain-text body. Always send one alongside HTML.
HTML HTML body.
Template Name or id of a stored template.
Variables Values substituted into the template.
Tags Labels carried through to analytics.
TrackOpens *bool. Use vibemail.Bool(false) to turn it off; nil takes the default.
TrackClicks *bool, same.
ScheduledAt ISO 8601 or a relative offset like "in 2 hours". ScheduleAt(t) sets it from a time.Time.

Not in the SDK yet

Webhooks and analytics are served by the API but are not wrapped here. They will be added when the shapes settle; until then reach them with net/http and the same bearer token.

License

MIT

Documentation

Overview

Package vibemail is the official Go SDK for the VibeMail transactional email API.

The surface here is limited to endpoints the API actually serves. An Analytics method existed in an earlier draft and returned 404 against every deployment; it is absent rather than broken, and will return when the endpoint does.

Index

Constants

View Source
const Version = "1.2.0"

Version of this SDK, reported in the User-Agent.

Variables

View Source
var ErrNoRecipient = errors.New("vibemail: To must contain a recipient address")

ErrNoRecipient is returned when To is empty.

View Source
var ErrTooManyRecipients = errors.New(
	"vibemail: To takes a single address, use Batch to send to several recipients, " +
		"since one message addressed to many would disclose the list to all of them")

ErrTooManyRecipients is returned when Send is given more than one address. The API delivers to a single recipient and derives per-domain rate limiting from it; silently taking the first would drop the rest with no error.

Functions

func Bool

func Bool(b bool) *bool

Bool returns a pointer, for setting TrackOpens and TrackClicks.

Types

type BatchResponse

type BatchResponse struct {
	Data []BatchResult `json:"data"`
}

BatchResponse holds one entry per message, in the order submitted.

type BatchResult

type BatchResult struct {
	ID     string `json:"id,omitempty"`
	Status string `json:"status"`
	Error  string `json:"error,omitempty"`
}

BatchResult is either an accepted message or the reason it was rejected.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to the VibeMail API. It is safe for concurrent use.

func New

func New(apiKey string, opts ...Option) *Client

New returns a Client for the given API key.

func (*Client) Batch

func (c *Client) Batch(ctx context.Context, emails []SendRequest, opts ...SendOption) (*BatchResponse, error)

Batch sends several messages in one request, one recipient each. Entries succeed or fail individually.

func (*Client) Cancel

func (c *Client) Cancel(ctx context.Context, id string) (*SendResponse, error)

Cancel withdraws a scheduled send, while it is still scheduled. Once the dispatcher has claimed it the message is on its way out.

func (*Client) Contacts added in v1.2.0

func (c *Client) Contacts(ctx context.Context, opts ListOptions) (*Page[Contact], error)

Contacts lists contacts. Set ListOptions.Search to match address or name.

func (*Client) CreateContact added in v1.2.0

func (c *Client) CreateContact(ctx context.Context, email, name, notes string) (*Contact, error)

CreateContact stores a contact. Empty name and notes are left off the wire.

func (*Client) CreateDomain added in v1.2.0

func (c *Client) CreateDomain(ctx context.Context, domain string) (*Domain, error)

CreateDomain adds a domain. The reply carries the records to publish; the domain stays unverified until they resolve.

func (*Client) DeleteContact added in v1.2.0

func (c *Client) DeleteContact(ctx context.Context, id string) (*Deleted, error)

DeleteContact forgets a contact.

func (*Client) DeleteDomain added in v1.2.0

func (c *Client) DeleteDomain(ctx context.Context, id string) (*Deleted, error)

DeleteDomain removes a domain. Mail already sent from it is unaffected.

func (*Client) Domain added in v1.2.0

func (c *Client) Domain(ctx context.Context, id string) (*Domain, error)

Domain reads one domain, including the DNS records that verify it.

func (*Client) Domains added in v1.2.0

func (c *Client) Domains(ctx context.Context, opts ListOptions) (*Page[Domain], error)

Domains lists sending domains.

func (*Client) Get

func (c *Client) Get(ctx context.Context, id string) (*SendResponse, error)

Get looks up a send by the id returned from Send.

func (*Client) Send

func (c *Client) Send(ctx context.Context, req SendRequest, opts ...SendOption) (*SendResponse, error)

Send delivers one message.

func (*Client) Suppressions added in v1.2.0

func (c *Client) Suppressions(ctx context.Context, opts ListOptions) (*Page[Suppression], error)

Suppressions lists addresses that will not be sent to: hard bounces, complaints and manual blocks. A send to one of these is dropped rather than attempted.

func (*Client) Templates

func (c *Client) Templates(ctx context.Context) ([]Template, error)

Templates lists the templates stored on the account.

type Contact added in v1.2.0

type Contact struct {
	ID        int64  `json:"id"`
	Email     string `json:"email"`
	Name      string `json:"name,omitempty"`
	Notes     string `json:"notes,omitempty"`
	CreatedAt string `json:"created_at,omitempty"`
}

Contact is a stored contact.

type DNSRecord added in v1.2.0

type DNSRecord struct {
	Type string `json:"type"`
	// Host is the record name to create, fully qualified.
	Host     string `json:"host"`
	Value    string `json:"value"`
	Priority int    `json:"priority,omitempty"`
}

DNSRecord is a record to publish so a domain can be verified.

type Deleted added in v1.2.0

type Deleted struct {
	Deleted bool   `json:"deleted"`
	Domain  string `json:"domain,omitempty"`
	Email   string `json:"email,omitempty"`
}

Deleted is the outcome of a delete.

type Domain added in v1.2.0

type Domain struct {
	ID                int64  `json:"id"`
	Domain            string `json:"domain"`
	IsVerified        bool   `json:"is_verified"`
	VerificationToken string `json:"verification_token,omitempty"`
	CreatedAt         string `json:"created_at,omitempty"`
	// DNSRecords is returned by Domain and CreateDomain, keyed by purpose:
	// "verification", "mx", "spf", "dmarc".
	DNSRecords map[string]DNSRecord `json:"dns_records,omitempty"`
}

Domain is a sending domain on the account.

type Error

type Error struct {
	StatusCode int
	// Detail is the API's "error" field when the body was JSON.
	Detail string
	// Body is the raw response, for anything Detail does not capture.
	Body string
	// RetryAfter is the server's requested wait on a 429, if it sent one.
	RetryAfter time.Duration
}

Error is a non-2xx response from the API.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Retryable

func (e *Error) Retryable() bool

Retryable reports whether repeating the request could plausibly succeed.

type ListOptions added in v1.2.0

type ListOptions struct {
	// Limit defaults to 50 and is capped at 100.
	Limit int
	// Offset skips records.
	Offset int
	// Search matches address or name. Only honoured by Contacts.
	Search string
}

ListOptions is paging for the list endpoints. The zero value asks for the server defaults.

type Option

type Option func(*Client)

Option configures a Client.

func WithBaseURL

func WithBaseURL(u string) Option

WithBaseURL points the client at a different deployment.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies your own *http.Client, for a custom transport, proxy, or a stub in tests. Its Timeout governs each attempt.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets how many times a 429 or 5xx is retried. Zero disables retrying. Requests the server rejected on their merits are never retried.

type Page added in v1.2.0

type Page[T any] struct {
	Object string `json:"object"`
	Total  int64  `json:"total"`
	Limit  int64  `json:"limit"`
	Offset int64  `json:"offset"`
	Data   []T    `json:"data"`
}

Page is one page of results. Total counts everything matching, not just this page.

type SendOption

type SendOption func(*sendOptions)

SendOption configures a single Send or Batch call.

func WithIdempotencyKey

func WithIdempotencyKey(key string) SendOption

WithIdempotencyKey makes a retry safe: a repeat carrying the same key returns the original result instead of sending a second copy. A client that times out cannot otherwise tell whether the message went out.

type SendRequest

type SendRequest struct {
	// To is the recipient. Exactly one, see Batch for several.
	To string `json:"to"`
	// From must be an address your account owns. Defaults to the account address.
	From string `json:"from,omitempty"`
	// Subject is required unless a Template supplies one.
	Subject string `json:"subject,omitempty"`
	Text    string `json:"text,omitempty"`
	HTML    string `json:"html,omitempty"`
	// Template is the name or id of a stored template. Explicit fields win over it.
	Template  string            `json:"template,omitempty"`
	Variables map[string]string `json:"variables,omitempty"`
	Tags      []string          `json:"tags,omitempty"`
	// TrackOpens and TrackClicks apply to HTML sends. Both default to on;
	// pointers distinguish "leave the default" from an explicit false.
	TrackOpens  *bool `json:"track_opens,omitempty"`
	TrackClicks *bool `json:"track_clicks,omitempty"`
	// ScheduledAt takes an RFC 3339 timestamp or a relative offset such as
	// "in 2 hours", up to 30 days ahead. Use ScheduleAt to set it from a time.Time.
	ScheduledAt string `json:"scheduled_at,omitempty"`
}

SendRequest is one outbound message.

func (*SendRequest) ScheduleAt

func (r *SendRequest) ScheduleAt(t time.Time)

ScheduleAt sets ScheduledAt from a time.Time, in UTC.

type SendResponse

type SendResponse struct {
	ID          string `json:"id"`
	Status      string `json:"status"`
	ScheduledAt string `json:"scheduled_at,omitempty"`
}

SendResponse is the API's acknowledgement. It means the message was accepted, not that it was delivered.

type Suppression added in v1.2.0

type Suppression struct {
	Email     string `json:"email"`
	Reason    string `json:"reason,omitempty"`
	CreatedAt string `json:"created_at,omitempty"`
}

Suppression is an address that will not be sent to.

type Template

type Template struct {
	ID       int64  `json:"id"`
	Name     string `json:"name"`
	Subject  string `json:"subject"`
	HTMLBody string `json:"html_body,omitempty"`
	TextBody string `json:"text_body,omitempty"`
}

Template stored on the account.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL