---
title: "API do WhatsApp em C# (.NET): enviar mensagens e receber webhook"
description: "Tutorial de API do WhatsApp em C# e .NET: HttpClient tipado, envio de texto, imagem e lista, webhook em ASP.NET Core com fila em background e tratamento de 429."
url: "https://wame.api.br/blog/api-whatsapp-csharp-dotnet"
language: "pt-BR"
og:type: "article"
published_time: "2026-09-28"
modified_time: "2026-09-28"
author: "Raphael Serafim"
keywords: "api whatsapp não oficial, api não oficial whatsapp, api whatsapp c#, api whatsapp .net, whatsapp asp.net core webhook, enviar mensagem whatsapp c#, httpclient whatsapp api"
reading_time: "10 min de leitura"
---

# API do WhatsApp em C# (.NET): enviar mensagens e receber webhook

> Tutorial de API do WhatsApp em C# e .NET: HttpClient tipado, envio de texto, imagem e lista, webhook em ASP.NET Core com fila em background e tratamento de 429.

**Para usar a API do WhatsApp em C# você não precisa de SDK: um `HttpClient` tipado envia mensagens para `https://server.wame.api.br/SUA_KEY/...` com JSON, e uma minimal API do ASP.NET Core recebe o webhook no envelope da Cloud API.** Neste tutorial você monta as duas pontas com .NET moderno — records, `System.Text.Json`, `IHttpClientFactory`, fila com `Channel<T>` e `BackgroundService` — usando a API não oficial da WAME, que conecta seu número por QR Code ou código de pareamento, sem aprovação na Meta.

> A camada não oficial não é afiliada, endossada ou suportada pelo WhatsApp ou pela Meta. O uso é de responsabilidade de quem envia.

## O que você precisa

- .NET 8 ou mais novo.
- Uma instância na WAME conectada ao seu número (veja [como conectar sem QR Code](/blog/conectar-whatsapp-codigo-pareamento-sem-qr-code) se preferir código de pareamento).
- Uma URL pública para o webhook. Em desenvolvimento, um túnel como ngrok resolve.

A autenticação é a própria `key` na URL. Guarde-a em configuração, nunca no código:

```json
{
  "Wame": {
    "BaseUrl": "https://server.wame.api.br/",
    "Key": "SUA_KEY"
  }
}
```

## Um cliente tipado com HttpClient

O erro clássico em .NET é criar `new HttpClient()` a cada envio e esgotar sockets. Com `IHttpClientFactory` e um cliente tipado, o ciclo de vida fica por conta do framework:

```csharp
using System.Net;
using System.Net.Http.Json;

public sealed class WameOptions
{
    public string BaseUrl { get; init; } = "https://server.wame.api.br/";
    public string Key { get; init; } = "";
}

public sealed class WameClient(HttpClient http)
{
    public Task SendTextAsync(string to, string text, CancellationToken ct = default) =>
        PostAsync("message/text", new { to, text }, ct);

    public Task SendImageAsync(string to, string url, string? caption = null, CancellationToken ct = default) =>
        PostAsync("message/image", new { to, url, caption }, ct);

    public Task SendListAsync(string to, string text, string buttonText,
        IEnumerable<ListSection> sections, CancellationToken ct = default) =>
        PostAsync("message/list", new { to, text, buttonText, sections }, ct);

    public Task SetTypingAsync(string to, CancellationToken ct = default) =>
        PostAsync("message/presence", new { to, status = "composing" }, ct);

    private async Task PostAsync(string path, object body, CancellationToken ct)
    {
        using var resp = await http.PostAsJsonAsync(path, body, ct);

        if (resp.StatusCode == HttpStatusCode.TooManyRequests)
            throw new WameRateLimitException(await resp.Content.ReadAsStringAsync(ct));

        resp.EnsureSuccessStatusCode();
    }
}

public sealed record ListRow(string Title, string RowId, string? Description = null);
public sealed record ListSection(string Title, IReadOnlyList<ListRow> Rows);

public sealed class WameRateLimitException(string message) : Exception(message);
```

