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.
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.
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
go mod init).Install
go get github.com/twilio/twilio-go
Verified against github.com/twilio/twilio-go v1.30.9.
Send the message
package mainimport ("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.
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
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 mainimport ("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.
Both options above solve delivery. Here is what usually turns up in the fortnight afterward:
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:
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 mainimport ("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.
| Twilio | Plivo | Notification API | |
|---|---|---|---|
| Module | github.com/twilio/twilio-go v1.30.9 | github.com/plivo/plivo-go/v7 v7.60.3 | github.com/trycourier/courier-go/v4 v4.23.0 |
| Client | twilio.NewRestClientWithParams() | plivo.NewClient("", "", opts) | courier.NewClient() |
| Send call | client.Api.CreateMessage(params) | client.Messages.Create(params) | client.Send.Message(ctx, params) |
| Reads credentials from env | you pass them | yes, with empty strings | yes |
| Context-aware | no | no | yes, takes context.Context |
| Other channels | SMS, WhatsApp, voice, email | SMS, WhatsApp, voice | whatever you connect |
| Templates outside your binary | no | no | yes |
| Preferences and opt-outs | you build it | you build it | built in |
| Provider failover | n/a | n/a | yes, across providers |
| Best for | first send, widest community | high volume, cost-sensitive | SMS 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.
FAQ
Keep exploring
One API, every channel
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.
© 2026 Courier. All rights reserved.