Send SMS from Go: No Twilio, Just Your Android Phone
Send real SMS from Go using textbee and your Android phone as the gateway. Native net/http, struct marshaling, error handling, and bulk send included.
TL;DR
- textbee's REST API needs no SDK — Go's standard
net/httppackage handles everything. - No external dependencies required: just
encoding/json,net/http, andos. - Store credentials in environment variables, not source code.
- The
recipientsfield is a slice — one request, multiple numbers. - No per-message API fee: messages send through the SIM in your Android phone.
You don't need a Twilio account or a rented phone number to send SMS from Go. If you have an Android phone with a working SIM, textbee turns it into an SMS gateway your Go code can call over a plain REST API. The message goes out from your actual phone number.
Before you start
- textbee app installed on your Android device and linked to your account: download and quickstart
- Device ID and API key from the dashboard at textbee.dev
- Go 1.18+ (examples use generics-free code, works on any modern Go version)
- On Android 15 or 16? You may need to manually enable SMS permissions once
The code
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type smsRequest struct {
Recipients []string `json:"recipients"`
Message string `json:"message"`
}
type smsResponse struct {
Data struct {
Success bool `json:"success"`
Message string `json:"message"`
SmsBatchID string `json:"smsBatchId"`
RecipientCount int `json:"recipientCount"`
} `json:"data"`
}
func sendSMS(deviceID, apiKey, to, message string) (*smsResponse, error) {
url := fmt.Sprintf(
"https://api.textbee.dev/api/v1/gateway/devices/%s/send-sms",
deviceID,
)
payload := smsRequest{
Recipients: []string{to},
Message: message,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal payload: %w", err)
}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("SMS send failed (HTTP %d): %s", resp.StatusCode, respBody)
}
var result smsResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
return &result, nil
}
func main() {
deviceID := os.Getenv("TEXTBEE_DEVICE_ID")
apiKey := os.Getenv("TEXTBEE_API_KEY")
result, err := sendSMS(deviceID, apiKey, "+15551234567", "Hello from Go!")
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Sent! Batch ID: %s, Recipients: %d\n",
result.Data.SmsBatchID, result.Data.RecipientCount)
}Export your credentials and run:
export TEXTBEE_DEVICE_ID="your-device-id"
export TEXTBEE_API_KEY="your-api-key"
go run main.goSending to multiple recipients
Change Recipients in smsRequest to a slice with multiple entries — one API call reaches all of them:
func sendBulkSMS(deviceID, apiKey string, recipients []string, message string) (*smsResponse, error) {
url := fmt.Sprintf(
"https://api.textbee.dev/api/v1/gateway/devices/%s/send-sms",
deviceID,
)
payload := smsRequest{
Recipients: recipients,
Message: message,
}
// ... rest identical to sendSMS above
}
// Usage
recipients := []string{"+15551234567", "+15559876543", "+15550001111"}
result, err := sendBulkSMS(deviceID, apiKey, recipients, "Appointment reminder: tomorrow at 10am.")Adding a timeout and context
For production use, always set a timeout on the HTTP client:
import (
"context"
"time"
)
func sendSMSWithTimeout(deviceID, apiKey, to, message string) (*smsResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// ... build req as before, then:
req = req.WithContext(ctx)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
// ...
}What the API returns
On success:
{
"data": {
"success": true,
"message": "SMS added to queue for processing",
"smsBatchId": "abc123xyz",
"recipientCount": 1
}
}success: true means the message was accepted and queued on your device — not yet necessarily delivered to the recipient's handset. The device sends asynchronously over the cellular network. Track delivery status in the textbee dashboard, or configure webhooks for status callbacks.
On failure, you get a non-2xx status code. The sendSMS function above surfaces the response body in the error, which typically contains an error field explaining the cause (bad API key, device offline, invalid number format, etc.).
Packaging as a reusable client
For a real application, wrap the client in a struct:
type TextbeeClient struct {
deviceID string
apiKey string
http *http.Client
}
func NewTextbeeClient(deviceID, apiKey string) *TextbeeClient {
return &TextbeeClient{
deviceID: deviceID,
apiKey: apiKey,
http: &http.Client{Timeout: 10 * time.Second},
}
}
func (c *TextbeeClient) Send(to, message string) (*smsResponse, error) {
// use c.deviceID, c.apiKey, c.http
}
func (c *TextbeeClient) SendBulk(recipients []string, message string) (*smsResponse, error) {
// same, with recipients slice
}Initialize once at startup (NewTextbeeClient(os.Getenv(...), os.Getenv(...))), inject into your handlers, and reuse the HTTP client across requests.
Things worth knowing
Phone number format: Use E.164 (+15551234567). Local formats without a country code may work for same-country sends on some carriers but E.164 is unambiguous and always correct.
No per-message fee: Messages route through your own Android SIM. Your cost is the textbee flat subscription — not a per-message meter. See pricing.
Opt-in matters: Messages come from your real phone number. Read the compliance checklist before sending to lists.
Frequently asked questions
Is there a Go SDK for textbee?
Not yet. The REST API is simple enough — the 60 lines above are the "SDK." The API docs cover all endpoints including inbound webhooks, device status, and batch operations.
How do I handle retries on transient failures?
Wrap sendSMS in a retry loop with exponential backoff. On a 429 or 5xx response, wait and retry up to 3 times. Don't retry on 4xx errors (bad request, auth failure) — those won't succeed on retry.
Can I use this in a Goroutine-heavy service?
Yes. http.DefaultClient is safe for concurrent use, and the TextbeeClient struct above is also safe to share across goroutines since all state is in the request, not the client. Use a context with a timeout to avoid goroutine leaks on slow sends.
What Go version is required?
Go 1.13+ for fmt.Errorf with %w. The code otherwise uses only standard library packages available since Go 1.0. No generics, no module-version-specific features.
Keep going
- Full API reference — send, receive, webhooks, device management
- Receive SMS with webhooks — handle inbound SMS in your Go server
- Send SMS from Node.js and Python — same API, different language examples
- Pricing — free tier available
Download textbee and send your first Go SMS in under 5 minutes.
You may also like

Android 15+ SEND_SMS Permission: How to Enable SMS Permissions on Android 15 & 16
SMS permission greyed out on Android 15 or 16? Here's the exact fix to enable SEND_SMS and RECEIVE_SMS for sideloaded apps - in under 2 minutes.

Send SMS from Make.com, Zapier, and n8n with textbee
Automate SMS without a dev team: HTTP module config, phone normalization, retry logic, and workflow patterns for Make.com, Zapier, and n8n. No code needed.

Setting Up Two-Factor Authentication (2FA) with textbee SMS Gateway
A practical guide to implementing SMS-based two-factor authentication using textbee.dev. Step-by-step instructions with code examples.