Official Go SDK for the Sendly SMS API.
go get github.com/SendlyHQ/sendly-go/v3package main
import (
"context"
"fmt"
"log"
"github.com/SendlyHQ/sendly-go/v3/sendly"
)
func main() {
// Create a client
client := sendly.NewClient("sk_live_v1_your_api_key")
ctx := context.Background()
// Send an SMS
message, err := client.Messages.Send(ctx, &sendly.SendMessageRequest{
To: "+15551234567",
Text: "Hello from Sendly!",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Message sent: %s\n", message.ID)
}Before sending live SMS messages, you need:
-
Business Verification - Complete verification in the Sendly dashboard
- International: Instant approval (just provide Sender ID)
- US/Canada: Requires carrier approval (3-7 business days)
-
Credits - Add credits to your account
- Test keys (
sk_test_*) work without credits (sandbox mode) - Live keys (
sk_live_*) require credits for each message
- Test keys (
-
Live API Key - Generate after verification + credits
- Dashboard → API Keys → Create Live Key
| Key Type | Prefix | Credits Required | Verification Required | Use Case |
|---|---|---|---|---|
| Test | sk_test_v1_* |
No | No | Development, testing |
| Live | sk_live_v1_* |
Yes | Yes | Production messaging |
Note: You can start development immediately with a test key. Messages to sandbox test numbers are free and don't require verification.
import (
"time"
"github.com/SendlyHQ/sendly-go/v3/sendly"
)
// Create client with options
client := sendly.NewClient("sk_live_v1_xxx",
sendly.WithBaseURL("https://sendly.live/api/v1"),
sendly.WithTimeout(60*time.Second),
sendly.WithMaxRetries(5),
sendly.WithDebug(true),
)// Marketing message (default)
message, err := client.Messages.Send(ctx, &sendly.SendMessageRequest{
To: "+15551234567",
Text: "Check out our new features!",
})
if err != nil {
log.Fatal(err)
}
// Transactional message (bypasses quiet hours)
message, err := client.Messages.Send(ctx, &sendly.SendMessageRequest{
To: "+15551234567",
Text: "Your verification code is: 123456",
MessageType: "transactional",
})
// With custom metadata (max 4KB)
message, err := client.Messages.Send(ctx, &sendly.SendMessageRequest{
To: "+15551234567",
Text: "Your order #12345 has shipped!",
Metadata: map[string]interface{}{
"order_id": "12345",
"customer_id": "cust_abc",
},
})
// Send from one of your owned numbers (or an alphanumeric sender ID).
// Omit From to use your default sender.
message, err := client.Messages.Send(ctx, &sendly.SendMessageRequest{
To: "+15551234567",
Text: "Hello from our team!",
From: "+447111111111",
})
fmt.Printf("ID: %s\n", message.ID)
fmt.Printf("Status: %s\n", message.Status)
fmt.Printf("Credits: %d\n", message.CreditsUsed)resp, err := client.Messages.List(ctx, &sendly.ListMessagesRequest{
Limit: 50,
Offset: 0,
Status: sendly.MessageStatusDelivered,
To: "+15551234567",
})
if err != nil {
log.Fatal(err)
}
for _, msg := range resp.Data {
fmt.Printf("%s: %s (%s)\n", msg.ID, msg.To, msg.Status)
}message, err := client.Messages.Get(ctx, "msg_abc123")
if err != nil {
log.Fatal(err)
}
fmt.Printf("To: %s\n", message.To)
fmt.Printf("Text: %s\n", message.Text)
fmt.Printf("Status: %s\n", message.Status)// Schedule a message for future delivery
scheduled, err := client.Messages.Schedule(ctx, &sendly.ScheduleMessageRequest{
To: "+15551234567",
Text: "Your appointment is tomorrow!",
ScheduledAt: "2025-01-15T10:00:00Z",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Scheduled: %s\n", scheduled.ID)
fmt.Printf("Will send at: %s\n", scheduled.ScheduledAt)
// List scheduled messages
resp, err := client.Messages.ListScheduled(ctx, nil)
for _, msg := range resp.Data {
fmt.Printf("%s: %s\n", msg.ID, msg.ScheduledAt)
}
// Get a specific scheduled message
msg, err := client.Messages.GetScheduled(ctx, "sched_xxx")
// Cancel a scheduled message (refunds credits)
result, err := client.Messages.CancelScheduled(ctx, "sched_xxx")
fmt.Printf("Refunded: %d credits\n", result.CreditsRefunded)// Send multiple messages in one API call (up to 1000)
batch, err := client.Messages.SendBatch(ctx, &sendly.SendBatchRequest{
Messages: []sendly.BatchMessageItem{
{To: "+15551234567", Text: "Hello User 1!"},
{To: "+15559876543", Text: "Hello User 2!"},
{To: "+15551112222", Text: "Hello User 3!"},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Batch ID: %s\n", batch.BatchID)
fmt.Printf("Queued: %d\n", batch.Queued)
fmt.Printf("Failed: %d\n", batch.Failed)
fmt.Printf("Credits used: %d\n", batch.CreditsUsed)
// Get batch status
status, err := client.Messages.GetBatch(ctx, "batch_xxx")
// List all batches
batches, err := client.Messages.ListBatches(ctx, nil)
// Preview batch (dry run) - validates without sending
preview, err := client.Messages.PreviewBatch(ctx, &sendly.SendBatchRequest{
Messages: []sendly.BatchMessageItem{
{To: "+15551234567", Text: "Hello User 1!"},
{To: "+447700900123", Text: "Hello UK!"},
},
})
fmt.Printf("Total credits needed: %d\n", preview.TotalCredits)
fmt.Printf("Valid: %d, Invalid: %d\n", preview.Valid, preview.Invalid)Send one MMS to 2-8 US/Canada recipients who all share a thread. Group
messaging is an A2P 10DLC capability — the sending number must be an
MMS-enabled, 10DLC-registered number you own. Omit From to use your
default sender.
group, err := client.Messages.SendGroup(ctx, &sendly.SendGroupMessageRequest{
To: []string{"+15551234567", "+15559876543"},
Text: "Dinner at 7 tonight?",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Group message: %s (%s)\n", group.ID, group.Status)
if group.GroupMessageID != "" {
fmt.Printf("Thread: %s\n", group.GroupMessageID)
}Rewrite a draft message for clarity, compliance, and send-readiness. Provide
Text, MessageType, or both.
enhanced, err := client.Messages.Enhance(ctx, &sendly.EnhanceMessageRequest{
Text: "hey wanna buy our stuff its on sale",
MessageType: "marketing",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(enhanced.Enhanced) // the rewritten message
fmt.Println(enhanced.Explanation) // a short note on what changedConnect a number you own to WhatsApp, create Meta-reviewed message templates,
and send with client.Messages.SendWhatsApp. Connecting is a one-time $19
setup (no monthly fee) and always ends with a human step: the connect URL must
be opened in a browser and completed with a Facebook login. Free-form text and
media only deliver inside an open 24-hour customer-service window — outside
it, send an approved template.
// 1. Connect a number ($19 one-time). A human must open the connect URL.
signup, err := client.WhatsApp.Signup.Create(ctx, "+15559876543")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Have your user open: %s\n", signup.ConnectURL)
// 2. Poll until active
status, err := client.WhatsApp.Signup.Get(ctx, signup.ID)
fmt.Println(status.Status) // "initiated" -> "registering" -> "active"
// 3. List your WhatsApp senders
senders, err := client.WhatsApp.Senders.List(ctx)
for _, s := range senders.Senders {
fmt.Printf("%s: %s\n", s.PhoneNumber, s.Status)
}
// 4. Create a template (Meta reviews it, usually 24-48h)
template, err := client.WhatsApp.Templates.Create(ctx, &sendly.CreateWhatsAppTemplateRequest{
Sender: "+15559876543",
Name: "order_shipped",
Language: "en_US",
Category: "UTILITY",
Body: "Hi {{1}}, your order {{2}} has shipped!",
Examples: map[string]string{"1": "Sam", "2": "#4821"},
})
fmt.Println(template.Status) // "PENDING"
// 5. Check the 24-hour window, then send
window, err := client.WhatsApp.Window(ctx, "+15559876543", "+15551234567")
if window.Open {
// Free-form text (or media with a caption via MediaUrls + Text)
msg, err := client.Messages.SendWhatsApp(ctx, &sendly.SendWhatsAppMessageRequest{
To: "+15551234567",
From: "+15559876543",
Text: "Your table is ready!",
})
fmt.Println(msg.ID)
} else {
// Approved template — works regardless of the window
msg, err := client.Messages.SendWhatsApp(ctx, &sendly.SendWhatsAppMessageRequest{
To: "+15551234567",
From: "+15559876543",
Template: &sendly.WhatsAppTemplateSendParams{
Name: "order_shipped",
Language: "en_US",
Variables: map[string]string{"1": "Sam", "2": "#4821"},
},
})
fmt.Println(msg.WhatsApp.Kind) // "template"
}Every connected sender has a WhatsApp Business profile — the name, photo, and
business details recipients see when they tap your number. Read it and edit it
in place (send only the fields you want to change; About is capped at 139
characters and Description at 512):
profile, err := client.WhatsApp.Senders.GetProfile(ctx, "+15559876543")
if err != nil {
log.Fatal(err)
}
if profile.DisplayName != nil {
fmt.Println(*profile.DisplayName)
}
updated, err := client.WhatsApp.Senders.UpdateProfile(ctx, "+15559876543", &sendly.UpdateWhatsAppSenderProfileRequest{
About: "Fresh bread, daily.",
Description: "Family bakery in Austin since 1998.",
Email: "hello@example.com",
Website: "https://example.com",
})RCS is the branded, rich upgrade to SMS: your verified agent name and logo instead of a bare number, plus tappable suggestion chips and rich cards, on Android and iOS 18+ handsets. Messages send through an RCS agent registered for your brand — contact support to set one up. RCS requires a live API key.
Text sends fall back to plain SMS automatically when the recipient's device or
network doesn't support RCS, so one call covers your whole list. The fallback
is billed as SMS and is visible on the response: check FellBackTo (or
Channel).
// 1. Find your agents. Sendable means it can send right now.
agents, err := client.RCS.Agents.List(ctx)
if err != nil {
log.Fatal(err)
}
for _, a := range agents.Agents {
fmt.Printf("%s: %s (sendable: %t)\n", a.ID, a.Name, a.Sendable)
}
// 2. Optional pre-flight — sending handles the fallback on its own.
capability, err := client.RCS.Capability(ctx, "+15551234567", "")
fmt.Println(capability.Capable, capability.Features)
// 3. Text with tappable chips
msg, err := client.Messages.SendRcs(ctx, &sendly.SendRcsMessageRequest{
To: "+15551234567",
Text: "Your order #4821 has shipped!",
Suggestions: []sendly.RcsSuggestion{
{Reply: &sendly.RcsSuggestedReply{Text: "Track it", PostbackData: "track_4821"}},
{Action: &sendly.RcsSuggestedAction{
Text: "View receipt",
PostbackData: "receipt_4821",
URL: "https://example.com/receipts/4821",
}},
},
})
if err != nil {
log.Fatal(err)
}
if msg.FellBackTo == "sms" {
// Delivered as SMS — suggestions have no SMS form and were dropped.
fmt.Println("fell back to SMS:", msg.RCS.SuggestionsDropped)
} else {
fmt.Println("delivered over RCS from", msg.RCS.AgentName)
}
// 4. A rich card. Cards have no SMS form, so they never fall back —
// a recipient without RCS gets a 422 (rcs_not_supported_for_recipient).
card, err := client.Messages.SendRcs(ctx, &sendly.SendRcsMessageRequest{
To: "+15551234567",
Card: &sendly.RcsCard{
Title: "Your table is ready",
Description: "Head to the host stand — we'll hold it for 10 minutes.",
MediaURL: "https://example.com/table.jpg",
Orientation: "vertical",
Suggestions: []sendly.RcsSuggestion{
{Reply: &sendly.RcsSuggestedReply{Text: "On my way", PostbackData: "otw"}},
},
},
})
fmt.Println(card.RCS.Kind) // "card"
// Turn the fallback off to require RCS delivery (422 when unsupported).
rcsOnly := false
_, err = client.Messages.SendRcs(ctx, &sendly.SendRcsMessageRequest{
To: "+15551234567",
Text: "RCS only.",
FallbackToSms: &rcsOnly,
})// Create a webhook endpoint
webhook, err := client.Webhooks.Create(ctx, &sendly.CreateWebhookRequest{
URL: "https://example.com/webhooks/sendly",
Events: []string{"message.delivered", "message.failed"},
})
fmt.Printf("Webhook ID: %s\n", webhook.ID)
fmt.Printf("Secret: %s\n", webhook.Secret) // Store securely!
// List all webhooks
webhooks, err := client.Webhooks.List(ctx)
// Get a specific webhook
wh, err := client.Webhooks.Get(ctx, "whk_xxx")
// Update a webhook
client.Webhooks.Update(ctx, "whk_xxx", &sendly.UpdateWebhookRequest{
URL: "https://new-endpoint.example.com/webhook",
Events: []string{"message.delivered", "message.failed", "message.sent"},
})
// Test a webhook
result, err := client.Webhooks.Test(ctx, "whk_xxx")
// Rotate webhook secret
rotation, err := client.Webhooks.RotateSecret(ctx, "whk_xxx")
// Delete a webhook
err = client.Webhooks.Delete(ctx, "whk_xxx")// List the numbers attached to your workspace
owned, err := client.Numbers.List(ctx)
for _, n := range owned.Numbers {
fmt.Printf("%s: %s (%s)\n", n.ID, n.PhoneNumber, n.Status)
}
// Get a single number (includes whether it is your default sender)
number, err := client.Numbers.Get(ctx, "num_xxx")
if number.IsDefault != nil && *number.IsDefault {
fmt.Println("This is the default sender")
}
// Make a number your default sender (must be active)
isDefault := true
updated, err := client.Numbers.Update(ctx, "num_xxx", &sendly.UpdateNumberRequest{
IsDefault: &isDefault,
})
fmt.Printf("Default: %v\n", updated.IsDefault)
// Cancel a scheduled release ("keep this number")
keep := false
_, err = client.Numbers.Update(ctx, "num_xxx", &sendly.UpdateNumberRequest{
PendingCancellation: &keep,
})
// Release a number. A live paid purchase is cancelled at the end of the paid
// period, in which case the response is scheduled rather than immediate.
result, err := client.Numbers.Release(ctx, "num_xxx")
if result.Scheduled {
fmt.Printf("Releases at %s\n", *result.ScheduledReleaseAt)
} else {
fmt.Println("Released")
}// Get account information
account, err := client.Account.Get(ctx)
fmt.Printf("Email: %s\n", account.Email)
// Check credit balance
credits, err := client.Account.GetCredits(ctx)
fmt.Printf("Available: %d credits\n", credits.AvailableBalance)
fmt.Printf("Reserved: %d credits\n", credits.ReservedBalance)
fmt.Printf("Total: %d credits\n", credits.Balance)
// View credit transaction history
transactions, err := client.Account.GetCreditTransactions(ctx, nil)
for _, tx := range transactions {
fmt.Printf("%s: %d credits - %s\n", tx.Type, tx.Amount, tx.Description)
}
// List API keys
keys, err := client.Account.ListAPIKeys(ctx)
for _, key := range keys {
fmt.Printf("%s: %s*** (%s)\n", key.Name, key.Prefix, key.Type)
}
// Create a new API key
newKey, err := client.Account.CreateAPIKey(ctx, "Production Key")
fmt.Printf("New key: %s\n", newKey.Key) // Only shown once!
// Revoke an API key
err = client.Account.RevokeAPIKey(ctx, "key_xxx")
// Rotate an API key — issues a new secret and keeps the old one valid for a
// grace period (24-168 hours, default 24) so running code keeps working.
rotated, err := client.Account.RotateAPIKey(ctx, "key_xxx", &sendly.RotateAPIKeyRequest{
GracePeriodHours: 48,
})
fmt.Printf("New key: %s\n", rotated.NewKey.Key) // Only shown once!
fmt.Println(rotated.Message) // e.g. when the old key expiresMint branded short links for a destination URL, list them with click
analytics, and flip a per-link kill switch. Requires the url_shortener
feature on your account.
// Create a short link (destination must be an http:// or https:// URL)
link, err := client.Links.Create(ctx, "https://example.com/spring-sale")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s -> %s\n", link.ShortURL, link.DestinationURL)
// List your links with click counts
list, err := client.Links.List(ctx, &sendly.ListShortLinksRequest{Limit: 50})
for _, l := range list.Links {
fmt.Printf("%s: %d clicks\n", l.Code, l.ClickCount)
}
// Disable a link (its redirect returns 404 until re-enabled)
_, err = client.Links.Disable(ctx, link.Code)
// Re-enable it
_, err = client.Links.Enable(ctx, link.Code)message, err := client.Messages.Send(ctx, &sendly.SendMessageRequest{
To: "+15551234567",
Text: "Hello!",
})
if err != nil {
switch {
case sendly.IsAuthenticationError(err):
log.Fatal("Invalid API key")
case sendly.IsRateLimitError(err):
rateLimitErr := err.(*sendly.RateLimitError)
log.Printf("Rate limited, retry after %d seconds", rateLimitErr.RetryAfter)
case sendly.IsInsufficientCreditsError(err):
log.Fatal("Add more credits to your account")
case sendly.IsValidationError(err):
log.Printf("Invalid request: %v", err)
case sendly.IsNotFoundError(err):
log.Fatal("Resource not found")
case sendly.IsNetworkError(err):
log.Printf("Network error: %v", err)
default:
log.Printf("Error: %v", err)
}
return
}| Status | Description |
|---|---|
queued |
Message is queued for delivery |
sending |
Message is being sent |
sent |
Message was sent to carrier |
delivered |
Message was delivered |
failed |
Message delivery failed |
| Tier | Countries | Credits per SMS |
|---|---|---|
| Domestic | US, CA | 2 |
| Tier 1 | GB, PL, IN, etc. | 8 |
| Tier 2 | FR, JP, AU, etc. | 12 |
| Tier 3 | DE, IT, MX, etc. | 16 |
Use test API keys (sk_test_v1_xxx) with these test numbers:
| Number | Behavior |
|---|---|
| +15005550000 | Success (instant) |
| +15005550001 | Fails: invalid_number |
| +15005550002 | Fails: unroutable_destination |
| +15005550003 | Fails: queue_full |
| +15005550004 | Fails: rate_limit_exceeded |
| +15005550006 | Fails: carrier_violation |
The Enterprise API lets you programmatically manage workspaces, verification, credits, and API keys for multi-tenant platforms. Requires an enterprise master key (sk_live_v1_master_*).
Create a fully configured workspace in a single call:
client := sendly.NewClient("sk_live_v1_master_YOUR_KEY")
generateOptIn := true
result, err := client.Enterprise.Provision(ctx, &sendly.ProvisionWorkspaceRequest{
Name: "Acme Insurance - Austin",
SourceWorkspaceID: "ws_verified",
CreditAmount: 5000,
CreditSourceWorkspaceID: "SOURCE_WORKSPACE_ID",
KeyName: "Production",
KeyType: "live",
GenerateOptInPage: &generateOptIn,
})
fmt.Println(result.Workspace.ID)
fmt.Println(result.Key.Key)Three provisioning modes:
| Mode | Params | Description |
|---|---|---|
| Inherit | SourceWorkspaceID |
Shares toll-free number from verified workspace |
| Inherit + New Number | SourceWorkspaceID + InheritWithNewNumber: true |
Copies business info, purchases new number |
| Fresh | Verification: sendly.VerificationData{...} |
Full business details, new number + carrier approval |
ws, _ := client.Enterprise.Workspaces.Create(ctx, "Acme Insurance", "")
list, _ := client.Enterprise.Workspaces.List(ctx)
detail, _ := client.Enterprise.Workspaces.Get(ctx, "ws_xxx")
_ = client.Enterprise.Workspaces.Delete(ctx, "ws_xxx")result, _ := client.Enterprise.Workspaces.TransferCredits(ctx, "ws_dest", "ws_source", 5000)
key, _ := client.Enterprise.Workspaces.CreateKey(ctx, "ws_xxx", "Production", "live")
fmt.Println(key.Key)
_ = client.Enterprise.Workspaces.RevokeKey(ctx, "ws_xxx", "key_abc")webhook, _ := client.Enterprise.Webhooks.Set(ctx, "https://yourapp.com/webhooks")
overview, _ := client.Enterprise.Analytics.Overview(ctx)
messages, _ := client.Enterprise.Analytics.Messages(ctx, &sendly.AnalyticsMessagesOptions{Period: "30d"})
delivery, _ := client.Enterprise.Analytics.Delivery(ctx)Full enterprise docs: sendly.live/docs/enterprise
- Go 1.21+
MIT