Go SDK

Go SDK

The official Go SDK for Sent LogoSent 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-go

To pin a specific version:

go get github.com/sentdm/sent-dm-go@v0.17.0

Quick Start

Initialize the client

package main

import (
    "context"
    "os"

    "github.com/sentdm/sent-dm-go"
    "github.com/sentdm/sent-dm-go/option"
)

func main() {
    client := sentdm.NewClient(
        option.WithAPIKey(os.Getenv("SENT_DM_API_KEY")), // defaults to os.LookupEnv("SENT_DM_API_KEY")
    )
    // Use the client...
}

Send your first message

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/sentdm/sent-dm-go"
    "github.com/sentdm/sent-dm-go/option"
)

func main() {
    client := sentdm.NewClient(
        option.WithAPIKey(os.Getenv("SENT_DM_API_KEY")),
    )

    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.Messages[0].ID)
    fmt.Printf("Status: %s\n", response.Data.Messages[0].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 uses the omitzero semantics from Go 1.24+ encoding/json release for request fields.

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: sentdm.StringSlice([]string{"whatsapp"}), // optional property
}

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.Messages[0].ID)
fmt.Printf("Status: %s\n", response.Data.Messages[0].Status)

Test mode

Use TestMode 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"),
    },
    TestMode: 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.Messages[0].ID)

Error handling

When the API returns a non-success status code, we return 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.Message)
        fmt.Printf("Status: %d\n", apiErr.StatusCode)
    } else {
        fmt.Printf("Other error: %v\n", err)
    }
}

Pagination

For paginated list endpoints, you can use .ListAutoPaging() methods to iterate through items across all pages:

// List all contacts
iter := client.Contacts.ListAutoPaging(ctx, sentdm.ContactListParams{})
for iter.Next() {
    contact := iter.Current()
    fmt.Printf("%s - %s\n", contact.PhoneNumber, contact.AvailableChannels)
}
if err := iter.Err(); err != nil {
    log.Fatal(err)
}

Or use simple .List() methods to fetch a single page:

page, err := client.Contacts.List(ctx, sentdm.ContactListParams{
    Limit: sentdm.Int(100),
})
if err != nil {
    log.Fatal(err)
}

for _, contact := range page.Data {
    fmt.Printf("%s\n", contact.PhoneNumber)
}

// Get next page
if page.HasMore {
    nextPage, err := page.GetNextPage()
    // ...
}

Contacts

Create and manage contacts:

// Create a contact
response, err := client.Contacts.Create(ctx, sentdm.ContactCreateParams{
    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{
    Limit: sentdm.Int(100),
})

// Get a contact
contact, err := client.Contacts.Get(ctx, "contact-uuid")

// Update a contact
response, err = client.Contacts.Update(ctx, "contact-uuid", sentdm.ContactUpdateParams{
    PhoneNumber: sentdm.String("+1987654321"),
})

// Delete a contact
err = client.Contacts.Delete(ctx, "contact-uuid")

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 {
    fmt.Printf("%s (%s): %s\n", template.Name, template.Status, template.ID)
}

// Get a template
template, err := client.Templates.Get(ctx, "template-uuid")
fmt.Printf("Name: %s\n", template.Name)
fmt.Printf("Status: %s\n", template.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.

Gin Framework

package main

import (
    "context"
    "net/http"
    "os"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/sentdm/sent-dm-go"
    "github.com/sentdm/sent-dm-go/option"
)

func main() {
    client := sentdm.NewClient(
        option.WithAPIKey(os.Getenv("SENT_DM_API_KEY")),
    )

    r := gin.Default()

    r.POST("/send", func(c *gin.Context) {
        var req struct {
            To       []string          `json:"to"`
            Template sentdm.MessageSendParamsTemplate `json:"template"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
        defer cancel()

        response, err := client.Messages.Send(ctx, sentdm.MessageSendParams{
            To:       req.To,
            Template: req.Template,
        })
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, gin.H{
            "status": "sent",
            "message_id": response.Data.Messages[0].ID,
        })
    })

    r.Run(":8080")
}

Echo Framework

package main

import (
    "context"
    "net/http"
    "os"
    "time"

    "github.com/labstack/echo/v4"
    "github.com/sentdm/sent-dm-go"
    "github.com/sentdm/sent-dm-go/option"
)

func main() {
    client := sentdm.NewClient(
        option.WithAPIKey(os.Getenv("SENT_DM_API_KEY")),
    )

    e := echo.New()

    e.POST("/send", func(c echo.Context) error {
        req := new(sentdm.MessageSendParams)
        if err := c.Bind(req); err != nil {
            return err
        }

        ctx, cancel := context.WithTimeout(c.Request().Context(), 30*time.Second)
        defer cancel()

        response, err := client.Messages.Send(ctx, *req)
        if err != nil {
            return c.JSON(http.StatusInternalServerError, map[string]string{
                "error": err.Error(),
            })
        }

        return c.JSON(http.StatusOK, map[string]string{
            "status":     "sent",
            "message_id": response.Data.Messages[0].ID,
        })
    })

    e.Logger.Fatal(e.Start(":8080"))
}

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")
if err != nil {
    log.Fatal(err)
}

fmt.Println(response.Name)  // Access the field directly

// Check if field was present in response
if response.JSON.Name.Valid() {
    fmt.Println("Name was present")
}

// Access raw JSON
fmt.Println(response.JSON.Name.Raw())

Concurrent Sending

Leverage 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()

            response, 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 — 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:

  • messages — Message status changes (SENT, DELIVERED, READ, FAILED, …)
  • 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_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"`
        Payload map[string]any `json:"payload"`
    }
    json.Unmarshal(body, &event)

    // 3. Handle events — update message status in your own database
    if event.Field == "messages" {
        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

Getting Help

On this page