Go integration
Send WhatsApp messages with Go
A complete, runnable Go example for sending WhatsApp messages through the SPWP REST API. There is no SDK to install — Go talks to the API over ordinary HTTP, so you can paste this straight into an existing project.
About 5 minutes to your first delivered message
1. Setup
Create an API key in the dashboard under Accounts → your account → API keys, then make it available to your Go code as an environment variable. Never commit the key or ship it in client-side code.
# Standard library only.
go mod init example.com/whatsapp
export SPWP_API_KEY="wak_live_your_key_here"2. Send a text message
One POST with a recipient and a body. The API replies 202 Accepted with a message id in QUEUED state; delivery happens asynchronously and you can track it by id or webhook.
package spwp
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
const APIBase = "https://api.spwp.app/api/v1"
type Message struct {
ID string `json:"id"`
Status string `json:"status"`
}
type envelope struct {
Data Message `json:"data"`
}
func SendText(to, text string) (*Message, error) {
payload, _ := json.Marshal(map[string]string{"to": to, "text": text})
req, err := http.NewRequest(http.MethodPost, APIBase+"/public/messages/text", bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", os.Getenv("SPWP_API_KEY"))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
return nil, fmt.Errorf("spwp: unexpected status %d", res.StatusCode)
}
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
return &env.Data, nil
}3. Send an image, document or other media
Media endpoints take the same shape, with a source that is either a public HTTPS URL we fetch server-side or a base64 data URL you upload inline. Swap the path segment for video, audio, document or location.
func SendImage(to, source, caption string) (*Message, error) {
payload, _ := json.Marshal(map[string]string{
"to": to,
"source": source,
"caption": caption,
})
req, _ := http.NewRequest(http.MethodPost, APIBase+"/public/messages/image", bytes.NewReader(payload))
req.Header.Set("X-API-Key", os.Getenv("SPWP_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
err = json.NewDecoder(res.Body).Decode(&env)
return &env.Data, err
}4. Receive replies and delivery receipts
Inbound messages and status changes arrive as signed POSTs to your webhook URL. Recompute the HMAC over the timestamp and the raw request body — re-serialised JSON will not match — and reject anything older than five minutes.
package spwp
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"regexp"
"strconv"
"time"
)
var sigRe = regexp.MustCompile(`v1=([a-f0-9]+)`)
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
ts := r.Header.Get("X-WA-Timestamp")
match := sigRe.FindStringSubmatch(r.Header.Get("X-WA-Signature"))
if ts == "" || len(match) < 2 {
w.WriteHeader(http.StatusBadRequest)
return
}
// Replay protection.
sent, _ := strconv.ParseInt(ts, 10, 64)
if time.Since(time.UnixMilli(sent)).Abs() > 5*time.Minute {
w.WriteHeader(http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, []byte(os.Getenv("SPWP_WEBHOOK_SECRET")))
mac.Write([]byte(ts + "." + string(body)))
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(match[1]), []byte(expected)) {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK) // Acknowledge first, process asynchronously.
}Error handling
Errors carry a non-2xx status and a stable code string you can branch on: VALIDATION_FAILED, INVALID_API_KEY, ACCOUNT_NOT_CONNECTED, RATE_LIMITED, MEDIA_FETCH_FAILED. Retry on 429 while honouring Retry-After; treat 401 as a configuration problem rather than a transient one.
Next steps
The full reference covers every endpoint, field and error code, plus the webhook payload shapes.