Enviar WhatsApp con Go por API REST: ejemplo completo
Para enviar WhatsApp con Go solo necesitas la biblioteca estándar: un POST a /api/v1/{instanceId}/send/text con los encabezados x-api-key y x-api-secret y un JSON con number y text. Esta guía arma un cliente pequeño y reutilizable con net/http y encoding/json, envía texto y multimedia, valida números antes de enviar, trata los errores con contexto y recibe webhooks con validación de firma. Los ejemplos usan la ZapZap API, que es una API no oficial: el número se conecta por QR, y ese uso no está previsto en los Términos de WhatsApp.
Publicado el · 5 min de lectura
Este artículo también está disponible en: Português · English
¿Qué necesitas antes de empezar?
Una cuenta, una instancia conectada por QR, la clave y el secreto de API y el ID de la instancia. El paso a paso está en el inicio rápido y la autenticación en autenticación. Guarda las credenciales en variables de entorno: ZAPZAP_API_KEY, ZAPZAP_API_SECRET e INSTANCE_ID.
La dirección base es https://api.zapzapapi.com/api/v1. Los endpoints de envío cuelgan del ID de la instancia (por ejemplo, /{instanceId}/send/text), y todas las llamadas llevan los encabezados x-api-key y x-api-secret. El número de destino va con código de país y de área, solo dígitos, como 5511999999999. No se necesita ninguna dependencia externa: el ejemplo funciona con la biblioteca estándar de Go.
¿Cómo armar un cliente HTTP en Go?
Concentra la autenticación, el timeout y el manejo de errores en un único método. Así evitas repetir encabezados, te aseguras de que toda llamada tenga plazo y conviertes las respuestas con status 400 o mayor en un error que incluye el cuerpo, lo que facilita mucho la depuración. Usa siempre un http.Client propio con Timeout: el cliente por defecto no tiene límite de tiempo.
zapzap/client.go
package zapzap
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const base = "https://api.zapzapapi.com/api/v1"
type Client struct {
HTTP *http.Client
Key string
Secret string
Instance string
}
func FromEnv() *Client {
return &Client{
HTTP: &http.Client{Timeout: 30 * time.Second},
Key: os.Getenv("ZAPZAP_API_KEY"),
Secret: os.Getenv("ZAPZAP_API_SECRET"),
Instance: os.Getenv("INSTANCE_ID"),
}
}
// do executa a chamada, decodifica o JSON em out e devolve erro com o corpo se status >= 400.
func (c *Client) do(method, path string, in, out any) error {
var body io.Reader
if in != nil {
b, err := json.Marshal(in)
if err != nil {
return err
}
body = bytes.NewReader(b)
}
req, err := http.NewRequest(method, base+path, body)
if err != nil {
return err
}
req.Header.Set("x-api-key", c.Key)
req.Header.Set("x-api-secret", c.Secret)
if in != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTP.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return fmt.Errorf("zapzap %s %s: status %d: %s", method, path, resp.StatusCode, raw)
}
if out != nil {
return json.Unmarshal(raw, out)
}
return nil
}¿Cómo enviar texto desde Go?
El endpoint enviar texto recibe number y text. Campos opcionales útiles: delay (en milisegundos), replyid (para responder a un mensaje) y digitando, que viene activado por defecto y muestra 'escribiendo...' por un tiempo proporcional al tamaño del texto. La respuesta trae messageId y status. Guarda el messageId si quieres correlacionarlo con los eventos de status del webhook.
zapzap/send.go
package zapzap
// SendText envia uma mensagem de texto e devolve o messageId.
func (c *Client) SendText(number, text string) (string, error) {
var out map[string]any
err := c.do("POST", "/"+c.Instance+"/send/text", map[string]any{
"number": number,
"text": text,
}, &out)
if err != nil {
return "", err
}
id, _ := out["messageId"].(string)
return id, nil
}
// SendMedia envia imagem, video, audio, documento etc. a partir de uma URL publica.
// mediaType: image, video, audio, ptt, document, sticker.
func (c *Client) SendMedia(number, mediaType, fileURL, caption string) (string, error) {
var out map[string]any
payload := map[string]any{
"number": number,
"type": mediaType,
"file": fileURL,
}
if caption != "" {
payload["text"] = caption
}
if err := c.do("POST", "/"+c.Instance+"/send/media", payload, &out); err != nil {
return "", err
}
id, _ := out["messageId"].(string)
return id, nil
}main.go
package main
import (
"fmt"
"log"
"example.com/app/zapzap"
)
func main() {
c := zapzap.FromEnv()
id, err := c.SendText("5511999999999", "Olá! Seu pedido saiu para entrega.")
if err != nil {
log.Fatal(err)
}
fmt.Println("texto enviado:", id)
id, err = c.SendMedia("5511999999999", "document", "https://exemplo.com/nota.pdf", "Segue a sua nota fiscal.")
if err != nil {
log.Fatal(err)
}
fmt.Println("documento enviado:", id)
}¿Cómo validar el número antes de enviar?
Enviar a un número que no existe en WhatsApp genera fallas y, en volumen, es un patrón que WhatsApp penaliza. El endpoint verificar número recibe una lista en numbers y devuelve, para cada elemento, query, isInWhatsapp y jid. Valida en lote antes de una campaña y quita los inexistentes. La misma lógica vale para el cuidado con el ritmo y la lista descrito en la guía de antibloqueo.
zapzap/check.go
package zapzap
// Existentes devolve apenas os numeros que estao no WhatsApp.
func (c *Client) Existentes(numbers []string) ([]string, error) {
var out []map[string]any
err := c.do("POST", "/"+c.Instance+"/chat/check", map[string]any{"numbers": numbers}, &out)
if err != nil {
return nil, err
}
var ok []string
for _, item := range out {
if in, _ := item["isInWhatsapp"].(bool); in {
q, _ := item["query"].(string)
ok = append(ok, q)
}
}
return ok, nil
}¿Qué errores tratar y cómo hacer retry?
| Status | Significado probable | Acción en el código |
|---|---|---|
| 400 | Solicitud inválida (campo faltante, número mal formado) | No repetir. Corregir el dato |
| 401 o 403 | Clave, secreto o permiso | No repetir. Revisar las credenciales |
| 402 | Falta de saldo | Alertar a la operación. No repetir |
| 404 | ID de instancia equivocado | Revisar el ID |
| 409 | Operación en curso (por ejemplo, generación de QR) | Esperar y volver a intentar |
| 429 | Exceso de solicitudes | Retry con espera creciente |
| 5xx o timeout | Falla momentánea | Retry con espera creciente y límite de intentos |
En Go, el patrón es un bucle con número máximo de intentos y espera exponencial, repitiendo solo para 429, 5xx y errores de red. Para 4xx de validación, repetir no sirve. Si el envío es importante, guarda el messageId y confirma el status por el webhook, en lugar de reenviar a ciegas, porque reenviar un mensaje que ya salió duplica el texto para el cliente. La lista completa de endpoints está en la referencia de la API y en la página de API de WhatsApp.
¿Cómo recibir el webhook en Go con firma?
Configura la URL en configurar webhook y guarda el webhook_secret. Cada entrega llega con X-ZapZap-Signature con el formato t=<unix>,v1=<hmac>, donde el HMAC es SHA-256 de t, punto y cuerpo crudo. Lee el cuerpo completo antes de decodificar, calcula el HMAC y compara con hmac.Equal. Responde 200 rápido y procesa en una goroutine o en una cola. Las reglas de idempotencia y retry están en webhook en producción.
webhook.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"math"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func assinaturaValida(secret string, raw []byte, header string) bool {
var t int64
var v1 string
for _, p := range strings.Split(header, ",") {
kv := strings.SplitN(strings.TrimSpace(p), "=", 2)
if len(kv) != 2 {
continue
}
switch kv[0] {
case "t":
t, _ = strconv.ParseInt(kv[1], 10, 64)
case "v1":
v1 = kv[1]
}
}
if t == 0 || v1 == "" || math.Abs(float64(time.Now().Unix()-t)) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(strconv.FormatInt(t, 10) + "."))
mac.Write(raw)
esperado := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(esperado), []byte(v1))
}
func main() {
secret := os.Getenv("ZAPZAP_WEBHOOK_SECRET")
http.HandleFunc("/webhooks/zapzap", func(w http.ResponseWriter, r *http.Request) {
raw, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 2<<20))
if err != nil || !assinaturaValida(secret, raw, r.Header.Get("X-ZapZap-Signature")) {
w.WriteHeader(http.StatusUnauthorized)
return
}
var evento map[string]any
if json.Unmarshal(raw, &evento) != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK) // confirma primeiro
go func(id string, e map[string]any) { // processa depois
data, _ := e["data"].(map[string]any)
msg, _ := data["message"].(map[string]any)
if fromMe, _ := msg["fromMe"].(bool); fromMe || msg == nil {
return
}
log.Println("entrega", id, "mensagem de", msg["chatid"], ":", msg["text"])
}(r.Header.Get("X-ZapZap-Id"), evento)
})
log.Fatal(http.ListenAndServe(":3000", nil))
}Preguntas de producción: ¿qué más cuidar?
Concurrencia: si envías en un bucle, mantén una pausa variable entre mensajes y no dispares miles en paralelo; el ritmo es parte del cuidado con el número, como explica la guía de antibloqueo. Conexión: antes de las campañas, consulta el status de la instancia. Logs: registra el messageId, el número (enmascarado) y el status, nunca la clave ni el secreto.
Si prefieres ver el mismo flujo en Node, Python y PHP, está la guía de enviar mensajes por API en Node, Python y PHP. Para .NET, mira la guía de C# con HttpClient. Los costos por número están en precios.
Resumen rápido
- Enviar WhatsApp en Go es un POST con x-api-key y x-api-secret a /{instanceId}/send/text, con number y text.
- Centraliza autenticación, timeout y errores en un método y usa http.Client con Timeout.
- Valida la lista con el endpoint de verificación de número antes de las campañas.
- Repite solo 429, 5xx y errores de red. Los errores 4xx de validación no se arreglan repitiendo.
- En el webhook, valida X-ZapZap-Signature sobre el cuerpo crudo, responde 200 y procesa después.
Sigue leyendo
Lee también
Preguntas frecuentes
¿Necesito alguna biblioteca para usar la API en Go?
No. La biblioteca estándar (net/http y encoding/json) basta. Los ejemplos de esta guía no usan dependencias externas.
¿Cómo autenticar las llamadas en Go?
Con los encabezados x-api-key y x-api-secret en cada solicitud. Guarda los valores en variables de entorno.
¿Cómo enviar una imagen o un PDF desde Go?
Usa el endpoint de enviar multimedia con number, type (image, document, etc.) y file con la URL pública del archivo. El campo text se vuelve el pie de foto.
¿Cómo validar la firma del webhook en Go?
Calcula el HMAC SHA-256 de t, punto y cuerpo crudo con el webhook_secret, compáralo con hmac.Equal contra el v1 del encabezado X-ZapZap-Signature y rechaza timestamps con más de 5 minutos.
¿Puedo mandar mensajes masivos desde Go?
Técnicamente sí, pero el ritmo y la calidad de la lista determinan el riesgo para el número. Usa pausas variables, valida la lista y lee la guía de antibloqueo. Ninguna herramienta elimina el riesgo de restricción.
Haz tu primer envío en Go hoy
Crea la cuenta, verifica tu teléfono y recibe el crédito de bienvenida, sin depositar (uno por teléfono, sujeto a la campaña vigente). Conecta por QR y copia el cliente de esta guía.