Dois detalhes que evitam dor de cabeça:

- **`BaseAddress` precisa terminar com a key e uma barra.** Sem a barra final, o `HttpClient` descarta o último segmento ao combinar com `message/text`.
- **O `System.Text.Json` com `PostAsJsonAsync` usa camelCase por padrão**, então `RowId` vira `rowId` e `ButtonText` vira `buttonText` — exatamente o que a API espera.

O registro no `Program.cs`:

```csharp
var builder = WebApplication.CreateBuilder(args);

var wame = builder.Configuration.GetSection("Wame").Get<WameOptions>()!;

builder.Services.AddHttpClient<WameClient>(c =>
{
    c.BaseAddress = new Uri($"{wame.BaseUrl}{wame.Key}/");
    c.Timeout = TimeSpan.FromSeconds(30);
});
```

## Enviar texto, imagem e lista

Com o cliente injetado, o envio fica em uma linha. O número vai sempre no formato internacional, só dígitos: código do país, DDD e número.

```csharp
await wame.SendTextAsync("5511999999999", "Olá! Seu pedido foi confirmado.");

await wame.SendImageAsync("5511999999999",
    "https://exemplo.com/comprovante.png", "Seu comprovante");

await wame.SendListAsync("5511999999999",
    text: "Como posso ajudar?",
    buttonText: "Ver opções",
    sections:
    [
        new ListSection("Atendimento",
        [
            new ListRow("Segunda via de boleto", "boleto"),
            new ListRow("Falar com atendente", "humano", "Horário comercial")
        ])
    ]);
```

Uma observação sobre listas e botões: em conversa **fria** — contato que nunca trocou mensagem com o seu número — o WhatsApp do destinatário pode não exibir a mensagem interativa. Abra com um texto e mande o menu em seguida. O motivo está em [como aquecer um número](/blog/aquecer-numero-whatsapp-api-nao-oficial).

Todos os envios da camada não oficial já saem com comportamento humano (online, "digitando…" proporcional ao texto, envio). O `SetTypingAsync` acima é útil quando o seu processamento demora — uma consulta ao ERP, uma chamada de IA — e você quer sinalizar ao cliente que a resposta está vindo.

## Receber o webhook no formato meta

Configure o webhook da instância apontando para a sua aplicação, no formato `meta`:

```bash
curl -X PUT "https://server.wame.api.br/SUA_KEY/instance" \
  -H "Content-Type: application/json" \
  -d '{
    "allowWebhook": true,
    "allowNumber": "all",
    "webhookMessage": "https://seu-dominio.com/webhook/wame",
    "webhookConnection": "https://seu-dominio.com/webhook/wame",
    "webhookFormat": "meta"
  }'
```

Cada mensagem chega no envelope padrão da Cloud API. Records modelam só o que interessa — o resto é ignorado na desserialização:

```csharp
using System.Text.Json.Serialization;

public sealed record WebhookEnvelope(string? Provider, List<Entry>? Entry);
public sealed record Entry(string? Id, List<Change>? Changes);
public sealed record Change(string? Field, ChangeValue? Value);
public sealed record ChangeValue(List<IncomingMessage>? Messages);

public sealed record IncomingMessage(
    string From,
    string Id,
    string Type,
    TextBody? Text,
    Interactive? Interactive,
    [property: JsonPropertyName("group_id")] string? GroupId,
    [property: JsonPropertyName("from_me")] bool FromMe = false);

public sealed record TextBody(string Body);

public sealed record Interactive(
    string Type,
    [property: JsonPropertyName("list_reply")] ReplyId? ListReply,
    [property: JsonPropertyName("button_reply")] ReplyId? ButtonReply);

public sealed record ReplyId(string Id, string? Title);
```

Repare em três campos:

