Run these in order. Your first bridge is free for 7 days with a card on file, nothing charged until the trial ends.
Create an API key at Dashboard → API keys. It starts with sk_ and is shown once, so copy it now. Every call below sends it as Authorization: Bearer $WA_API_KEY.
Go has no required dependencies. The examples use the standard library net/http and encoding/json.
customer_ref = sandbox. Skip steps 2 and 3, use sandbox wherever a customer_ref appears below, and send to 15550001234, a fake contact who replies within seconds. Events show on your dashboard.export WA_API_KEY="sk_..."
Your first bridge is created from the dashboard: one click connects a number and starts the 7-day free trial (card on file, $0 today). Every API call below can address it as /api/instances/default/…: default always means your first bridge, so no name goes in the URL. The call here is how you add more bridges to the subscription.
customer_ref is your internal identifier (user ID, slug, anything you control). webhook_url is optional: leave it out to use the hosted inbox and read events on the dashboard, change it any time with PATCH.
Store the webhook_secret that comes back. It signs every webhook the bridge sends you.
402 no_subscription: connect a number in the dashboard first.package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const base = "https://wabridges.com/api"
func waPost(path string, body any) map[string]any {
apiKey := os.Getenv("WA_API_KEY")
b, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", base+path, bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
return result
}
func main() {
bridge := waPost("/instances", map[string]string{
"customer_ref": "user-123",
"webhook_url": "https://yourbackend.com/hook",
})
fmt.Println(bridge)
}
{"id": "7d09aa9c-…", "customer_ref": "user-123", "state": "running", "created_at": 1777180605, "webhook_secret": "whs_…", "webhook_url": "https://…/hook"}
You need the phone in hand for this step. Request a pairing code, open WhatsApp on the phone, go to Linked Devices → Link a device → Link with phone number, then enter the code. Any number works, personal, VoIP, or a spare SIM. No phone? Use sandbox.
Once paired, the bridge fires a connected webhook event. Poll /proxy/status or wait for the event.
result := waPost("/instances/user-123/proxy/pair", map[string]string{
"phone": "15550001234",
})
fmt.Println("Enter this code on the phone:", result["code"])
{"code": "ABCD-EFGH"}
Add an Idempotency-Key: <unique-per-message> header in production so a timeout plus retry can never double-send. See Idempotency.
A bridge that sat for a few hours with no phone linked is paused to save resources. The first call after that returns 503 bridge_starting with a Retry-After header. Wait those seconds and retry once.
msg := waPost("/instances/user-123/proxy/send/text", map[string]string{
"chat": "15559876543",
"body": "Hello from the API!",
})
fmt.Println(msg["message_id"])
{"message_id": "ACE41E...", "timestamp": 1777180605}
Inbound messages and events are delivered by POST to your webhook_url. Return 200 immediately and do the work afterwards.
The webhook guide has every event type and field, plus signature verification.
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func hookHandler(w http.ResponseWriter, r *http.Request) {
var payload map[string]any
json.NewDecoder(r.Body).Decode(&payload)
w.WriteHeader(http.StatusOK)
if payload["event"] == "message" && payload["from_me"] != true {
fmt.Printf("%s: %s\n", payload["name"], payload["body"])
}
}
func main() {
http.HandleFunc("/hook", hookHandler)
http.ListenAndServe(":3000", nil)
}
Everything the quickstart skipped: media, polls, contacts, presence, and every webhook payload.