A modern, fluent dotnet SDK for sending WhatsApp messages through
the WhatsApp Cloud API.
Built as a lightweight class library using System.Text.Json for serialization — with no external JSON dependencies.
dotnet add package FluentWhatsAppEvery message is built using the fluent WhatsAppMessage API and sent via IWhatsAppClient.
// Build a request
var request = WhatsAppMessage.To("+1234567890").Text("Hello!").Build();
// Send it
SendMessageResponse response = await client.SendAsync(request);
Console.WriteLine(response.FirstMessageId); // wamid.xxxawait client.SendToIndividualAsync("+1234567890", b =>
b.Text("Hello!").Build());
await client.SendToGroupAsync("GROUP_ID", b =>
b.Text("Hello group!").Build());Register the client in your DI container with your phone number ID and access token:
builder.Services.AddWhatsAppClient(options =>
{
options.PhoneNumberId = "YOUR_PHONE_NUMBER_ID";
options.AccessToken = "YOUR_ACCESS_TOKEN";
// options.ApiVersion = "v22.0"; // default
});Then inject IWhatsAppClient wherever you need it:
public class NotificationService(IWhatsAppClient whatsApp)
{
public Task NotifyAsync(string phone) =>
whatsApp.SendToIndividualAsync(phone, b => b.Text("Hello from FluentWhatsApp!").Build());
}The library registers a standard resilience pipeline (retry, circuit-breaker, timeouts) via
Microsoft.Extensions.Http.Resilience. Customise it through the optional second parameter:
builder.Services.AddWhatsAppClient(
options =>
{
options.PhoneNumberId = "...";
options.AccessToken = "...";
},
resilience =>
{
resilience.Retry.MaxRetryAttempts = 5;
});Bring your own IWhatsAppClient implementation (e.g., for testing):
builder.Services.AddWhatsAppClient<MyCustomClient>();var request = WhatsAppMessage
.To("+1234567890")
.Text("Hello, World!")
.Build();With URL preview:
var request = WhatsAppMessage
.To("+1234567890")
.Text("Check this out: https://example.com", previewUrl: true)
.Build();// By URL
var request = WhatsAppMessage
.To("+1234567890")
.Image(link: "https://example.com/image.jpg", caption: "Look at this!")
.Build();
// By media ID (uploaded via Media API)
var request = WhatsAppMessage
.To("+1234567890")
.Image(mediaId: "MEDIA_ID")
.Build();var request = WhatsAppMessage
.To("+1234567890")
.Audio(link: "https://example.com/audio.mp3")
.Build();var request = WhatsAppMessage
.To("+1234567890")
.Video(link: "https://example.com/video.mp4", caption: "Watch this")
.Build();var request = WhatsAppMessage
.To("+1234567890")
.Document(link: "https://example.com/file.pdf", caption: "Your invoice")
.WithFilename("invoice.pdf")
.Build();var request = WhatsAppMessage
.To("+1234567890")
.Sticker(link: "https://example.com/sticker.webp")
.Build();var request = WhatsAppMessage
.To("+1234567890")
.Location(37.7749, -122.4194)
.WithName("San Francisco")
.WithAddress("San Francisco, CA, USA")
.Build();// Returns a MessageRequest directly — no .Build() needed
var request = WhatsAppMessage
.To("+1234567890")
.Reaction("wamid.MESSAGE_ID", "👍");
await client.SendAsync(request);var request = WhatsAppMessage
.To("+1234567890")
.Contacts()
.Add(c => c
.Name("John Doe", firstName: "John", lastName: "Doe")
.Phone("+19998887777")
.Email("john@example.com"))
.Add(c => c
.Name("Jane Doe", firstName: "Jane")
.Phone("+19997776666", type: "WORK"))
.Build();Up to 3 buttons.
var request = WhatsAppMessage
.To("+1234567890")
.Interactive("Would you like to proceed?")
.WithFooter("Tap a button to respond")
.ReplyButtons()
.Add("yes", "Yes, continue")
.Add("no", "No, cancel")
.Build();var request = WhatsAppMessage
.To("+1234567890")
.Interactive("Choose a subscription plan")
.WithHeader("Our Plans")
.WithFooter("Prices are monthly")
.List("View Plans")
.Section("Monthly")
.Row("m_basic", "Basic", "$9/mo")
.Row("m_pro", "Pro", "$19/mo")
.Row("m_ultra", "Ultra", "$49/mo")
.Section("Annual")
.Row("a_basic", "Basic Annual", "$90/yr")
.Row("a_pro", "Pro Annual", "$190/yr")
.Build();var request = WhatsAppMessage
.To("+1234567890")
.Interactive("Visit our website")
.CtaUrl("Open site", "https://example.com");var request = WhatsAppMessage
.To("+1234567890")
.Interactive("Fill in your details")
.Flow("FLOW_TOKEN", "FLOW_ID", "Start");var request = WhatsAppMessage
.To("+1234567890")
.Interactive("Browse our catalog")
.Catalog();var request = WhatsAppMessage
.To("+1234567890")
.Template("order_confirmation", "en_US")
.AddBodyParameters(p => p
.Text("John")
.Text("Order #42")
.Text("$59.99"))
.AddButton(0, "url", p => p.Text("/orders/42"))
.Build();Show a "typing…" indicator before sending a message:
var request = WhatsAppMessage
.To("+1234567890")
.TypingIndicator("wamid.INCOMING_MESSAGE_ID")
.Build();
await client.SendAsync(request);Chain .ReplyTo() and/or .WithCallbackData() before the message type method to attach a reply context or a business
opaque callback string:
var request = WhatsAppMessage
.To("+1234567890")
.ReplyTo("wamid.ORIGINAL_MESSAGE_ID")
.WithCallbackData("order_ref_42")
.Text("Got your message!")
.Build();var request = WhatsAppMessage
.ToGroup("GROUP_ID")
.Text("Hello everyone!")
.Build();
await client.SendAsync(request);Or use the helper:
await client.SendToGroupAsync("GROUP_ID", b =>
b.Text("Hello everyone!").Build());When you need to send from multiple phone numbers at runtime, inject IWhatsAppClientFactory:
public class MultiTenantSender(IWhatsAppClientFactory factory)
{
public Task SendAsync(string phoneNumberId, string token, string to, string text)
{
var client = factory.Create(o =>
{
o.PhoneNumberId = phoneNumberId;
o.AccessToken = token;
});
return client.SendAsync(WhatsAppMessage.To(to).Text(text).Build());
}
}Register the factory-only overload (no static options required):
builder.Services.AddWhatsAppClient();Failed API calls throw WhatsAppApiException:
try
{
await client.SendAsync(request);
}
catch (WhatsAppApiException ex)
{
Console.WriteLine($"Code: {ex.ErrorCode}");
Console.WriteLine($"Type: {ex.ErrorType}");
Console.WriteLine($"Trace: {ex.FbTraceId}");
Console.WriteLine($"Message: {ex.Message}");
}SendMessageResponse exposes the sent message ID and contact info returned by the API:
var response = await client.SendAsync(request);
if (response.IsSuccessful)
Console.WriteLine($"Sent: {response.FirstMessageId}");MIT