Dev

Send WhatsApp messages with C# (.NET) via REST API

To send WhatsApp messages with C# and .NET, use an HttpClient with the base URL https://api.zapzapapi.com/api/v1/ and the x-api-key and x-api-secret headers, then make a POST to {instanceId}/send/text with number and text. This guide shows the client with dependency injection, text and media sends, number validation and an ASP.NET Core endpoint that receives webhooks with HMAC signature checks. ZapZap is an unofficial API: the number is connected by QR code, and that use is not covered by the WhatsApp Terms.

Published on · 5 min read

This article is also available in: Português · Español

What do you need before you start?

An account, an instance connected by QR code, the API key and secret and the instance ID (quick start and authentication). Keep everything in user secrets or environment variables, never in the repository. The example uses .NET 8 or later, System.Net.Http.Json and System.Text.Json, which ship with the framework. Registering with AddHttpClient in a console app requires the Microsoft.Extensions.Http package, which already comes with ASP.NET Core projects.

The destination number goes with country code and area code, digits only (5511999999999). The send response carries messageId and status. Optional text fields include delay in milliseconds, replyid and digitando, which is on by default.

How do you configure HttpClient with dependency injection?

Do not create a new HttpClient on every send: it exhausts sockets under load. Register a typed client with AddHttpClient, which manages connection lifetimes, and put the base URL and the authentication headers in the registration. The trailing slash on BaseAddress matters: without it, the relative path may be built incorrectly.

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);

How do you send text and media in C#?

The class below wraps the send text and send media endpoints, plus number verification. It uses PostAsJsonAsync to serialize the body and only reports the failure after reading the error body, so the API's message is not lost. In send media, type accepts image, video, audio, ptt, document and sticker, and file takes the file's public URL.

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;
    }
}

Which errors should you handle and how do you retry?

StatusLikely meaningAction in code
400Invalid request (missing field, malformed number)Do not retry. Fix the data
401 or 403Key, secret or permissionDo not retry. Check credentials
402Low balanceAlert operations. Do not retry
404Wrong instance IDCheck the ID
409Operation in progressWait and try again
429Too many requestsRetry with growing backoff
5xx or timeoutMomentary failureRetry with growing backoff and an attempt limit

In .NET, the idiomatic way is a resilience policy on AddHttpClient (the Microsoft.Extensions.Http.Resilience library, which uses Polly) with exponential retry for 429, 5xx and network failures. Be careful with sends: resending a message that already went out duplicates the text to the customer. For anything important, keep the messageId and confirm the status through the webhook instead of retrying blindly. The list of endpoints is in the API reference and on the WhatsApp API page.

How do you receive the webhook in ASP.NET Core with a signature?

Set the URL in set webhook and keep the webhook_secret. Every delivery carries X-ZapZap-Signature (t=<unix>,v1=<hmac>), where the HMAC is SHA-256 of t, a dot and the raw body. Read the body as bytes before any deserialization, compute the HMAC and compare in constant time with CryptographicOperations.FixedTimeEquals. Return 200 fast and put the work on a queue (Channel, BackgroundService or a broker). Idempotency and retries are covered in webhooks in production.

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();

Production questions: what else to watch?

Pace: if you send in batches, use a variable pause between messages (Task.Delay with a random value) and do not fire thousands of tasks in parallel. Pace and list quality are what weigh most on the number, as described in the anti-ban guide. Connection: check the instance status before campaigns. Secrets: user secrets in development and a secrets vault in production.

For the same flow in other languages, see the guide for Node, Python and PHP and the one for Go. Per-active-number prices are on the pricing page.

How do you test with curl before writing code?

Before debugging C#, confirm that the credentials, the ID and the number work with a curl call. If curl responds with a messageId and status, the problem, if any, is in the code. If curl fails, the problem is in the credential, the instance or the number. That two-minute test saves hours of debugging in the wrong place.

It is also worth checking the instance state before any test: a disconnected instance does not send. The guides on the QR code not showing and on WhatsApp disconnecting on its own cover the most common cases.

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"}

Quick summary

  • Sending WhatsApp messages in C# is a POST to {instanceId}/send/text with the x-api-key and x-api-secret headers.
  • Register a typed HttpClient with AddHttpClient and keep the base URL with a trailing slash.
  • Validate numbers with the verification endpoint before campaigns.
  • Use exponential retry only for 429, 5xx and network failures, and confirm important sends through the webhook.
  • On the webhook, verify X-ZapZap-Signature over the body bytes with FixedTimeEquals and return 200 fast.

Also read

Frequently asked questions

Do I need an SDK to use the API in C#?

No. An HttpClient with System.Net.Http.Json and System.Text.Json is enough. The examples use no third-party packages (AddHttpClient comes from Microsoft.Extensions.Http, already included in ASP.NET Core).

How do I authenticate calls in .NET?

Add the x-api-key and x-api-secret headers to the registered HttpClient's DefaultRequestHeaders, reading the values from environment variables or user secrets.

How do I send an image or PDF in C#?

Use the send media endpoint with number, type (image, document and others) and file with the file's public URL. The text field works as a caption.

How do I verify the webhook signature in ASP.NET Core?

Read the body as bytes, compute the HMAC SHA-256 of t, a dot and the body with the webhook_secret and compare it with the v1 value of the X-ZapZap-Signature header using CryptographicOperations.FixedTimeEquals. Reject timestamps more than 5 minutes old.

What is the risk of using the unofficial API from my system?

Unofficial use is not covered by the WhatsApp Terms and the number can be restricted, regardless of language. Pace, consent and a clean list lower the risk but do not remove it.

Run your first C# send today

Create your account, verify your phone and get the welcome credit, no deposit needed (one per phone, subject to the current campaign). Connect by QR code and copy this guide's client.