Enviar WhatsApp con C# (.NET) por API REST
Para enviar WhatsApp con C# y .NET, usa un HttpClient con la URL base https://api.zapzapapi.com/api/v1/ y los encabezados x-api-key y x-api-secret, y haz un POST a {instanceId}/send/text con number y text. Esta guía muestra el cliente con inyección de dependencias, el envío de texto y multimedia, la validación de números y un endpoint de ASP.NET Core que recibe webhooks con verificación de firma HMAC. ZapZap 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, clave y secreto de API y el ID de la instancia (inicio rápido y autenticación). Guarda todo en user secrets o en variables de entorno, nunca en el repositorio. El ejemplo usa .NET 8 o superior, System.Net.Http.Json y System.Text.Json, que ya vienen en el framework. El registro con AddHttpClient en una aplicación de consola exige el paquete Microsoft.Extensions.Http, que ya viene en los proyectos ASP.NET Core.
El número de destino va con código de país y de área, solo dígitos (5511999999999). La respuesta del envío trae messageId y status. Los campos opcionales del texto incluyen delay en milisegundos, replyid y digitando, que viene activado por defecto.
¿Cómo configurar el HttpClient con inyección de dependencias?
No crees un HttpClient nuevo en cada envío: eso agota los sockets bajo carga. Registra un cliente tipado con AddHttpClient, que administra el ciclo de vida de las conexiones, y coloca la URL base y los encabezados de autenticación en el registro. La barra final de la BaseAddress importa: sin ella, la ruta relativa puede armarse de forma incorrecta.
Program.cs (registro do cliente)
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddHttpClient<ZapZapClient>(http =>
{
http.BaseAddress = new Uri("https://api.zapzapapi.com/api/v1/");
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Add("x-api-key", Environment.GetEnvironmentVariable("ZAPZAP_API_KEY"));
http.DefaultRequestHeaders.Add("x-api-secret", Environment.GetEnvironmentVariable("ZAPZAP_API_SECRET"));
});
var provider = services.BuildServiceProvider();
var zap = provider.GetRequiredService<ZapZapClient>();
var id = await zap.SendTextAsync("5511999999999", "Olá! Seu pedido saiu para entrega.");
Console.WriteLine("texto enviado: " + id);
var idDoc = await zap.SendMediaAsync("5511999999999", "document", "https://exemplo.com/nota.pdf", "Segue a sua nota fiscal.");
Console.WriteLine("documento enviado: " + idDoc);¿Cómo enviar texto y multimedia en C#?
La clase de abajo envuelve los endpoints enviar texto y enviar multimedia, más la verificación de número. Usa PostAsJsonAsync para serializar el cuerpo y lee el cuerpo del error antes de lanzar la excepción, para que el mensaje de la API no se pierda. En enviar multimedia, type acepta image, video, audio, ptt, document y sticker, y file recibe la URL pública del archivo.
ZapZapClient.cs
using System.Net.Http.Json;
using System.Text.Json;
public class ZapZapClient
{
private readonly HttpClient _http;
private readonly string _instance = Environment.GetEnvironmentVariable("INSTANCE_ID")!;
public ZapZapClient(HttpClient http) => _http = http;
public Task<string?> SendTextAsync(string number, string text) =>
PostAsync($"{_instance}/send/text", new { number, text });
public Task<string?> SendMediaAsync(string number, string type, string fileUrl, string? caption = null)
{
var body = new Dictionary<string, object> { ["number"] = number, ["type"] = type, ["file"] = fileUrl };
if (!string.IsNullOrEmpty(caption)) body["text"] = caption;
return PostAsync($"{_instance}/send/media", body);
}
// devolve apenas os numeros que existem no WhatsApp
public async Task<List<string>> ExistentesAsync(IEnumerable<string> numbers)
{
using var resp = await _http.PostAsJsonAsync($"{_instance}/chat/check", new { numbers });
var json = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new HttpRequestException($"zapzap {(int)resp.StatusCode}: {json}");
var ok = new List<string>();
using var doc = JsonDocument.Parse(json);
foreach (var item in doc.RootElement.EnumerateArray())
{
if (item.GetProperty("isInWhatsapp").GetBoolean())
ok.Add(item.GetProperty("query").GetString()!);
}
return ok;
}
private async Task<string?> PostAsync(string path, object body)
{
using var resp = await _http.PostAsJsonAsync(path, body);
var json = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new HttpRequestException($"zapzap {(int)resp.StatusCode}: {json}");
using var doc = JsonDocument.Parse(json);
return doc.RootElement.TryGetProperty("messageId", out var id) ? id.GetString() : null;
}
}¿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 | 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 .NET, la forma idiomática es una política de resiliencia en AddHttpClient (la biblioteca Microsoft.Extensions.Http.Resilience, que usa Polly) con retry exponencial para 429, 5xx y fallas de red. Cuidado con el envío: reenviar un mensaje que ya salió duplica el texto para el cliente. Para lo importante, guarda el messageId y confirma el status por el webhook, en lugar de repetir a ciegas. La lista de endpoints está en la referencia de la API y en la página de API de WhatsApp.
¿Cómo recibir el webhook en ASP.NET Core con firma?
Configura la URL en configurar webhook y guarda el webhook_secret. Cada entrega trae X-ZapZap-Signature (t=<unix>,v1=<hmac>), en que el HMAC es SHA-256 de t, punto y cuerpo crudo. Lee el cuerpo como bytes antes de cualquier deserialización, calcula el HMAC y compara en tiempo constante con CryptographicOperations.FixedTimeEquals. Responde 200 rápido y pon el trabajo en una cola (Channel, BackgroundService o un broker). La idempotencia y el retry están en webhook en producción.
WebhookApi/Program.cs (ASP.NET Core minimal API)
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var app = WebApplication.CreateBuilder(args).Build();
var secret = Environment.GetEnvironmentVariable("ZAPZAP_WEBHOOK_SECRET")!;
app.MapPost("/webhooks/zapzap", async (HttpRequest req) =>
{
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
var raw = ms.ToArray();
if (!Valida(secret, raw, req.Headers["X-ZapZap-Signature"].ToString()))
return Results.Unauthorized();
var deliveryId = req.Headers["X-ZapZap-Id"].ToString(); // chave de deduplicacao
using var doc = JsonDocument.Parse(raw);
var root = doc.RootElement;
if (root.GetProperty("event").GetString() == "messages"
&& root.TryGetProperty("data", out var data)
&& data.TryGetProperty("message", out var msg)
&& !(msg.TryGetProperty("fromMe", out var fm) && fm.GetBoolean()))
{
Console.WriteLine($"{deliveryId}: {msg.GetProperty("chatid").GetString()} -> {msg.GetProperty("text").GetString()}");
// aqui: enfileirar o trabalho em vez de processar na requisicao
}
return Results.Ok();
});
static bool Valida(string secret, byte[] raw, string header)
{
long t = 0; string v1 = "";
foreach (var parte in header.Split(','))
{
var kv = parte.Trim().Split('=', 2);
if (kv.Length != 2) continue;
if (kv[0] == "t") long.TryParse(kv[1], out t);
if (kv[0] == "v1") v1 = kv[1];
}
if (t == 0 || v1.Length == 0) return false;
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var payload = Encoding.UTF8.GetBytes(t + ".").Concat(raw).ToArray();
var esperado = Convert.ToHexString(hmac.ComputeHash(payload)).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(esperado), Encoding.UTF8.GetBytes(v1));
}
app.Run();Preguntas de producción: ¿qué más cuidar?
Ritmo: si envías en lote, usa una pausa variable entre mensajes (Task.Delay con un valor aleatorio) y no dispares miles de tareas en paralelo. El ritmo y la calidad de la lista son lo que más pesa para el número, como se describe en la guía de antibloqueo. Conexión: consulta el status de la instancia antes de las campañas. Secretos: user secrets en desarrollo y una bóveda de secretos en producción.
Para el mismo flujo en otros lenguajes, mira la guía de Node, Python y PHP y la de Go. Los precios por número activo están en precios.
¿Cómo probar con curl antes de escribir código?
Antes de depurar C#, confirma que las credenciales, el ID y el número funcionan con un curl. Si el curl responde con messageId y status, el problema, si lo hay, está en el código. Si el curl falla, el problema está en la credencial, en la instancia o en el número. Esta prueba de dos minutos evita horas de depuración en el lugar equivocado.
Conviene también revisar el estado de la instancia antes de cualquier prueba: una instancia desconectada no envía. La guía de QR que no aparece y la de WhatsApp que se desconecta solo cubren los casos más comunes.
Teste de envio com curl
curl -X POST "https://api.zapzapapi.com/api/v1/$INSTANCE_ID/send/text" \
-H "x-api-key: $ZAPZAP_API_KEY" \
-H "x-api-secret: $ZAPZAP_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"number":"5511999999999","text":"Teste de envio"}'
# resposta esperada: {"messageId":"...","status":"sent"}Resumen rápido
- Enviar WhatsApp en C# es un POST a {instanceId}/send/text con los encabezados x-api-key y x-api-secret.
- Registra un HttpClient tipado con AddHttpClient y deja la URL base con barra final.
- Valida los números con el endpoint de verificación antes de las campañas.
- Usa retry exponencial solo para 429, 5xx y fallas de red, y confirma los envíos importantes por el webhook.
- En el webhook, valida X-ZapZap-Signature sobre los bytes del cuerpo con FixedTimeEquals y responde 200 rápido.
Sigue leyendo
Lee también
Preguntas frecuentes
¿Necesito un SDK para usar la API en C#?
No. Un HttpClient con System.Net.Http.Json y System.Text.Json basta. Los ejemplos no usan paquetes de terceros (AddHttpClient viene de Microsoft.Extensions.Http, ya incluido en ASP.NET Core).
¿Cómo autenticar las llamadas en .NET?
Agrega los encabezados x-api-key y x-api-secret al DefaultRequestHeaders del HttpClient registrado, leyendo los valores de variables de entorno o user secrets.
¿Cómo enviar una imagen o un PDF en C#?
Usa el endpoint de enviar multimedia con number, type (image, document y otros) y file con la URL pública del archivo. El campo text funciona como pie de foto.
¿Cómo validar la firma del webhook en ASP.NET Core?
Lee el cuerpo como bytes, calcula el HMAC SHA-256 de t, punto y cuerpo con el webhook_secret y compáralo con el v1 del encabezado X-ZapZap-Signature usando CryptographicOperations.FixedTimeEquals. Rechaza timestamps con más de 5 minutos.
¿Cuál es el riesgo de usar la API no oficial desde mi sistema?
El uso no oficial no está previsto en los Términos de WhatsApp y el número puede ser restringido, sin importar el lenguaje. El ritmo, el consentimiento y la lista limpia reducen el riesgo, pero no lo eliminan.
Haz tu primer envío en C# 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.
