.NET SDK 1.0 REST API v1 WhatsApp ready NuGet · August 2026

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.

Program.cs SDK connected
var message = new MessageBuilder(
    "Your order has shipped.",
    from: phoneNumberId,
    to: customerNumber)
    .WithAllowedChannels(Channel.WhatsApp)
    .WithReference("order-123")
    .Build();

await messaging.SendMessageAsync(message);
API base URLhttps://api.devflowltd.com
AuthenticationX-Api-Key header
Message designChannel-neutral envelope
Overview

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.

POST /v1/messages

Track

Query tenant history and receive provider delivery status updates.

GET /v1/messages/history

Receive

Inbound customer messages are delivered to your HTTPS endpoint.

messages webhook
Quick start

Send your first message

Install the .NET package, register the client once, then inject IDevFlowMessagingClient wherever you send messages.

Public SDK availability: August 2026 The DevFlowMessaging.Sdk NuGet package is not publicly available yet. It will become available later in August 2026; the examples below can be used to prepare your integration in advance.
1

Install the SDK

The current package targets .NET 8.

Terminal
dotnet add package DevFlowMessaging.Sdk --version 1.0.0
2

Register the client

Keep the API key in server-side configuration or a secret store—never in browser code.

Program.cs
using DevFlowMessaging.Extensions;

builder.Services
    .AddDevFlowMessaging()
    .WithApiKey(builder.Configuration["DevFlowMessaging:ApiKey"]!);
3

Build and send

For WhatsApp, from is the phone number ID shown in Account → Developers and to is the customer's international number.

OrderNotifier.cs
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);
    }
}
Authentication

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.

Treat your API key like a password Store it in a secret manager, restrict access to server-side workloads and revoke it immediately if exposed.
appsettings.json
{
  "DevFlowMessaging": {
    "ApiKey": "dfm_live_replace_me"
  }
}
Messages

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.

fromChannel-specific sender address. For WhatsApp, use the phone number ID—not the display number.
toOne or more recipient addresses. Use the complete international phone number for WhatsApp.
allowedChannelsChannels in routing/fallback order. WhatsApp is currently supported.
referenceYour correlation value for logs, support and downstream processing.
Text

Send a normal text message

Put the text in the generic body. No provider-specific rich content is required.

.NET SDK
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);
    }
}
REST API · JSON
{
  "allowedChannels": ["WhatsApp"],
  "body": {
    "content": "Your order has shipped.",
    "type": "auto"
  },
  "from": "YOUR_META_PHONE_NUMBER_ID",
  "to": [
    { "number": "+27820000000" }
  ],
  "reference": "order-123"
}
Location

Share a location

Add a channel-neutral LocationMessage with coordinates and optional customer-facing place details.

.NET SDK
using DevFlowMessaging.BusinessMessaging;
using DevFlowMessaging.BusinessMessaging.Model;
using DevFlowMessaging.BusinessMessaging.Model.MultiChannel;

var location = new LocationMessage(
    latitude: -26.2041,
    longitude: 28.0473,
    name: "DevFlow Johannesburg",
    address: "Johannesburg, South Africa");

var message = new MessageBuilder(
        messageText: string.Empty,
        from: phoneNumberId,
        to: customerNumber)
    .WithAllowedChannels(Channel.WhatsApp)
    .WithRichMessage(location)
    .WithReference("location-123")
    .Build();

await messaging.SendMessageAsync(message);
REST API · JSON
{
  "allowedChannels": ["WhatsApp"],
  "body": {
    "content": "",
    "type": "auto"
  },
  "from": "YOUR_META_PHONE_NUMBER_ID",
  "to": [
    { "number": "+27820000000" }
  ],
  "reference": "location-123",
  "richContent": {
    "conversation": [
      {
        "type": "location",
        "latitude": -26.2041,
        "longitude": 28.0473,
        "name": "DevFlow Johannesburg",
        "address": "Johannesburg, South Africa"
      }
    ]
  }
}
Image and media

Send an image, document, video or audio file

Use MediaMessage with a stable HTTPS media URL, MIME type, optional filename and caption.

