Dev

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?

StatusSignificado probableAcción en el código
400Solicitud inválida (campo faltante, número mal formado)No repetir. Corregir el dato
401 o 403Clave, secreto o permisoNo repetir. Revisar las credenciales
402Falta de saldoAlertar a la operación. No repetir
404ID de instancia equivocadoRevisar el ID
409Operación en curso (por ejemplo, generación de QR)Esperar y volver a intentar
429Exceso de solicitudesRetry con espera creciente
5xx o timeoutFalla momentáneaRetry 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.

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.