How to Send WhatsApp Messages from Go (No SDK Required)
Send WhatsApp messages from Go using only net/http and encoding/json — no third-party SDK required. Session setup, QR auth, and production error handling.
Meta Description: Send WhatsApp messages from Go using only net/http and encoding/json — no third-party SDK required. Session setup, QR auth, and production error handling.
Introduction
Every WhatsApp-in-Go tutorial you'll find reaches for a vendor SDK first. Skip it — the WhatsApp Messaging API is plain REST over HTTPS, and Go's standard library (net/http, encoding/json) is all you need.
In this guide, you'll send a real WhatsApp message from Go in under 5 minutes, with zero external dependencies.
What you'll need:
- •Go 1.18+
- •A RapidAPI account (free)
- •5 minutes
Let's send a message.
Step 1: Get Your API Key (30 seconds)
- 1.Go to RapidAPI Hub
- 2.Search for "Whatsapp messaging bot"
- 3.Click Subscribe (Free plan available, no card required)
- 4.Open API Settings and copy your X-RapidAPI-Key
Step 2: Create a WhatsApp Session
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const apiHost = "whatsapp-messaging-bot.p.rapidapi.com"
var apiKey = os.Getenv("RAPIDAPI_KEY")
func call(method, path string, body map[string]any) (int, map[string]any, error) {
var reqBody io.Reader
if body != nil {
b, _ := json.Marshal(body)
reqBody = bytes.NewReader(b)
}
req, err := http.NewRequest(method, "https://"+apiHost+path, reqBody)
if err != nil {
return 0, nil, err
}
req.Header.Set("x-rapidapi-key", apiKey)
req.Header.Set("x-rapidapi-host", apiHost)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
return resp.StatusCode, result, nil
}
func main() {
sessionName := "my-session"
// Create the session
_, created, _ := call("POST", "/v1/sessions", map[string]any{"name": sessionName})
fmt.Println("Session created:", created)
// Get the QR code to authenticate
_, qr, _ := call("GET", "/v1/sessions/"+sessionName+"/auth/qr", nil)
fmt.Println("Scan this with WhatsApp → Linked Devices:")
fmt.Println(qr["qr"])
}RAPIDAPI_KEY=your_key_here go run main.goOpen WhatsApp on your phone → Settings → Linked Devices → Link a Device, and scan the printed QR code.
Step 3: Send Your First Message
func sendText(chatId, text, session string) (int, map[string]any, error) {
return call("POST", "/v1/sendText", map[string]any{
"chatId": chatId,
"text": text,
"session": session,
})
}
func main() {
status, result, err := sendText("14155552671", "Hello from Go!", "my-session")
if err != nil {
fmt.Println("Request failed:", err)
return
}
if status == 200 || status == 201 {
fmt.Println("Message sent.")
} else {
fmt.Println("Send failed:", result)
}
}Check your phone — that's a real WhatsApp message sent from Go with zero external dependencies.
Step 4: Sending Images and Files
Same call() helper, different endpoint and payload:
func sendImage(chatId, imageUrl, caption, session string) (int, map[string]any, error) {
return call("POST", "/v1/sendImage", map[string]any{
"chatId": chatId,
"image": imageUrl,
"caption": caption,
"session": session,
})
}Both sendImage and sendFile take a public URL — the API fetches and delivers it, no upload step needed. Full parameter reference for every message type is in the interactive API reference.
Production Best Practices
1. Retry with backoff
func sendWithRetry(chatId, text, session string, maxRetries int) (int, map[string]any, error) {
var status int
var result map[string]any
var err error
for attempt := 1; attempt <= maxRetries; attempt++ {
status, result, err = sendText(chatId, text, session)
if err == nil && status < 400 {
return status, result, nil
}
time.Sleep(time.Duration(attempt) * 2 * time.Second) // 2s, 4s, 6s backoff
}
return status, result, err
}(add "time" to your imports)
2. Reuse http.DefaultClient or a shared *http.Client
Building a new http.Client per request throws away connection pooling. A single package-level client (as used in call() above) is the idiomatic Go pattern.
3. Confirm delivery with a webhook, not the HTTP response
A 200 from /v1/sendText means "accepted," not "delivered" — a number that isn't on WhatsApp can still return success. Register a webhook and read the message.ack event for real delivery/read status:
call("POST", "/v1/sessions/"+sessionName+"/webhooks", map[string]any{
"webhookUrl": "https://yourapp.com/webhooks/whatsapp",
"events": []string{"message.ack"},
})Troubleshooting
| Issue | Fix |
|---|---|
| 401 Unauthorized | RAPIDAPI_KEY environment variable isn't set, or the key's been rotated in the RapidAPI dashboard. |
x509: certificate errors on older systems | Update your system's CA certificates — Go's net/http uses the OS trust store. |
| QR code expires before you scan it | QR codes are short-lived — fetch it right before you're ready to scan. |
| Message accepted but never arrives | Check message.ack via webhook — a 200 only means the API accepted the send. |
Next Steps
- •WhatsApp OTP Authentication — reuse this same Go client to send login codes instead of SMS
- •Send Files, PDFs, and Documents via WhatsApp API — full MIME type reference and size limits
- •Best WhatsApp API for Startups and Indie Hackers — how this compares on price and setup time
- •Will My Number Get Banned? — the honest version of the "unofficial APIs get you banned" warning
External references:
- •net/http package documentation — Go's official docs for every function used above
- •WhatsApp Business Platform overview — Meta's official docs, for context on how the official API differs
Try Free on RapidAPI → Get your API key in 30 seconds → Send your first Go message in 5 minutes.
Ready to Get Started?
Try the WhatsApp API free on RapidAPI with no credit card required.
Try Free on RapidAPI