- **`group_id`** só aparece quando a mensagem veio de um grupo; aí `from` é quem enviou dentro dele.
- **`from_me`** marca o eco das mensagens enviadas pelo próprio número. Ignore, ou o bot responde a si mesmo.
- **`interactive.list_reply.id`** traz o `rowId` que você definiu na lista.

## Responder 200 na hora e processar em background

O endpoint do webhook não pode esperar o processamento. Webhook lento é reenviado, e o cliente recebe a mesma resposta duas vezes. A solução idiomática em .NET é um `Channel<T>` como fila em memória e um `BackgroundService` consumindo:

```csharp
using System.Threading.Channels;

builder.Services.AddSingleton(Channel.CreateBounded<IncomingMessage>(
    new BoundedChannelOptions(1_000) { FullMode = BoundedChannelFullMode.Wait }));
builder.Services.AddHostedService<MessageWorker>();

var app = builder.Build();

app.MapPost("/webhook/wame", async (WebhookEnvelope env, Channel<IncomingMessage> queue) =>
{
    var messages = env.Entry?
        .SelectMany(e => e.Changes ?? [])
        .Where(c => c.Field == "messages")
        .SelectMany(c => c.Value?.Messages ?? [])
        .Where(m => !m.FromMe) ?? [];

    foreach (var m in messages)
        await queue.Writer.WriteAsync(m);

    return Results.Ok();
});

app.Run();
```

E o worker, que decide o que responder:

```csharp
public sealed class MessageWorker(
    Channel<IncomingMessage> queue,
    WameClient wame,
    ILogger<MessageWorker> log) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        await foreach (var msg in queue.Reader.ReadAllAsync(ct))
        {
            try
            {
                await HandleAsync(msg, ct);
            }
            catch (WameRateLimitException ex)
            {
                log.LogWarning("429 ao responder {From}: {Msg}", msg.From, ex.Message);
                await Task.Delay(TimeSpan.FromSeconds(30), ct);
            }
            catch (Exception ex)
            {
                log.LogError(ex, "Falha ao processar {Id}", msg.Id);
            }
        }
    }

    private Task HandleAsync(IncomingMessage msg, CancellationToken ct)
    {
        if (msg.GroupId is not null) return Task.CompletedTask; // este bot só atende 1:1

        var escolha = msg.Interactive?.ListReply?.Id;
        return escolha switch
        {
            "boleto" => wame.SendTextAsync(msg.From, "Vou gerar sua segunda via.", ct),
            "humano" => wame.SendTextAsync(msg.From, "Já chamo alguém do time.", ct),
            _ when msg.Type == "text" => wame.SendTextAsync(msg.From,
                $"Recebi: {msg.Text?.Body}", ct),
            _ => Task.CompletedTask
        };
    }
}
```

O `WameClient` é registrado como transient pelo `AddHttpClient`, e o `BackgroundService` é singleton. Funciona porque o cliente tipado só segura um `HttpClient` vindo da factory; se o seu worker precisar de serviços scoped (um `DbContext`, por exemplo), injete `IServiceScopeFactory` e crie um escopo por mensagem.

## Idempotência: o mesmo evento pode chegar duas vezes

Mesmo respondendo rápido, reentrega acontece — rede instável, deploy no meio do caminho. Guarde o `id` de cada mensagem processada e descarte repetições. Em produção, um `SETNX` no Redis com expiração de algumas horas resolve; em teste, um `ConcurrentDictionary` serve. O padrão completo, com assinatura e retry, está em [webhook em produção](/blog/webhook-producao-assinatura-retry-idempotencia).

## Tratando o 429 do jeito certo

O `429` da WAME não é instabilidade: é um freio deliberado. Ele aparece quando a instância fala com **números novos demais em pouco tempo** ou manda **o mesmo texto para números demais** em poucos minutos — o desenho clássico de disparo em massa. Por isso, a política certa não é retry imediato com Polly em loop:

