A unified SMS sending library for Go with support for multiple providers including Twilio, AWS SNS, Vonage, and MSG91.
Each provider is a separate Go module, so you only download the dependencies you actually need.
Install only what you need:
# Core (required)
go get github.com/KARTIKrocks/gosms
# Providers (pick one or more)
go get github.com/KARTIKrocks/gosms/twilio
go get github.com/KARTIKrocks/gosms/sns
go get github.com/KARTIKrocks/gosms/vonage
go get github.com/KARTIKrocks/gosms/msg91import (
"github.com/KARTIKrocks/gosms"
"github.com/KARTIKrocks/gosms/twilio"
)
provider, err := twilio.NewProvider(twilio.Config{
AccountSID: "account_sid",
AuthToken: "auth_token",
From: "+15551234567",
})
if err != nil {
log.Fatal(err)
}
client := gosms.NewClient(provider)
result, err := client.Send(ctx, "+15559876543", "Hello from gosms!")
if err != nil {
log.Fatal(err)
}
log.Printf("Message sent: %s, Status: %s", result.MessageID, result.Status)import (
"github.com/KARTIKrocks/gosms"
"github.com/KARTIKrocks/gosms/sns"
)
config := sns.DefaultConfig()
config.Region = "us-east-1"
config.AccessKeyID = "access_key"
config.SecretAccessKey = "secret_key"
config.SenderID = "MyApp"
config.SMSType = sns.SMSTransactional
provider, err := sns.NewProvider(ctx, config)
if err != nil {
log.Fatal(err)
}
client := gosms.NewClient(provider)
result, err := client.Send(ctx, "+15559876543", "Your code is 123456")import (
"github.com/KARTIKrocks/gosms"
"github.com/KARTIKrocks/gosms/vonage"
)
provider, err := vonage.NewProvider(vonage.Config{
APIKey: "api_key",
APISecret: "api_secret",
From: "MyApp",
})
if err != nil {
log.Fatal(err)
}
client := gosms.NewClient(provider)
result, err := client.Send(ctx, "+15559876543", "Hello from Vonage!")MSG91 is the standard SMS gateway for India. It uses DLT-approved Flow templates;
variables are passed via msg91.SetVar, not the Body field.
import (
"github.com/KARTIKrocks/gosms"
"github.com/KARTIKrocks/gosms/msg91"
)
provider, err := msg91.NewProvider(msg91.Config{
AuthKey: "your_authkey",
SenderID: "SENDER", // 6-char DLT sender ID
TemplateID: "tmpl_xxx", // DLT-approved Flow template
Route: msg91.RouteTransactional,
})
if err != nil {
log.Fatal(err)
}
client := gosms.NewClient(provider)
msg := gosms.NewMessage("+919876543210", "")
msg91.SetVar(msg, "name", "Kartik")
msg91.SetVar(msg, "otp", "1234")
result, err := client.SendMessage(ctx, msg)Template variables and Body fallback. MSG91 Flow templates reference
placeholders like ##name## or ##otp##. Set each one with msg91.SetVar.
For templates with a single ##body## placeholder, any non-empty Message.Body
is automatically passed as body when no vars are set — so the unified
client.Send(ctx, to, text) path works without extra wiring.
Per-message overrides. Use msg91.SetTemplateID(msg, "tmpl_other") to
override Config.TemplateID for a single message, or msg.WithFrom("OTHER")
to override the sender ID.
Phone normalization. Non-E.164 numbers up to 10 digits are prefixed with
Config.Country (default 91). +919876543210, 919876543210, and
9876543210 all normalize identically.
Bulk. SendBulk groups recipients by effective (template_id, sender)
and sends each group as one Flow API call. Groups larger than
Config.MaxRecipientsPerCall (default 1000) are automatically chunked
across multiple calls; set a negative value to disable chunking.
OTP capability. MSG91 implements the optional gosms.OTPProvider for
the full send / verify / resend flow. Callers holding a gosms.Provider
can detect it with a type assertion:
if otp, ok := provider.(gosms.OTPProvider); ok {
// MSG91 generates the code server-side when OTPRequest.OTP is empty.
_, err := otp.SendOTP(ctx, &gosms.OTPRequest{
Phone: "+919876543210",
Length: 6,
Expiry: 5 * time.Minute,
})
vr, err := otp.VerifyOTP(ctx, "+919876543210", "123456")
if err == nil && vr.Verified {
// OTP matched
}
// Resend via "text" or "voice".
_ = otp.ResendOTP(ctx, "+919876543210", "voice")
}Delivery status is webhook-driven; parse incoming callbacks with msg91.ParseWebhook(r).
- Unified
Providerinterface across all SMS backends - Multi-module architecture — no unnecessary dependencies
- Message builder with fluent API
- Bulk messaging with
BatchandSendToMany - Delivery status tracking and webhook parsing
- Multi-provider with fallback and round-robin strategies
- Phone number validation (E.164) and normalization
- SMS segment calculation with proper GSM 03.38 charset support
- Pre-built message templates (OTP, alerts, notifications)
- Mock provider for testing
msg := gosms.NewMessage("+15559876543", "Hello!").
WithFrom("+15551234567").
WithReference("order-123").
WithValidity(1 * time.Hour).
WithMetadata("user_id", "12345")
result, err := client.SendMessage(ctx, msg)msg := gosms.NewMessage("+15559876543", "Reminder: Your appointment is tomorrow").
WithSchedule(time.Now().Add(24 * time.Hour))
result, err := client.SendMessage(ctx, msg)batch := gosms.NewBatch()
batch.AddNew("+15551111111", "Message 1")
batch.AddNew("+15552222222", "Message 2")
batch.AddNew("+15553333333", "Message 3")
results, err := batch.Send(ctx, client)
for _, result := range results {
if result.Success() {
log.Printf("Sent to %s: %s", result.To, result.MessageID)
} else {
log.Printf("Failed to %s: %s", result.To, result.Error)
}
}results, err := gosms.SendToMany(ctx, client,
"Flash sale! 50% off today only!",
"+15551111111",
"+15552222222",
"+15553333333",
)status, err := client.GetStatus(ctx, "message_id")
if err != nil {
log.Fatal(err)
}
if status.Status.IsFinal() {
if status.Status.IsSuccess() {
log.Printf("Message delivered at %v", status.UpdatedAt)
} else {
log.Printf("Delivery failed: %s", status.ErrorMessage)
}
}import "github.com/KARTIKrocks/gosms/twilio"
http.HandleFunc("/webhook/twilio", func(w http.ResponseWriter, r *http.Request) {
status, err := twilio.ParseWebhook(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("Message %s status: %s", status.MessageID, status.Status)
w.WriteHeader(http.StatusOK)
})import "github.com/KARTIKrocks/gosms/vonage"
http.HandleFunc("/webhook/vonage", func(w http.ResponseWriter, r *http.Request) {
status, err := vonage.ParseWebhook(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("Message %s status: %s", status.MessageID, status.Status)
w.WriteHeader(http.StatusOK)
})Try Twilio first, fall back to Vonage on failure:
multi := gosms.NewMultiProvider(twilioProvider, vonageProvider)
client := gosms.NewClient(multi)
result, err := client.Send(ctx, to, body)Rotate across providers:
multi := gosms.NewMultiProvider(twilioProvider, vonageProvider).
WithStrategy(gosms.StrategyRoundRobin)
client := gosms.NewClient(multi)// Validate E.164 format
if gosms.ValidateE164("+15551234567") {
log.Println("Valid E.164 number")
}
// Normalize phone number
normalized := gosms.NormalizePhone("555-123-4567", "+1")
// Returns: +15551234567
// Check if message uses GSM 7-bit encoding
if gosms.IsGSMEncoding("Hello world") {
log.Println("GSM encoding (160 char limit)")
}
// Calculate SMS segments
segments := gosms.CalculateSegments("Hello, this is a test message!")
log.Printf("Message will use %d segment(s)", segments)// OTP: "123456 is your MyApp verification code."
msg := gosms.OTPMessage("+15551234567", "123456", "MyApp")
// Alert: "[URGENT] Server is down!"
msg := gosms.AlertMessage("+15551234567", "URGENT", "Server is down!")
// Notification: "Order Update: Your order has shipped"
msg := gosms.NotificationMessage("+15551234567", "Order Update", "Your order has shipped")mock := gosms.NewMockProvider()
client := gosms.NewClient(mock)
// Send message
result, err := client.Send(ctx, "+15551234567", "Test message")
// Verify
if mock.MessageCount() != 1 {
t.Error("Expected 1 message")
}
lastMsg := mock.LastMessage()
if lastMsg.Message.Body != "Test message" {
t.Error("Message body mismatch")
}
// Simulate failures
mock.WithFailAll(true)
result, err = client.Send(ctx, "+15551234567", "This will fail")
// result.Status == gosms.StatusFailed
// Simulate errors
mock.WithSendError(gosms.ErrRateLimited)
_, err = client.Send(ctx, "+15551234567", "This will error")
// errors.Is(err, gosms.ErrRateLimited) == true
// Reset mock
mock.Reset()result, err := client.Send(ctx, to, body)
if err != nil {
switch {
case errors.Is(err, gosms.ErrInvalidPhone):
log.Println("Invalid phone number")
case errors.Is(err, gosms.ErrInvalidMessage):
log.Println("Invalid message content")
case errors.Is(err, gosms.ErrRateLimited):
log.Println("Rate limited, try again later")
case errors.Is(err, gosms.ErrInsufficientFunds):
log.Println("Account balance too low")
case errors.Is(err, gosms.ErrBlacklisted):
log.Println("Number is blacklisted")
case errors.Is(err, gosms.ErrProviderError):
log.Println("Provider error:", err)
default:
log.Println("Unknown error:", err)
}
}| Status | Description |
|---|---|
StatusPending |
Message is pending |
StatusQueued |
Message is queued for delivery |
StatusAccepted |
Message accepted by provider |
StatusSent |
Message sent to carrier |
StatusDelivered |
Message delivered to recipient |
StatusFailed |
Delivery failed |
StatusRejected |
Message was rejected |
StatusExpired |
Message expired before delivery |
StatusUnknown |
Status unknown |
import "github.com/KARTIKrocks/gosms/sns"
provider, _ := sns.NewProvider(ctx, config)
// Set account-level SMS attributes
err := provider.SetSMSAttributes(ctx,
"100.00", // Monthly spend limit
"arn:aws:iam::123:role/SNSRole", // IAM role for delivery logs
"100", // Success sampling rate %
)
// Check opt-out status
optedOut, err := provider.CheckIfPhoneNumberIsOptedOut(ctx, "+15551234567")
// List opted-out numbers
numbers, err := provider.ListPhoneNumbersOptedOut(ctx)
// Opt-in a number
err = provider.OptInPhoneNumber(ctx, "+15551234567")See the examples/ directory for runnable examples:
| Example | Description |
|---|---|
| basic | Core API usage with mock provider |
| twilio-provider | Sending via Twilio |
| sns-provider | Sending via AWS SNS |
| vonage-provider | Sending via Vonage |
| msg91-provider | Sending via MSG91 (Flow templates + OTP) |
| multi-provider | Fallback and round-robin strategies |
| webhooks | Delivery status webhook server |
| mock-testing | Using MockProvider in tests |
| helpers | Phone validation, normalization, segment calculation |
# Run an example (no credentials needed)
cd examples/basic && go run .gosms/
├── sms.go # Core types: Provider, Message, Result, Status, Client
├── helpers.go # Utilities: validation, segments, batch, multi-provider
├── mock.go # MockProvider for testing
├── doc.go # Package documentation
├── twilio/ # Twilio provider (separate module)
│ ├── go.mod
│ └── twilio.go
├── sns/ # AWS SNS provider (separate module)
│ ├── go.mod
│ └── sns.go
├── vonage/ # Vonage provider (separate module)
│ ├── go.mod
│ └── vonage.go
├── msg91/ # MSG91 provider (separate module)
│ ├── go.mod
│ └── msg91.go
└── examples/ # Runnable examples
├── basic/
├── twilio-provider/
├── sns-provider/
├── vonage-provider/
├── msg91-provider/
├── multi-provider/
├── webhooks/
├── mock-testing/
└── helpers/
- All providers are safe for concurrent use
Clientis safe for concurrent use after initializationMockProvideris safe for concurrent use with internal lockingMultiProviderround-robin counter is atomic