149 countries · crypto-native · no KYC

Send SMS with Go in 2026: Stdlib-Only API Tutorial

No SDK, no dependencies. Just net/http, a context timeout, and a retry that respects idempotency. Production-shaped Go SMS in one file. For example, production shaping adds error handling for malformed phone numbers and rate limiting to prevent server overload.

$0.035/msg from sub-100ms median 98.6% delivered
Send SMS with Go in 2026: Stdlib-Only API Tutorial — smsroute
$0.004
per SMS from
149
countries
60s
to first message
6
crypto rails
To send SMS with Go you need nothing beyond the standard library. net/http makes the request, context bounds it, and encoding/json handles the payload. No SDK means no dependency to audit, no version to chase, and code that moves to any provider by changing one URL. We target SMSRoute endpoints below. SMSRoute is a no-KYC SMS API with crypto billing (BTC, ETH, USDT, XMR, LTC, and SOL), so signup to first send is minutes. But the shape is universal.

Why stdlib is the whole toolkit here

Why use only Go standard library for SMS API calls?

Using Go's standard library eliminates external dependencies, reduces build complexity, and ensures long-term maintainability. SMSRoute's REST API works seamlessly with net/http, making integration straightforward. You get production-grade HTTP client capabilities without third-party packages, keeping your codebase lean and auditable.

To send SMS with Go you need nothing beyond the standard library. net/http makes the request, context bounds it, and encoding/json handles the payload. No SDK means no dependency to audit, no version to chase, and code that moves to any provider by changing one URL. We target SMSRoute endpoints below. SMSRoute is a no-KYC SMS API with crypto billing (BTC, ETH, USDT, XMR, LTC, and SOL), so signup to first send is minutes. But the shape is universal.

package sms

import (
	"bytes"; "context"; "encoding/json"; "fmt"; "net/http"; "os"; "time"
)

func Send(ctx context.Context, to, text string) (string, error) {
	body, _ := json.Marshal(map[string]string{"to": to, "from": "YourSenderID", "message": text})
	req, _ := http.NewRequestWithContext(ctx, "POST",
		"https://api.smsroute.cc/sms/send", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SMSROUTE_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil { return "", err }
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		return "", fmt.Errorf("send failed: %d", resp.StatusCode)
	}
	// Response schema not specified. Handle generically.
	var out map[string]interface{}
	json.NewDecoder(resp.Body).Decode(&out)
	return fmt.Sprintf("%v", out), nil
}

Two Go-idiomatic habits are baked in. The key comes from os.Getenv, never a literal. And the request takes a context.Context, so the caller controls the timeout and cancellation. A hung OTP send should never block a request goroutine.

Timeouts and retries the Go way

How to implement SMS API timeouts and retries in Go?

Go's context package and net/http client provide built-in timeout and retry mechanisms. Set a context deadline per request, then implement exponential backoff with jitter for retries. SMSRoute's API responds reliably within these patterns, and our 99.9% uptime means retries rarely trigger, keeping your code efficient.

Timeouts and retries the Go way — comparison diagram

In Go, timeouts belong on the context, and retries belong to failures where the message definitely did not send. Never retry a success; never retry a 4xx.

func SendWithRetry(to, text string, tries int) (id string, err error) {
	for i := 0; i < tries; i++ {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		id, err = Send(ctx, to, text)
		cancel()
		if err == nil { return id, nil }
		// retry only timeouts and 5xx; 4xx means fix the request
		if ctx.Err() == nil && !is5xx(err) { return "", err }
		time.Sleep(time.Duration(500*(1<<i)) * time.Millisecond) // 0.5s,1s,2s
	}
	return "", err
}
Failure Retry? Why
Context deadline / network Yes, backoff The request may never have landed
5xx Yes, backoff Provider-side, pre-acceptance
4xx (bad number, auth) No Retry re-fails identically. Fix the input
Success (id returned) Never Message is out; retry double-sends

Cap total time for OTP. Three retries with backoff already spends ~4 seconds; past that, hand the user a resend button rather than keep the goroutine grinding. The UX ladder from our OTP best-practices guide. Our OTP best practices guide recommends capping retries at three with exponential backoff, then offering a resend button. See the full guide at https://example.com/otp-best-practices.

DLR webhook, idempotent by default

How to handle SMS delivery reports (DLR) idempotently in Go?

SMSRoute sends real-time DLR webhooks with unique message IDs. In Go, store processed IDs in a map or database to skip duplicates. Use sync.Mutex for thread safety. This idempotent pattern ensures your application correctly tracks delivery status even if webhooks arrive multiple times.

A returned id means accepted, not delivered. The delivery receipt arrives later by webhook. Handle it fast and idempotently.

func dlrHandler(w http.ResponseWriter, r *http.Request) {
	var dlr struct{ ID, Status string }
	json.NewDecoder(r.Body).Decode(&dlr)
	markDelivery(dlr.ID, dlr.Status) // keyed on ID. DLRs can repeat.
	w.WriteHeader(http.StatusOK)     // ack immediately, process async
}

Testing offline with an interface

How to test SMS sending code offline in Go?

Define an interface for your SMS client, then implement a mock that returns predefined responses. SMSRoute's API structure maps cleanly to this pattern. Your production code uses the real HTTP client, while tests use the mock. No network needed. This approach validates retry logic, error handling, and DLR processing without sending actual messages.

Go's interfaces make the fake trivial. Define a Sender interface, inject a real client in production and a fake in tests. No per-commit spend, full coverage of the failure branches.

type Sender interface{ Send(ctx context.Context, to, text string) (string, error) }

type fakeSender struct{}
func (fakeSender) Send(_ context.Context, _, _ string) (string, error) {
	return "test_123", nil // deterministic, free, offline
}

Assert on response shape (an id came back, no error), prove the retry path sends once on a 500-then-200 sequence, and confirm duplicate DLRs update state once. Reserve a single real send to your own phone as the pre-release smoke test. The full pattern is in our test-numbers guide. Production cost is per message and per destination. Budget with the international cost guide. SMSRoute's published route pages list delivery from $0.004/message (premium direct-carrier corridors up to $0.035) with sub-100ms median submission and ~98.6% delivered success (smsroute.cc route pages, 2026). For authoritative guidance on retry strategies, see IETF RFC 7231 Section 6.3.1 on idempotent methods.

FAQ

How do I send an SMS in Go without a third-party SDK?
Use net/http: marshal a JSON body with the recipient and text, POST it to your provider's send endpoint with a Bearer key from os.Getenv, and pass a context.Context for timeout control. The standard library covers everything — no dependency to add or audit.
How should I add timeouts to Go SMS calls?
Put the deadline on the context, not a package-level client, using context.WithTimeout and http.NewRequestWithContext. This lets each caller bound and cancel its own request — essential so a slow OTP send never blocks a request goroutine indefinitely.
When should a Go SMS send be retried?
Only on failures where the message definitely didn't go out: context deadline/network errors and 5xx responses, with exponential backoff and a small cap. Never retry a successful send (you'll double-message users) or a 4xx (it will fail identically until you fix the request).
How do I test SMS sending in Go without paying?
Define a Sender interface and inject a fake implementation that returns a deterministic id offline. Assert on response shape, retry behavior, and idempotent DLR handling in your test suite, and keep just one real send to a number you control as a pre-release smoke test.

Send your first SMS in 5 minutes

No KYC. Pay with BTC, ETH, USDT, XMR, LTC, and SOL. Live routes to 149 countries.

Get an API key →