Go SDK
The official Go SDK for Sent provides a lightweight, high-performance client with minimal dependencies. Built for microservices, CLI tools, and high-throughput applications with full context support.
Requirements
This library requires Go 1.22 or later.
Installation
go get github.com/sentdm/sent-dm-goTo pin a specific version, append a release tag from GitHub Releases:
go get github.com/sentdm/sent-dm-go@<version>Quick Start
Initialize the client
import (
"github.com/sentdm/sent-dm-go"
)
// Reads the SENT_DM_API_KEY environment variable
client := sentdm.NewClient()Send your first message
package main
import (
"context"
"fmt"
"log"
"github.com/sentdm/sent-dm-go"
)
func main() {
// Reads the SENT_DM_API_KEY environment variable
client := sentdm.NewClient()
ctx := context.Background()
response, err := client.Messages.Send(ctx, sentdm.MessageSendParams{
To: []string{"+1234567890"},
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
Name: sentdm.String("welcome"),
Parameters: map[string]string{
"name": "John Doe",
"order_id": "12345",
},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Message sent: %s\n", response.Data.Recipients[0].MessageID)
fmt.Printf("Status: %s\n", response.Data.Status)
}Authentication
The client can be configured using environment variables or explicitly using functional options:
import (
"github.com/sentdm/sent-dm-go"
"github.com/sentdm/sent-dm-go/option"
)
// Using environment variables (SENT_DM_API_KEY)
client := sentdm.NewClient()
// Or explicit configuration
client := sentdm.NewClient(
option.WithAPIKey("your_api_key"),
)Request fields
The sentdm library follows the omitzero serialization semantics that the Go 1.24 encoding/json release introduced for request fields. This is a serialization convention implemented by the library itself; the minimum supported Go version remains 1.22.
Required primitive fields feature the tag json:"...,required". These fields are always serialized, even their zero values.
Optional primitive types are wrapped in a param.Opt[T]. These fields can be set with the provided constructors, sentdm.String(), sentdm.Int(), etc.
params := sentdm.MessageSendParams{
To: []string{"+1234567890"}, // required property
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("template-id"),
Name: sentdm.String("welcome"),
},
Channel: []string{"rcs", "sms"}, // optional — "rcs", "sms", "whatsapp", or combinations
}To send null instead of a param.Opt[T], use param.Null[T](). To check if a field is omitted, use param.IsOmitted().
Send Messages
Send a message
response, err := client.Messages.Send(ctx, sentdm.MessageSendParams{
To: []string{"+1234567890"},
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
Name: sentdm.String("welcome"),
Parameters: map[string]string{
"name": "John Doe",
"order_id": "12345",
},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Sent: %s\n", response.Data.Recipients[0].MessageID)
fmt.Printf("Status: %s\n", response.Data.Status)Sandbox mode
Use Sandbox to validate requests without sending real messages:
response, err := client.Messages.Send(ctx, sentdm.MessageSendParams{
To: []string{"+1234567890"},
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
Name: sentdm.String("welcome"),
},
Sandbox: sentdm.Bool(true), // Validates but doesn't send
})
if err != nil {
log.Fatal(err)
}
// Response will have test data
fmt.Printf("Validation passed: %s\n", response.Data.Recipients[0].MessageID)Check message status
Retrieve the current status of a sent message. The Direction field indicates whether the message is "OUTBOUND" (sent by you) or "INBOUND" (a reply or opt-out keyword received from an end user):
status, err := client.Messages.GetStatus(ctx, "msg-uuid", sentdm.MessageGetStatusParams{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status: %s\n", status.Data.Status) // e.g. "DELIVERED"
fmt.Printf("Channel: %s\n", status.Data.Channel) // e.g. "sms"
fmt.Printf("Direction: %s\n", status.Data.Direction) // "OUTBOUND" | "INBOUND"Message activities
Retrieve the full activity log for a message, useful for auditing delivery attempts across carriers:
activities, err := client.Messages.GetActivities(ctx, "msg-uuid", sentdm.MessageGetActivitiesParams{})
if err != nil {
log.Fatal(err)
}
for _, activity := range activities.Data.Activities {
fmt.Printf("%s: %s via %s\n", activity.Timestamp, activity.Status, activity.From)
fmt.Printf(" Price: %s | Active contact price: %s\n", activity.Price, activity.ActiveContactPrice)
}Numbers
Look up carrier and line-type information for any phone number before sending:
result, err := client.Numbers.Lookup(ctx, "+12025551234", sentdm.NumberLookupParams{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Valid: %v\n", result.Data.IsValid)
fmt.Printf("Carrier: %s\n", result.Data.CarrierName)
fmt.Printf("Line type: %s\n", result.Data.LineType) // "mobile", "landline", "voip"
fmt.Printf("VoIP: %v\n", result.Data.IsVoip)Error handling
When the API returns a non-success status code, the SDK returns an error of type *sentdm.Error:
response, err := client.Messages.Send(ctx, params)
if err != nil {
var apiErr *sentdm.Error
if errors.As(err, &apiErr) {
fmt.Printf("API error: %s\n", apiErr.Error())
fmt.Printf("Status: %d\n", apiErr.StatusCode)
} else {
fmt.Printf("Other error: %v\n", err)
}
}Pagination
Use .List() to fetch a page of results. Pass Page and PageSize to control pagination:
page, err := client.Contacts.List(ctx, sentdm.ContactListParams{
Page: 1,
PageSize: 100,
})
if err != nil {
log.Fatal(err)
}
for _, contact := range page.Data.Contacts {
fmt.Printf("%s\n", contact.PhoneNumber)
}Contacts
Create and manage contacts:
// Create a contact
response, err := client.Contacts.New(ctx, sentdm.ContactNewParams{
PhoneNumber: "+1234567890",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Contact ID: %s\n", response.Data.ID)
// List contacts
page, err := client.Contacts.List(ctx, sentdm.ContactListParams{
Page: 1,
PageSize: 100,
})
// Get a contact
contact, err := client.Contacts.Get(ctx, "contact-uuid", sentdm.ContactGetParams{})
// Update a contact
response, err = client.Contacts.Update(ctx, "contact-uuid", sentdm.ContactUpdateParams{
DefaultChannel: sentdm.String("whatsapp"),
})
// Delete a contact
err = client.Contacts.Delete(ctx, "contact-uuid", sentdm.ContactDeleteParams{})Templates
List and retrieve templates:
// List templates
templates, err := client.Templates.List(ctx, sentdm.TemplateListParams{})
if err != nil {
log.Fatal(err)
}
for _, template := range templates.Data.Templates {
fmt.Printf("%s (%s): %s\n", template.Name, template.Status, template.ID)
}
// Get a template
template, err := client.Templates.Get(ctx, "template-uuid", sentdm.TemplateGetParams{})
fmt.Printf("Name: %s\n", template.Data.Name)
fmt.Printf("Status: %s\n", template.Data.Status)RequestOptions
This library uses the functional options pattern. Functions defined in the option package return a RequestOption, which is a closure that mutates a RequestConfig. These options can be supplied to the client or at individual requests:
client := sentdm.NewClient(
// Adds a header to every request made by the client
option.WithHeader("X-Some-Header", "custom_header_info"),
)
// Override per-request
response, err := client.Messages.Send(ctx, params,
option.WithHeader("X-Some-Header", "some_other_value"),
option.WithJSONSet("custom.field", map[string]string{"my": "object"}),
)The request option option.WithDebugLog(nil) may be helpful while debugging.
See the full list of request options.
Framework Integration
Dedicated guides cover client setup, message sending, verified webhook handling, and sandbox testing for each framework:
Gin
Configure the client, send template messages from a Gin handler, verify webhooks, and test with sandbox mode.
Echo
Configure the client, send template messages from an Echo handler, verify webhooks, and test with sandbox mode.
Response objects
All fields in response structs are ordinary value types. Response structs also include a special JSON field containing metadata about each property.
response, err := client.Templates.Get(ctx, "template-uuid", sentdm.TemplateGetParams{})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Data.Name) // Access the field directly
// Check if field was present in response
if response.Data.JSON.Name.Valid() {
fmt.Println("Name was present")
}
// Access raw JSON
fmt.Println(response.Data.JSON.Name.Raw())Concurrent Sending
Use Go's concurrency for high-throughput:
func sendBulkMessages(
client *sentdm.Client,
phoneNumbers []string,
templateID string,
) error {
var wg sync.WaitGroup
errChan := make(chan error, len(phoneNumbers))
// Semaphore to limit concurrency
sem := make(chan struct{}, 10)
for _, phone := range phoneNumbers {
wg.Add(1)
go func(p string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err := client.Messages.Send(ctx, sentdm.MessageSendParams{
To: []string{p},
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String(templateID),
},
})
if err != nil {
errChan <- fmt.Errorf("failed to send to %s: %w", p, err)
}
}(phone)
}
wg.Wait()
close(errChan)
var errs []error
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return fmt.Errorf("failed to send %d messages", len(errs))
}
return nil
}Webhooks
Recommended pattern: Webhooks are the primary way to track message delivery, so don't poll the API. Save the message ID when you send, then update your database as webhook events arrive.
Sent delivers signed POST requests to your endpoint for every status change. Two event types exist:
message: Message status changes (QUEUED,ROUTED,SCHEDULED,SENT,DELIVERED,READ,FAILED,FILTERED,BLOCKED,RECEIVED); each fires as a sub-type (for example,message.delivered,message.filtered). Usemessage.receivedto receive inbound messages from contacts.templates: WhatsApp template approval/rejection
The signing secret (from the Sent Dashboard) has a whsec_ prefix. Strip it and base64-decode the remainder to obtain the raw HMAC key. The signed content is {X-Webhook-ID}.{X-Webhook-Timestamp}.{rawBody} and the signature format is v1,{base64(hmac)}.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"math"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
webhookID := r.Header.Get("X-Webhook-ID")
timestamp := r.Header.Get("X-Webhook-Timestamp")
signature := r.Header.Get("X-Webhook-Signature")
// 1. Verify: signed content = "{webhookId}.{timestamp}.{rawBody}"
raw := os.Getenv("SENT_DM_WEBHOOK_SECRET") // "whsec_abc123..."
keyStr := strings.TrimPrefix(raw, "whsec_")
keyBytes, _ := base64.StdEncoding.DecodeString(keyStr)
signed := webhookID + "." + timestamp + "." + string(body)
mac := hmac.New(sha256.New, keyBytes)
mac.Write([]byte(signed))
expected := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(signature), []byte(expected)) {
http.Error(w, `{"error":"invalid signature"}`, http.StatusUnauthorized)
return
}
// 2. Optional: reject replayed events older than 5 minutes
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
http.Error(w, `{"error":"timestamp too old"}`, http.StatusUnauthorized)
return
}
var event struct {
Field string `json:"field"`
Event string `json:"event"`
Payload map[string]any `json:"payload"`
}
json.Unmarshal(body, &event)
// 3. Handle events — update message status in your own database
if event.Field == "message" {
if event.Event == "message.received" {
// Inbound message from a contact
inboundNumber := event.Payload["inbound_number"]
text := event.Payload["text"]
channel := event.Payload["channel"]
// db.CreateInboundMessage(ctx, inboundNumber.(string), text.(string), channel.(string))
_ = inboundNumber; _ = text; _ = channel
} else {
// Outbound message status update
messageID := event.Payload["message_id"]
status := event.Payload["message_status"]
// db.UpdateMessageStatus(ctx, messageID.(string), status.(string))
_ = messageID; _ = status
}
}
// 4. Always return 200 quickly
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"received":true}`))
}See the Webhooks reference for the full payload schema and all status values.
Source & Issues
- Releases: GitHub Releases
- GitHub:
sentdm/sent-dm-go - GoDoc:
pkg.go.dev/github.com/sentdm/sent-dm-go - Issues: Report a bug
Getting Help
- Documentation: API Reference
- Troubleshooting: Common Issues
- Support: email support@sent.dm with your request ID
Sending Sent messages from Celery background tasks
Send Sent messages from Celery tasks: install the Python SDK, configure a worker-safe client, add a retrying send task, and verify the task with sandbox mode.
Sending messages from a Gin service with the Sent Go SDK
Wire the Sent Go SDK into a Gin service: install, configure the client, send a template message from a handler, verify webhooks, and test with sandbox mode.