- **Espere e espalhe.** Reduza o ritmo da fila; não reenvie a mesma mensagem na hora.
- **Personalize.** Texto idêntico para muita gente é exatamente o que o freio procura.
- **Revise o fluxo.** Se um bot de atendimento está batendo em 429, algo está errado — um loop, um eco de `from_me` não filtrado, uma campanha disfarçada.

A WAME não apoia spam. Esses freios existem para que atendimento, notificação e bots legítimos rodem com taxa de bloqueio muito baixa para quem usa do jeito certo. As regras estão em [uso responsável da API não oficial](/blog/uso-responsavel-api-whatsapp-nao-oficial-spam).

## Saúde do número no mesmo webhook

Como o webhook de conexão aponta para o mesmo endpoint, você também recebe o evento de saúde da instância (`field: "health"`), com o estado atual, o anterior e um indicador `should_pause`. Vale acrescentar um `Change` com esse campo ao modelo e, quando `should_pause` vier verdadeiro, parar de escrever na fila de envios ativos. Os sinais e o que fazer com cada um estão em [sinais de que o número está em risco](/blog/sinais-numero-whatsapp-risco-bloqueio).

## Um código para oficial e não oficial

Se você imagina migrar para a API oficial um dia — ou rodar as duas —, existe um atalho: o endpoint `POST /{key}/message` aceita exatamente o corpo da WhatsApp Cloud API. Combinado com o webhook `meta`, a mesma camada C# atende os dois tipos de instância. O desenho está em [um código só para a API oficial e a não oficial](/blog/mesmo-codigo-api-oficial-e-nao-oficial-whatsapp).

## Conclusão

Em .NET, integrar WhatsApp é HTTP e JSON: um `HttpClient` tipado registrado com `IHttpClientFactory` para enviar, records do `System.Text.Json` para ler o envelope da Cloud API, uma minimal API que responde 200 na hora e um `BackgroundService` com `Channel<T>` para processar. Somando idempotência pelo `id` da mensagem e respeito ao `429`, você tem uma integração de produção sem SDK e sem self-host. Se preferir outra linguagem, há o [tutorial em Python](/blog/api-whatsapp-python-nao-oficial) e o [SDK JavaScript](/blog/sdk-javascript-typescript-whatsapp); a referência completa dos endpoints está na [documentação](/docs).

## Perguntas frequentes

### Existe SDK oficial da WAME para C#?

Não é preciso. A API é REST pura: a instância é identificada por uma key na própria URL (https://server.wame.api.br/SUA_KEY/...) e o corpo é JSON. Com HttpClient e System.Text.Json você cobre texto, mídia, listas, grupos e o webhook sem dependência extra.

### Como recebo mensagens do WhatsApp numa aplicação ASP.NET Core?

Configure o webhook da instância com PUT /{key}/instance apontando para um endpoint público da sua aplicação e escolha webhookFormat meta. Cada mensagem chega no envelope da Cloud API (entry, changes, value.messages). Responda 200 na hora e processe numa fila em background.

### Por que responder 200 antes de processar o webhook?

Webhook que demora a responder é reenviado. Se você chama uma IA, um banco ou outra API antes de responder, o mesmo evento pode chegar duas ou três vezes e o cliente recebe respostas duplicadas. Enfileire com Channel<T> e processe num BackgroundService.

### O que significa o erro 429 da API?

É um freio de envio: a instância falou com números novos demais em pouco tempo ou mandou o mesmo texto para números demais em poucos minutos. Não repita na hora; espere, espalhe os envios na fila e revise se o fluxo não está parecendo disparo em massa.

### Posso usar o mesmo código C# com a API oficial?

Sim, se você usar o endpoint POST /{key}/message, que aceita exatamente o corpo da WhatsApp Cloud API, e o webhook no formato meta. Assim a mesma camada de envio serve instâncias oficiais e não oficiais, mudando só a key.
