All integration guides

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.

Other languages and frameworks