3 ways to send SMS with Go

Send SMS from Go with Twilio, Plivo, or a notification API. Current module paths and code that actually compiles, plus how to choose between the three.

Updated Aug 7, 2026

Last updated: August 2026. All module versions verified against the Go module proxy on 2026-08-06.

Go has no built-in way to send a text message, so every option here calls a third-party API. The send itself is short. What differs is how much of the surrounding machinery you end up owning.

This guide covers three options with code that compiles against current module versions: Twilio, Plivo, and a notification API layered over either.

Which option should you pick?

  • Twilio for the shortest path to a working send and the largest body of existing answers when something breaks.
  • Plivo if per-message cost is what you are optimizing at volume.
  • A notification API in front of either one, once SMS is part of a product flow rather than a one-off. That is where templates, per-user opt-outs, retries, and multi-channel routing stop being small problems.

One Go-specific warning. Module major versions live in the import path, so a stale tutorial does not just give you an old API, it gives you an import path that resolves to different code. If a snippet fails with an undefined function, check the version suffix on the import before you debug anything else.

1. Send SMS with Twilio

Twilio publishes an official Go helper library, so you do not need to hand-roll HTTP requests against the REST API.

What you need first

  • A Twilio account, trial or paid.
  • Go 1.18 or newer, in a module (go mod init).
  • An SMS-enabled Twilio number. On a trial you can only send to numbers you have verified with Twilio, which is the usual reason a first send goes nowhere.
  • For US consumer traffic, a registered 10DLC campaign or a verified toll-free number.

Install

go get github.com/twilio/twilio-go

Verified against github.com/twilio/twilio-go v1.30.9.

Send the message

package main
import (
"fmt"
"log"
"os"
"github.com/twilio/twilio-go"
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
)
func main() {
client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: os.Getenv("TWILIO_ACCOUNT_SID"),
Password: os.Getenv("TWILIO_AUTH_TOKEN"),
})
params := &twilioApi.CreateMessageParams{}
params.SetTo(os.Getenv("CELL_PHONE_NUMBER"))
params.SetFrom(os.Getenv("TWILIO_PHONE_NUMBER"))
params.SetBody("Your order has shipped.")
resp, err := client.Api.CreateMessage(params)
if err != nil {
log.Fatalf("sending SMS: %v", err)
}
fmt.Println(*resp.Sid, *resp.Status)
}

Run it with go run main.go.

Note that the response fields are pointers, so dereference them or check for nil before printing. The Status you get back is queued or accepted, not delivered. To find out what actually happened, either fetch the message again by SID or register a status callback webhook.

Where it gets thin: one provider, message copy in your source, and delivery events arriving as raw webhooks you store and interpret yourself.

2. Send SMS with Plivo

Plivo covers the same ground at generally lower per-message pricing, which is why it turns up in high-volume sending. Its Go SDK is on v7 and actively released.

What you need first

  • A Plivo account, plus your Auth ID and Auth Token.
  • Go 1.18 or newer.
  • A Plivo number or registered sender ID. On a trial account you can only send to numbers you have verified with Plivo.

Install

go get github.com/plivo/plivo-go/v7

Verified against github.com/plivo/plivo-go/v7 v7.60.3. The /v7 suffix is part of the module path, not optional.

Send the message

package main
import (
"fmt"
"log"
"os"
"github.com/plivo/plivo-go/v7"
)
func main() {
// Empty credential strings make the client read PLIVO_AUTH_ID and
// PLIVO_AUTH_TOKEN from the environment.
client, err := plivo.NewClient("", "", &plivo.ClientOptions{})
if err != nil {
log.Fatalf("creating client: %v", err)
}
resp, err := client.Messages.Create(plivo.MessageCreateParams{
Src: os.Getenv("PLIVO_PHONE_NUMBER"),
Dst: os.Getenv("CELL_PHONE_NUMBER"),
Text: "Your order has shipped.",
})
if err != nil {
log.Fatalf("sending SMS: %v", err)
}
fmt.Println(resp.MessageUUID)
}

Passing empty strings for the credentials is the documented way to make Plivo read them from the environment, which looks odd but keeps them out of your source. Both Src and Dst need E.164 formatting.

Where it gets thin: the same layer as Twilio, so the same gaps.

3. Send SMS with a notification API

