Sending WhatsApp Messages from .NET/C# with the textmeflow API
Most WhatsApp API guides assume PHP or Node. If your stack is .NET, the integration is just as simple — it's plain JSON over HTTPS — but a few C#-specific details (typed HttpClient, async webhook signature checks, retry handling) are worth getting right from the start. This walks through sending a message, reading its status, and verifying inbound replies from an ASP.NET Core app.
Sending a message with HttpClient
Register a named HttpClient for textmeflow in your DI container so the base address and auth header are set once, not on every call:
// Program.cs
builder.Services.AddHttpClient("textmeflow", client =>
{
client.BaseAddress = new Uri("https://api.textmeflow.eu");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", builder.Configuration["TextMeFlow:ApiKey"]);
});
Keep the API key in configuration (appsettings.json with user secrets locally, an environment variable or key vault in production) — never hard-code it.
Sending a message is one POST to /v1/messages:
public record SendMessageRequest(string To, string Text, string? MediaUrl = null, bool Urgent = false);
public record SendMessageResponse(string MessageId, string Status);
public class TextMeFlowClient
{
private readonly HttpClient _http;
public TextMeFlowClient(IHttpClientFactory factory) => _http = factory.CreateClient("textmeflow");
public async Task<SendMessageResponse?> SendAsync(SendMessageRequest request, CancellationToken ct = default)
{
var response = await _http.PostAsJsonAsync("/v1/messages", request, ct);
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
// rate limit or monthly quota hit — see handling below
throw new TextMeFlowRateLimitException();
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SendMessageResponse>(cancellationToken: ct);
}
}
to must be E.164 (+32472932208, with the leading +) — a number without the country code is the single most common integration bug, regardless of language. A successful call returns 202 Accepted with a message_id and status: queued, not a final delivery state: WhatsApp delivery happens asynchronously, so if your workflow needs to know when a message actually lands, poll GET /v1/messages/{id} or — better — listen for the delivery webhook instead of polling in a loop.
Handling 429s without losing the message
A 429 means either the per-number rate limit (1 msg/sec, 60/min, 1,000/hour) or the plan's monthly quota was hit. Those two cases need different handling: a rate limit is temporary and worth a retry with backoff; a quota exhaustion will keep failing until the billing period resets or you upgrade, so retrying in a tight loop just burns time. A simple Polly retry policy covers the rate-limit case cleanly:
var retryPolicy = Policy
.Handle<TextMeFlowRateLimitException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
Three retries with exponential backoff is enough to ride out a short burst without risking a duplicate send loop. If you're queueing many messages in bulk (reminders, order updates), push them through a background queue — IHostedService with Channel<T>, or Hangfire/Quartz.NET if you already run one — rather than firing them all from a single request handler.
Verifying inbound webhooks
Customer replies arrive as a POST to whatever endpoint you configure in /admin/webhook, signed with HMAC-SHA256 over the raw request body in an X-TextMeFlow-Signature: sha256=<hex> header. In ASP.NET Core, the request body stream is consumed by model binding before a typical action method runs, so read it manually in a minimal API or a dedicated middleware, before anything deserializes it:
app.MapPost("/webhooks/textmeflow", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var raw = await reader.ReadToEndAsync();
var secret = Encoding.UTF8.GetBytes(builder.Configuration["TextMeFlow:WebhookSecret"]!);
var computed = "sha256=" + Convert.ToHexString(
new HMACSHA256(secret).ComputeHash(Encoding.UTF8.GetBytes(raw))).ToLowerInvariant();
var received = request.Headers["X-TextMeFlow-Signature"].ToString();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(computed), Encoding.UTF8.GetBytes(received)))
{
return Results.Unauthorized();
}
var payload = JsonSerializer.Deserialize<WebhookPayload>(raw);
// store payload, dedupe on X-TextMeFlow-Event-Id, respond 200 fast, process after
return Results.Ok();
});
CryptographicOperations.FixedTimeEquals is .NET's constant-time comparison — use it instead of == or SequenceEqual, which both short-circuit on the first mismatched byte and leak timing information. Respond within 10 seconds or textmeflow treats it as a failed delivery and retries up to 5 times; store the event and do any slow work (CRM update, order lookup) after returning 200, not before. The full header and retry reference is in the webhook docs, and the signature mechanics are covered in more depth in verifying WhatsApp webhook signatures if you want the PHP/Python/Node equivalents side by side.
A minimal end-to-end example
Putting it together, a console app that sends a confirmation and logs replies needs three things: an HttpClient registration, the SendAsync call above, and a small ASP.NET Core app (or Azure Function) exposing the webhook endpoint. There's no SDK to install — System.Net.Http.Json and System.Security.Cryptography from the base class library cover everything.
Try it
The free plan includes 50 messages a month, forever — enough to wire up this flow end to end and watch a real reply come back through your webhook before you commit to anything. Create a free account and send your first message from C# in a few minutes.
Zelf WhatsApp-berichten versturen via API?
Gratis voor altijd tot 50 berichten/maand. QR scannen en binnen 5 minuten verstuur je je eerste bericht.
Gratis voor altijd