loopsso-go

module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: LGPL-2.1

README

loopsso-go

A small, dependency-free Go client for the Loops.so transactional email API.

It is a drop-in replacement for brevo-go: the package name, builder, and method names match, so migrating means changing the import path and the template ID literal.

Install

go get github.com/Trillion-Digital/loopsso-go

Requires Go 1.24.5 or newer.

Usage

package main

import (
	"context"
	"log"
	"os"

	"github.com/Trillion-Digital/loopsso-go/mail"
)

func main() {
	// nil http client -> 30s timeout. Empty apiURL -> mail.DefaultAPIURL.
	client := mail.NewLoopsMailClient(nil, os.Getenv("LOOPS_API_KEY"), "")

	email := mail.NewEmail().
		To("user@example.com", "").
		TemplateID("clfq6dinn000yl70fgwwyp82l").
		Param("name", "Alice").
		Param("plan", "pro")

	if err := client.SendEmail(context.Background(), email); err != nil {
		log.Fatalf("send failed: %v", err)
	}
}

TemplateID is the transactional email ID from your Loops dashboard, and Param / Params populate the template's data variables.

Migrating from brevo-go

One call site change is required, plus three methods that become no-ops.

Required: TemplateID now takes a string. Loops identifies templates with cuids (clfq6dinn000yl70fgwwyp82l), which an int cannot express, so TemplateID(101) becomes TemplateID("clfq6dinn000yl70fgwwyp82l"). The compiler flags every occurrence.

Accepted but ignored. These compile unchanged so existing code keeps working, but the values never reach Loops:

Call Why it is ignored
Sender(email, name) Loops takes the sender from the transactional template. There is no wire field.
Subject(subject) Loops takes the subject from the template and offers no per-send override.
the name argument of To(email, name) Loops accepts a bare recipient address.

Behaviour changes:

  • To replaces the recipient instead of appending. Loops accepts one recipient per request.
  • A sender is no longer required by Build(), since it is unused.
  • MessageBuilder, NewMessage, and AddMessages are gone. Loops has no batch endpoint; send one request per recipient and mind the rate limit below.
  • Errors are now typed. See below.

Adding the contact

AddToAudience sets Loops' addToAudience flag, which creates the contact if it does not already exist:

email := mail.NewEmail().
	To("user@example.com", "").
	TemplateID("clfq6dinn000yl70fgwwyp82l").
	AddToAudience()

Idempotency

Loops discards a duplicate send carrying an already-seen Idempotency-Key within 24 hours and answers 409 instead:

key := mail.NewIdempotencyKey() // random UUIDv4

email := mail.NewEmail().
	To("user@example.com", "").
	TemplateID("clfq6dinn000yl70fgwwyp82l").
	IdempotencyKey(key)

Keys are capped at 100 characters; Build rejects longer ones.

The key is never generated automatically. Deduplication only works when retries of one logical send reuse the same key, and only your code knows which attempts those are — generate the key when the send is first intended, persist it alongside the work, and reuse it on every retry. A fresh key per attempt gives you no protection at all.

Error handling

Failures come back as *mail.APIError, which carries the status code and Loops' message and error.path fields. Four sentinels cover the common cases and match with errors.Is:

err := client.SendEmail(ctx, email)

switch {
case err == nil:
	// sent
case errors.Is(err, mail.ErrRateLimited):
	// 429 - back off and retry
case errors.Is(err, mail.ErrIdempotencyConflict):
	// 409 - already sent within the last 24h, treat as success
case errors.Is(err, mail.ErrNotFound):
	// 404 - no such transactional template
case errors.Is(err, mail.ErrUnauthorized):
	// 401 - bad API key
default:
	var apiErr *mail.APIError
	if errors.As(err, &apiErr) {
		log.Printf("loops %d: %s (path %q)", apiErr.StatusCode, apiErr.Message, apiErr.Path)
	}
}

A 200 response carrying {"success": false} is also reported as an *APIError, so a nil error always means the send was accepted.

Rate limits

Loops allows 10 requests per second per team. This client does not throttle or retry; exceeding the limit surfaces as mail.ErrRateLimited and back-off is left to the caller.

Not implemented

  • Attachments. Loops supports them, but only after you email Loops to enable the feature on your account.
  • Automatic 429 retry.
  • Other endpoints. Contacts, events, and campaigns are out of scope; this package covers POST /api/v1/transactional only.

Development

go vet ./...
go test ./... -race -cover
gofmt -l .

Tests are hermetic — they use httptest and make no network calls.

License

LGPL-2.1. See LICENSE.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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