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 Sep 11, 2026
Last updated: September 2026. Package versions verified against the Go module proxy on 2026-09-11.
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.31.1.
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: the Developer plan includes 10,000 messages a month, and past that it's $0.005 per message, the same rate on every channel. 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.31.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/shared")func main() {// With no options, the client reads COURIER_API_KEY from the environment.// Pass option.WithAPIKey("...") from the v4/option package to set it explicitly.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.
If you are writing this in Cursor, Claude Code, or Codex, connect the agent to Courier directly instead of pasting snippets. It then works from the current SDK shapes rather than the 2022-era API that most training data still contains, which is the difference between Go that compiles and Go that does not.
The MCP server gives the agent typed tool calls. In Claude Code:
claude mcp add --transport http courier https://mcp.courier.com --header api_key:YOUR_API_KEY
Courier Skills adds the practices that are easy to get wrong, such as never batching a one-time passcode:
npx skills add trycourier/courier-skills
Both sit alongside the Courier CLI for scripting and CI. courier.com/agents collects the setup for Cursor, Claude Code, Codex, and VS Code in one place, and Build with AI has the machine-readable docs index an agent can fetch on its own.
Then ask in plain language: "send an SMS when an order ships, and fall back to email if it fails." The agent picks the right primitive, and knows which channels need a recorded opt-in.
This matters for the same reason this guide version-stamps every snippet. An agent working from stale training data will confidently write a send call that no longer exists.
| Twilio | Plivo | Notification API | |
|---|---|---|---|
| Module | github.com/twilio/twilio-go v1.31.1 | github.com/plivo/plivo-go/v7 v7.60.3 | github.com/trycourier/courier-go/v4 v4.31.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
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.github.com/twilio/twilio-go at v1.31.1 for Twilio, or github.com/plivo/plivo-go/v7 at v7.60.3 for Plivo. Both are actively released. Avoid github.com/textmagic/textmagic-rest-go, which older tutorials recommend: it was last updated in 2015, has no tagged versions, and does not work cleanly with Go modules.courier.CreateClient(token, nil) and client.SendMessage(ctx, courier.SendMessageRequestBody{...}) belonged to courier-go v2. The module is now on v4, so the import path is github.com/trycourier/courier-go/v4, the constructor is courier.NewClient(option.WithAPIKey(...)), and the call is client.Send.Message(ctx, courier.SendMessageParams{...}) with typed structs instead of nested maps.os.Getenv rather than assigning string literals. The Plivo client goes further: plivo.NewClient("", "", &plivo.ClientOptions{}) with empty strings reads PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN from the environment, and the Courier client defaults to COURIER_API_KEY. Credentials committed to a public repository get scraped within minutes.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 Sep 11, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.