Build messaging into your product
Send channel-neutral messages, approved WhatsApp templates, published WhatsApp Flows and typing indicators with the DevFlow Messaging SDK. Receive inbound messages, Flow submissions and delivery updates through your own webhooks.
var message = new MessageBuilder(
"Your order has shipped.",
from: phoneNumberId,
to: customerNumber)
.WithAllowedChannels(Channel.WhatsApp)
.WithReference("order-123")
.Build();
await messaging.SendMessageAsync(message);One messaging contract, built to grow
Your application creates a generic message and chooses its allowed channels. DevFlow routes it to the available provider. WhatsApp is implemented today; the envelope is deliberately independent of Meta so additional channels can be added without redesigning your integration.
Send
Text, rich content and approved templates use the same message builder.
Track
Query tenant history and receive provider delivery status updates.
Receive
Inbound customer messages are delivered to your HTTPS endpoint.
Send your first message
Install the .NET package, register the client once, then inject IDevFlowMessagingClient wherever you send messages.
Install the SDK
The current package targets .NET 8.
dotnet add package DevFlowMessaging.Sdk --version 1.0.0Register the client
Keep the API key in server-side configuration or a secret store—never in browser code.
using DevFlowMessaging.Extensions;
builder.Services
.AddDevFlowMessaging()
.WithApiKey(builder.Configuration["DevFlowMessaging:ApiKey"]!);Build and send
For WhatsApp, from is the phone number ID shown in Account → Developers and to is the customer's international number.
using DevFlowMessaging.BusinessMessaging;
using DevFlowMessaging.BusinessMessaging.Model;
using DevFlowMessaging.Interfaces;
public sealed class OrderNotifier(IDevFlowMessagingClient messaging)
{
public async Task SendAsync(
string phoneNumberId,
string customerNumber,
CancellationToken cancellationToken)
{
var message = new MessageBuilder(
"Your order has shipped.",
from: phoneNumberId,
to: customerNumber)
.WithAllowedChannels(Channel.WhatsApp)
.WithReference("order-123")
.Build();
await messaging.SendMessageAsync(message, cancellationToken);
}
}Authenticate with an API key
Create and reveal your company API key in Account → Developers.
The SDK adds it to every request as X-Api-Key. Each key is scoped to one DevFlow company.
You can regenerate or revoke the key at any time; either action invalidates the previous credential immediately.
{
"DevFlowMessaging": {
"ApiKey": "dfm_live_replace_me"
}
}Use the generic message builder
The builder creates one predictable envelope for every channel. Set fallback order with
WithAllowedChannels, correlate the send with WithReference, and attach your own string metadata when required.
Generic message, channel-specific template
Your application always sends the generic TemplateMessage. Inside it, add the approved
channel variant—in this example, WhatsAppTemplate. This boundary is intentional: approved-template
identifiers, languages and component rules belong to the channel and cannot safely be reused across every provider.
The SDK sends approved templates; it does not create or edit them. Create the template in the DevFlow portal, wait for Meta approval, then send it by name and language. Future channels can add their own variant without changing the outer message builder.
{{1}}, the second replaces {{2}}, and so on.using DevFlowMessaging.BusinessMessaging;
using DevFlowMessaging.BusinessMessaging.Model;
using DevFlowMessaging.BusinessMessaging.Model.MultiChannel;
var approvedTemplate = new TemplateMessage
{
Content = new TemplateMessageContent
{
WhatsApp = new WhatsAppTemplate
{
Name = "shopify_campaign_test",
Language = new TemplateLanguage("en_US"),
Components =
[
new TemplateComponent
{
Type = "body",
Parameters =
[
TemplateParameter.FromText("Ava"), // {{1}}
TemplateParameter.FromText("+27820000000") // {{2}}
]
}
]
}
}
};
var message = new MessageBuilder(
messageText: string.Empty,
from: phoneNumberId,
to: customerNumber)
.WithAllowedChannels(Channel.WhatsApp)
.WithTemplate(approvedTemplate)
.WithReference("order-123-template")
.Build();
await messaging.SendMessageAsync(message);{
"allowedChannels": ["WhatsApp"],
"body": {
"content": "",
"type": "auto"
},
"from": "YOUR_META_PHONE_NUMBER_ID",
"to": [
{ "number": "+27820000000" }
],
"reference": "order-123-template",
"richContent": {
"conversation": [
{
"type": "template",
"template": {
"whatsapp": {
"name": "shopify_campaign_test",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Ava" },
{ "type": "text", "text": "+27820000000" }
]
}
]
}
}
}
]
}
}Send a published Flow with optional prefill data
Create, validate and publish the Flow in the DevFlow portal. The SDK does not manage Flow definitions; it sends a published Flow by its Meta Flow ID. Open the published Flow's Developer tab to copy its Flow ID, initial screen ID, valid prefill keys and exact response properties.
content.flow.flowToken on completion.using DevFlowMessaging.BusinessMessaging;
using DevFlowMessaging.BusinessMessaging.Model;
using DevFlowMessaging.BusinessMessaging.Model.MultiChannel;
// Copy these IDs and available prefill keys from the
// Developer tab on the published Flow.
var flow = new FlowMessage(
flowId: "YOUR_META_FLOW_ID",
text: "Please confirm your details.",
callToAction: "Open Flow",
screenId: "DETAILS")
.WithFlowToken("customer-123")
.WithPrefill("full_name", "Ava Customer")
.WithPrefill("interests", new[] { "support", "sales" });
var message = new MessageBuilder(
messageText: string.Empty,
from: phoneNumberId,
to: customerNumber)
.WithAllowedChannels(Channel.WhatsApp)
.WithFlow(flow)
.WithReference("customer-123-flow")
.Build();
await messaging.SendMessageAsync(message, cancellationToken);{
"allowedChannels": ["WhatsApp"],
"body": {
"content": "",
"type": "auto"
},
"from": "YOUR_META_PHONE_NUMBER_ID",
"to": [
{ "number": "+27820000000" }
],
"reference": "customer-123-flow",
"richContent": {
"conversation": [
{
"type": "flow",
"flowId": "YOUR_META_FLOW_ID",
"text": "Please confirm your details.",
"callToAction": "Open Flow",
"screenId": "DETAILS",
"flowToken": "customer-123",
"prefillData": {
"full_name": "Ava Customer",
"interests": ["support", "sales"]
}
}
]
}
}
When the customer submits the Flow, your subscribed messages webhook receives normalized
fields under content.flow.data. The original Meta payload remains available in
providerPayload for diagnostics.
{
"eventId": "01J...",
"tenantId": "your-tenant-id",
"event": "messages",
"channel": "whatsapp",
"occurredAt": "2026-07-28T18:45:00Z",
"messageId": "wamid.submission...",
"internalMessageId": "your-internal-message-id",
"direction": "incoming",
"from": "+27820000000",
"fromUserId": "meta-scoped-user-id",
"to": "YOUR_META_PHONE_NUMBER_ID",
"type": "interactive",
"contactName": "Ava Customer",
"content": {
"kind": "flow",
"flow": {
"name": "flow",
"body": "Sent",
"flowToken": "customer-123",
"replyToMessageId": "wamid.sent-flow...",
"data": {
"full_name": "Ava Customer",
"accept_terms": true,
"interests": ["support", "sales"]
}
}
},
"providerPayload": { }
}Set a typing indicator for a message
Use the provider message ID from an inbound message, the receiving phone number ID, and the same channel.
using DevFlowMessaging.BusinessMessaging;
using DevFlowMessaging.BusinessMessaging.Model;
using DevFlowMessaging.Models;
var indicator = new MessageIndicatorBuilder(
messageId: inboundProviderMessageId,
from: phoneNumberId,
channel: Channel.WhatsApp)
.WithIndicator(MessageIndicatorType.Typing)
.Build();
await messaging.SetMessageIndicatorAsync(indicator);Retrieve messages across channels
Use GET /v1/messages/history to retrieve the authenticated company's inbound and outbound messages.
The response uses the same channel-neutral model regardless of the provider. Calling the endpoint without query
parameters returns all available company message history, newest first.
WhatsApp. Filters are combined when several are supplied.GetMessageHistoryAsync() with no query to return everything, or pass a
MessageHistoryQuery to narrow the result. The API key always limits results to its own company.
using DevFlowMessaging.BusinessMessaging.Model;
using DevFlowMessaging.Models;
// Omit the query to return all company message history.
var history = await messaging.GetMessageHistoryAsync(
new MessageHistoryQuery
{
PhoneNumber = "+27820000000",
StartDate = new DateTimeOffset(2026, 7, 1, 0, 0, 0, TimeSpan.Zero),
EndDate = new DateTimeOffset(2026, 7, 31, 23, 59, 59, TimeSpan.Zero),
Channel = Channel.WhatsApp
},
cancellationToken);
foreach (var message in history.Messages)
{
Console.WriteLine(
$"{message.CreatedAt:u} {message.Channel} " +
$"{message.Direction} {message.Status}");
}GET /v1/messages/history?phoneNumber=%2B27820000000&startDate=2026-07-01T00%3A00%3A00Z&endDate=2026-07-31T23%3A59%3A59Z&channel=WhatsApp HTTP/1.1
Host: api.devflowltd.com
X-Api-Key: dfm_live_replace_me
Accept: application/json{
"totalCount": 1,
"messages": [
{
"id": "f3b30ca9-2238-45cb-91cb-64fc0fe0e241",
"messageId": "wamid.HBgL...",
"channel": "WhatsApp",
"direction": "Outbound",
"from": "YOUR_META_PHONE_NUMBER_ID",
"to": "+27820000000",
"customerPhoneNumber": "+27820000000",
"customerUserId": "meta-scoped-user-id",
"messageType": "text",
"status": "Delivered",
"createdAt": "2026-07-16T10:30:00+00:00",
"sentAt": "2026-07-16T10:30:01+00:00",
"deliveredAt": "2026-07-16T10:30:04+00:00",
"providerPayload": { }
}
]
}Receive messages and status events
Configure one public HTTPS endpoint for inbound messages and another for delivery
status updates in Account → Developers. You can change the URL, disable delivery temporarily,
or re-enable it at any time without regenerating your API key. DevFlow sends JSON with these delivery headers:
X-DevFlow-Event-IdUnique delivery event; use this as your idempotency key.X-DevFlow-Delivery-IdStable identifier for this webhook delivery.X-DevFlow-Delivery-AttemptThe current delivery attempt, beginning at 1.X-DevFlow-Eventmessages or status.X-DevFlow-Tenant-IdThe DevFlow company that owns the event.Retry-After response of up to 24 hours.
Inbound message
{
"eventId": "01J...",
"tenantId": "your-tenant-id",
"event": "messages",
"channel": "whatsapp",
"occurredAt": "2026-07-16T10:30:00Z",
"messageId": "wamid...",
"internalMessageId": "your-internal-message-id",
"direction": "incoming",
"from": "+27820000000",
"fromUserId": "meta-scoped-user-id",
"to": "YOUR_META_PHONE_NUMBER_ID",
"type": "text",
"text": "Hello",
"contactName": "Ava Customer",
"content": {
"kind": "text",
"text": "Hello"
},
"providerPayload": { }
}Delivery status
{
"eventId": "01J...",
"tenantId": "your-tenant-id",
"event": "status",
"channel": "whatsapp",
"occurredAt": "2026-07-16T10:30:04Z",
"messageId": "wamid...",
"status": "delivered",
"recipientId": "+27820000000",
"recipientUserId": "meta-scoped-user-id",
"errorCode": null,
"errorTitle": null,
"errorDetails": null,
"providerPayload": { }
}Endpoints and responses
| Method | Endpoint | Purpose | Authentication |
|---|---|---|---|
| POST | /v1/messages | Send text, rich content, a published Flow or an approved template. | X-Api-Key |
| POST | /v1/messages/indicator | Set a typing indicator for a provider message. | X-Api-Key |
| GET | /v1/messages/history | Retrieve all messages or filter by customer, date range and channel. | X-Api-Key |
Send responses include statusMessage, statusCode and a details entry per recipient,
including the provider message ID, reference, status, destination, channel and any error.