Both options above solve delivery. Here is what usually turns up in the fortnight afterward:

  • Message copy is a string literal in a compiled binary, so a wording change means a rebuild and redeploy.
  • Someone replies STOP and you now own an opt-out list and the obligation behind it.
  • The same event needs to reach one user by SMS and another by email.
  • Your provider has a bad hour and there is no second path.
  • Twenty events fire in a minute and one user gets twenty texts.
  • Someone asks how many notifications failed last week and you cannot answer.

A notification API such as Courier sits above your SMS provider and handles those. You keep Twilio or Plivo delivering, and your Go code stops knowing which one it is.

What the extra layer gives you:

  • Automatic failover between providers when one starts erroring.
  • Journeys for multi-step sequences, with digest and throttling nodes so a burst of events is not a burst of texts.
  • One send endpoint for SMS, email, push, in-app, Slack, and Microsoft Teams.
  • Per-user, per-channel preferences enforced before a send.
  • Templates edited outside your codebase, with localization per user. For a compiled language this matters more than usual: it takes copy changes off your release cycle.
  • One log covering every channel.

Set it up

Create a free account, then connect an SMS provider under Channels using the credentials that provider already gave you. This is the same Twilio or Plivo account from the sections above; you are not changing who delivers.

Install

go get github.com/trycourier/courier-go/v4

Verified against github.com/trycourier/courier-go/v4 v4.23.0, which requires Go 1.22 or newer. If you find a snippet importing courier-go/v2 or courier-go/v3, it will not compile against v4.

Send the message

package main
import (
"context"
"fmt"
"log"
"github.com/trycourier/courier-go/v4"
"github.com/trycourier/courier-go/v4/option"
"github.com/trycourier/courier-go/v4/shared"
)
func main() {
// Omitting option.WithAPIKey makes the client read COURIER_API_KEY
// from the environment.
client := courier.NewClient()
resp, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{
OfUserRecipient: &shared.UserRecipientParam{
UserID: courier.String("user-123"),
},
},
Template: courier.String("ORDER_SHIPPED"),
Data: map[string]any{
"name": "Erika",
"tracking_url": "https://example.com/t/abc",
},
},
})
if err != nil {
log.Fatalf("sending notification: %v", err)
}
fmt.Println(resp.RequestID)
}

Two things worth noticing. The recipient is a tagged union (SendMessageParamsMessageToUnion), because a recipient can be a user, a list, an audience, or a channel-specific address; you set the field matching the kind you want. And courier.String() is a helper for taking the address of a string literal, which Go does not let you do inline.

Nothing in that call names SMS. Routing and the user's stored preferences decide the channel, so moving a notification to push, or sending both, is configuration rather than a rebuild. If you would rather pass an explicit phone number than a stored user, set OfUserRecipient with a PhoneNumber field instead of UserID.

Where it gets thin: one more service in the path, and it is the wrong tool for two-way SMS conversations, which belong with your provider's inbound APIs.

Comparing the three options

TwilioPlivoNotification API
Modulegithub.com/twilio/twilio-go v1.30.9github.com/plivo/plivo-go/v7 v7.60.3github.com/trycourier/courier-go/v4 v4.23.0
Clienttwilio.NewRestClientWithParams()plivo.NewClient("", "", opts)courier.NewClient()
Send callclient.Api.CreateMessage(params)client.Messages.Create(params)client.Send.Message(ctx, params)
Reads credentials from envyou pass themyes, with empty stringsyes
Context-awarenonoyes, takes context.Context
Other channelsSMS, WhatsApp, voice, emailSMS, WhatsApp, voicewhatever you connect
Templates outside your binarynonoyes
Preferences and opt-outsyou build ityou build itbuilt in
Provider failovern/an/ayes, across providers
Best forfirst send, widest communityhigh volume, cost-sensitiveSMS inside a product flow

If you need one text sent this afternoon, go get github.com/twilio/twilio-go and stop reading. If SMS is becoming a feature, the case for the notification layer is stronger in Go than in most languages, because message copy compiled into a binary means every wording change is a deploy.

Next steps

FAQ

Frequently asked questions

Add a provider's SDK with `go get`, build a client from credentials in environment variables, and call its create-message method with a sender number, a recipient number in E.164 format, and a body. With Twilio that is `twilio.NewRestClientWithParams()` and `client.Api.CreateMessage(params)`. Go has no built-in SMS capability, so every option calls a provider's HTTP API.

One API, every channel

Ship notifications without the boilerplate

Courier gives you one API for email, SMS, push, and chat, with templates, routing, retries, and delivery logs built in.

Last updated Aug 7, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.