.NET SDK
using DevFlowMessaging.BusinessMessaging;
using DevFlowMessaging.BusinessMessaging.Model;
using DevFlowMessaging.BusinessMessaging.Model.MultiChannel;

var image = new MediaMessage(
    type: MediaType.Image,
    mediaUri: "https://cdn.example.com/orders/order-123.jpg",
    mimeType: "image/jpeg",
    mediaName: "order-123.jpg")
{
    Caption = "Your order is ready."
};

var message = new MessageBuilder(
        messageText: string.Empty,
        from: phoneNumberId,
        to: customerNumber)
    .WithAllowedChannels(Channel.WhatsApp)
    .WithRichMessage(image)
    .WithReference("media-123")
    .Build();

await messaging.SendMessageAsync(message);
REST API · JSON
{
  "allowedChannels": ["WhatsApp"],
  "body": {
    "content": "",
    "type": "auto"
  },
  "from": "YOUR_META_PHONE_NUMBER_ID",
  "to": [
    { "number": "+27820000000" }
  ],
  "reference": "media-123",
  "richContent": {
    "conversation": [
      {
        "type": "media",
        "mediaType": "Image",
        "media": {
          "mediaName": "order-123.jpg",
          "mediaUri": "https://cdn.example.com/orders/order-123.jpg",
          "mimeType": "image/jpeg"
        },
        "caption": "Your order is ready."
      }
    ]
  }
}
Templates

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.

TemplateMessageGeneric rich-message type used by the SDK
TemplateMessageContentContainer for one or more channel variants
WhatsAppTemplateMeta-approved name, language and components

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.

Parameter order matters The first body parameter replaces {{1}}, the second replaces {{2}}, and so on.
.NET SDK
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);
REST API · JSON
{
  "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" }
                ]
              }
            ]
          }
        }
      }
    ]
  }
}
WhatsApp Flows

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.

Direct Flow messages use the customer-service window A customer must have messaged your WhatsApp number within the active service window before you send a non-template Flow message. To initiate a conversation outside that window, use an approved WhatsApp template containing a Flow button.
flowIdThe Meta Flow ID of a Flow published in the same WhatsApp Business Account as the sender.
screenIdThe screen to open. Supply it whenever prefill data is included.
flowTokenYour correlation value. DevFlow returns it as content.flow.flowToken on completion.
prefillDataOptional values keyed only by properties configured for prefill in the Flow builder.
DevFlow validates the published definition before sending The Flow must be published, belong to your company and WhatsApp Business Account, and every prefill key must be available from the selected start screen onward.
.NET SDK
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);
REST API · JSON
{
  "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.

messages webhook · Flow completion
{
  "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": { }
}
Indicators

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.

Typing indicator
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);
History

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.

phoneNumberOptional customer phone number. For WhatsApp, use the complete international number.
startDateOptional inclusive start of the creation-time range, supplied as an ISO 8601 timestamp.
endDateOptional inclusive end of the creation-time range, supplied as an ISO 8601 timestamp.
channelOptional channel name such as WhatsApp. Filters are combined when several are supplied.
All filters are optional Use 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.
.NET SDK
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}");
}
REST API · GET
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
Response · 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": { }
    }
  ]
}
Webhooks

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.
Webhook security and retries Delivery headers identify the event and tenant; they are not a secret credential. Keep endpoints on HTTPS, validate the expected tenant ID and process events idempotently. Return any 2xx response only after you have safely accepted the event. Network errors and non-2xx responses are stored in a durable delivery ledger and retried automatically. By default, DevFlow makes up to 14 attempts over roughly 28 hours and honours a longer Retry-After response of up to 24 hours.

Inbound message

messages
{
  "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

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": { }
}
WhatsApp Flow

Completed Flow submission

Flow answers are normalized under content.flow.data. Use flowToken for your correlation and replyToMessageId to identify the outbound Flow message.

messages
{
  "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": { }
}
API reference

Endpoints and responses

MethodEndpointPurposeAuthentication
POST/v1/messagesSend text, rich content, a published Flow or an approved template.X-Api-Key
POST/v1/messages/indicatorSet a typing indicator for a provider message.X-Api-Key
GET/v1/messages/historyRetrieve 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.

Ready to connect?

Generate an API key and register your webhook URLs.

Open developer settings