C# integration
Send WhatsApp messages with C# / .NET
A complete, runnable C# / .NET example for sending WhatsApp messages through the SPWP REST API. There is no SDK to install — C# / .NET talks to the API over ordinary HTTP, so you can paste this straight into an existing project.
About 5 minutes to your first delivered message
1. Setup
Create an API key in the dashboard under Accounts → your account → API keys, then make it available to your C# / .NET code as an environment variable. Never commit the key or ship it in client-side code.
# HttpClient and System.Text.Json ship with .NET — nothing to add.
dotnet user-secrets set "Spwp:ApiKey" "wak_live_your_key_here"2. Send a text message
One POST with a recipient and a body. The API replies 202 Accepted with a message id in QUEUED state; delivery happens asynchronously and you can track it by id or webhook.
using System.Net.Http.Json;
public sealed class SpwpClient
{
private const string ApiBase = "https://api.spwp.app/api/v1";
private readonly HttpClient _http;
public SpwpClient(HttpClient http, string apiKey)
{
_http = http;
_http.DefaultRequestHeaders.Add("X-API-Key", apiKey);
_http.Timeout = TimeSpan.FromSeconds(10);
}
public record Message(string Id, string Status);
private record Envelope(Message Data);
public async Task<Message> SendTextAsync(string to, string text)
{
var response = await _http.PostAsJsonAsync(
$"{ApiBase}/public/messages/text",
new { to, text });
response.EnsureSuccessStatusCode();
var envelope = await response.Content.ReadFromJsonAsync<Envelope>();
return envelope!.Data;
}
}3. Send an image, document or other media
Media endpoints take the same shape, with a source that is either a public HTTPS URL we fetch server-side or a base64 data URL you upload inline. Swap the path segment for video, audio, document or location.
public async Task<Message> SendImageAsync(string to, string source, string? caption = null)
{
var response = await _http.PostAsJsonAsync(
$"{ApiBase}/public/messages/image",
new { to, source, caption });
response.EnsureSuccessStatusCode();
var envelope = await response.Content.ReadFromJsonAsync<Envelope>();
return envelope!.Data;
}4. Receive replies and delivery receipts
Inbound messages and status changes arrive as signed POSTs to your webhook URL. Recompute the HMAC over the timestamp and the raw request body — re-serialised JSON will not match — and reject anything older than five minutes.
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
public static class SpwpSignature
{
public static bool IsValid(string rawBody, string timestamp,
string signatureHeader, string secret)
{
var v1 = Regex.Match(signatureHeader, "v1=([a-f0-9]+)").Groups[1].Value;
if (string.IsNullOrEmpty(v1)) return false;
// Replay protection: five-minute window.
var sent = long.Parse(timestamp);
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (Math.Abs(now - sent) > 5 * 60_000) return false;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var digest = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}"));
var expected = Convert.ToHexString(digest).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(v1));
}
}Error handling
Errors carry a non-2xx status and a stable code string you can branch on: VALIDATION_FAILED, INVALID_API_KEY, ACCOUNT_NOT_CONNECTED, RATE_LIMITED, MEDIA_FETCH_FAILED. Retry on 429 while honouring Retry-After; treat 401 as a configuration problem rather than a transient one.
Next steps
The full reference covers every endpoint, field and error code, plus the webhook payload shapes.