Go SDK for the Loops API.
go get github.com/loops-so/loops-gopackage main
import (
"log"
loops "github.com/loops-so/loops-go"
)
func main() {
client := loops.NewClient("YOUR_API_KEY")
err := client.SendEvent(loops.SendEventRequest{
Email: "user@example.com",
EventName: "signup",
EventProperties: map[string]any{
"plan": "pro",
},
})
if err != nil {
log.Fatal(err)
}
}client := loops.NewClient("YOUR_API_KEY",
loops.WithBaseURL("https://app.loops.so/api/v1"),
loops.WithHTTPClient(myHTTPClient),
loops.WithUserAgent("my-app/1.0"),
loops.WithLogger(os.Stderr), // verbose request/response logging
)- API key —
GetAPIKey - Contacts —
CreateContact,UpdateContact,DeleteContact,FindContacts,CheckContactSuppression,RemoveContactSuppression - Contact properties —
ListContactProperties,CreateContactProperty - Mailing lists —
ListMailingLists - Audience segments —
GetAudienceSegment,ListAudienceSegments,CreateAudienceSegment - Events —
SendEvent - Event patterns —
ListEventPatterns,GetEventPatternByName,GetEventPattern - Transactional —
SendTransactional,ListTransactionals,CreateTransactional,GetTransactional,UpdateTransactional,EnsureTransactionalDraft,PublishTransactional - Transactional groups —
CreateTransactionalGroup,GetTransactionalGroup,UpdateTransactionalGroup,ListTransactionalGroups - Email messages —
GetEmailMessage,UpdateEmailMessage,PreviewEmailMessage,GetEmailMessageGuardian - Campaigns —
CreateCampaign,UpdateCampaign,GetCampaign,ListCampaigns - Campaign groups —
CreateCampaignGroup,GetCampaignGroup,UpdateCampaignGroup,ListCampaignGroups - Components —
GetComponent,ListComponents,CreateComponent,UpdateComponent - Themes —
GetTheme,ListThemes,CreateTheme,UpdateTheme - Uploads —
Upload,CreateUpload,CompleteUpload - Workflows —
ListWorkflows,GetWorkflow,GetWorkflowNode,CreateWorkflow,UpdateWorkflow,ChangeWorkflowMailingList,CreateWorkflowNode,UpdateWorkflowNode,AddWorkflowBranch,RerouteNodeConnection,DeleteWorkflowNode,DeleteWorkflowNodeRecursive
Full reference: pkg.go.dev/github.com/loops-so/loops-go.
API errors are returned as *loops.APIError with StatusCode and Message:
if err := client.SendEvent(req); err != nil {
var apiErr *loops.APIError
if errors.As(err, &apiErr) {
log.Printf("loops api error %d: %s", apiErr.StatusCode, apiErr.Message)
}
return err
}Requests are automatically retried with exponential backoff and jitter on 429 and 5xx responses (up to 2 retries).
SendEvent and SendTransactional accept an IdempotencyKey field, which is sent as the Idempotency-Key header.
ListTransactionals and ListCampaigns return a single page of results along with a *Pagination value. Pass a PaginationParams to control page size and cursor:
items, page, err := client.ListTransactionals(loops.PaginationParams{PerPage: "50"})
if err != nil {
log.Fatal(err)
}
// page.NextCursor is "" when there are no more pages.To fetch every page, use the generic Paginate helper:
all, err := loops.Paginate(func(cursor string) ([]loops.Transactional, *loops.Pagination, error) {
return client.ListTransactionals(loops.PaginationParams{Cursor: cursor})
})Upload is a one-call helper that requests a presigned URL, uploads the bytes, and finalizes the asset. Supported content types are image/jpeg, image/png, image/gif and image/webp, up to 4 MB.
f, err := os.Open("hero.png")
if err != nil {
log.Fatal(err)
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
log.Fatal(err)
}
asset, err := client.Upload(loops.UploadRequest{
EmailMessageID: "em_abc123",
ContentType: "image/png",
ContentLength: stat.Size(),
Body: f,
})
if err != nil {
log.Fatal(err)
}
// asset.FinalURL is the public URL to reference in your email LMX.For finer control, call CreateUpload and CompleteUpload directly and do the PUT to the presigned URL yourself.
Client.Do is a low-level entry point for endpoints that do not yet have a dedicated method. It shares the client's auth, User-Agent, retries and backoff. The body is JSON-encoded when non-nil; pass json.RawMessage to forward a pre-encoded payload. The raw *http.Response is returned so the caller decides how to handle non-2xx — close the body when done.
resp, err := client.Do(ctx, http.MethodPost, "/contacts/create", map[string]any{
"email": "user@example.com",
})
if err != nil {
return err
}
defer resp.Body.Close()Loops delivers signed events to your configured endpoint. VerifyWebhook checks the webhook-id, webhook-timestamp, and webhook-signature headers against your dashboard signing secret (the whsec_… value), and ParseWebhook decodes the raw body into a typed event.
Read the raw request body before any JSON decoding — the signature is computed over the exact bytes.
func handler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if err := loops.VerifyWebhook(os.Getenv("LOOPS_SIGNING_SECRET"), r.Header, body); err != nil {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
event, err := loops.ParseWebhook(body)
if err != nil {
http.Error(w, "bad payload", http.StatusBadRequest)
return
}
switch e := event.(type) {
case *loops.WebhookContactCreatedPayload:
log.Printf("contact created: %s", e.Contact.Email)
case *loops.WebhookEmailDeliveredPayload:
log.Printf("delivered to %s (%s)", e.ContactIdentity.Email, e.SourceType)
default:
log.Printf("unhandled event: %s", event.Type())
}
w.WriteHeader(http.StatusOK)
}Pass loops.WithWebhookTimestampTolerance(5*time.Minute) to VerifyWebhook to also reject stale deliveries (replay protection); it is off by default.
MIT