C# / .NET SDK

Sending messages from ASP.NET Core with the Sent C# SDK

This guide shows you how to wire Sent messaging into an existing ASP.NET Core app: install the C# SDK, register the client with dependency injection, send a template message from an endpoint, receive delivery webhooks, and verify the whole loop in sandbox mode.

Prerequisites

This guide assumes a working ASP.NET Core 8 app using minimal APIs or controllers. You also need:

Install the SDK

Add the package to your existing project:

dotnet add package Sentdm

Register the client with dependency injection

Set your credentials as environment variables so they stay out of code; the webhook secret arrives in step 4:

export SENT_DM_API_KEY="your-api-key"
export SENT_DM_WEBHOOK_SECRET="whsec_your_signing_secret"

Register one shared client. new SentClient() reads SENT_DM_API_KEY, and ISentClient (shipped with the SDK) is the seam your services and tests depend on:

// Program.cs (excerpt)
using Sentdm;

var builder = WebApplication.CreateBuilder(args);

var sentClient = new SentClient();
builder.Services.AddSingleton(typeof(ISentClient), sentClient);

var app = builder.Build();

Send a template message from an endpoint

Define a request record; the pass-through sandbox flag lets callers exercise the endpoint without delivering anything:

// Messages/SendMessageBody.cs
public record SendMessageBody(
    string PhoneNumber,                        // E.164 format, for example +14155551234
    string TemplateName,                       // reference by Name or ID, never both
    Dictionary<string, string>? Parameters,
    List<string>? Channels,                    // omit to let Sent pick per recipient
    bool Sandbox = false);                     // true = validate and simulate only

Map the endpoint that calls Messages.Send:

// Program.cs (excerpt)
using Sentdm.Models.Messages;

app.MapPost("/api/messages/send", async (SendMessageBody body, ISentClient client, CancellationToken ct) =>
{
    var parameters = new MessageSendParams
    {
        To = new List<string> { body.PhoneNumber },
        Channel = body.Channels,
        Template = new Sentdm.Models.Messages.Template
        {
            Name = body.TemplateName,
            Parameters = body.Parameters ?? new Dictionary<string, string>(),
        },
        Sandbox = body.Sandbox,
    };

    var result = await client.Messages.Send(parameters, ct);
    if (result.Success != true)
    {
        return Results.BadRequest(new { error = result.Error?.Message });
    }

    var recipient = result.Data?.Recipients?.FirstOrDefault();
    return Results.Accepted(value: new
    {
        message_id = recipient?.MessageID,
        status = result.Data?.Status,
    });
});

Sent accepts sends asynchronously: the API responds with status QUEUED and one message_id per recipient-and-channel pair. Store the message_id. Delivery outcomes arrive on your webhook endpoint instead of in this response.

Receive delivery webhooks

Add a verification helper that checks the X-Webhook-Signature header against the raw request body before your handler trusts any event. The scheme is HMAC-SHA256 over {X-Webhook-ID}.{X-Webhook-Timestamp}.{rawBody}, keyed with the base64-decoded secret after stripping its whsec_ prefix. Refer to webhook signature verification for the full scheme:

// Webhooks/WebhookSignature.cs
using System.Security.Cryptography;
using System.Text;

public static class WebhookSignature
{
    // Signed content = "{webhookId}.{timestamp}.{rawBody}"; signature format = "v1,{base64(hmac)}"
    public static bool Verify(string rawBody, string webhookId, string timestamp, string signature, string? secret)
    {
        // Fail closed: never accept webhooks when the secret is not configured
        if (string.IsNullOrEmpty(secret) || string.IsNullOrEmpty(signature))
        {
            return false;
        }

        // Strip the "whsec_" prefix and base64-decode to get the raw HMAC key
        var keyBase64 = secret.StartsWith("whsec_") ? secret["whsec_".Length..] : secret;
        var keyBytes = Convert.FromBase64String(keyBase64);

        var signed = $"{webhookId}.{timestamp}.{rawBody}";
        using var hmac = new HMACSHA256(keyBytes);
        var expected = "v1," + Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(signed)));

        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected),
            Encoding.UTF8.GetBytes(signature));
    }
}

Map the webhook endpoint. Every event arrives in the same envelope (field, event, timestamp, payload), so one handler routes all of them; return 200 quickly and do slow work elsewhere:

// Program.cs (excerpt)
using System.Text.Json.Nodes;

app.MapPost("/webhooks/sent", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    var rawBody = await reader.ReadToEndAsync();

    var webhookId = request.Headers["X-Webhook-ID"].ToString();
    var timestamp = request.Headers["X-Webhook-Timestamp"].ToString();
    var signature = request.Headers["X-Webhook-Signature"].ToString();
    var secret = Environment.GetEnvironmentVariable("SENT_DM_WEBHOOK_SECRET");

    if (!WebhookSignature.Verify(rawBody, webhookId, timestamp, signature, secret))
    {
        return Results.Unauthorized();
    }
    // Reject replayed events older than 5 minutes
    if (!long.TryParse(timestamp, out var ts) ||
        Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300)
    {
        return Results.Unauthorized();
    }

    var evt = JsonNode.Parse(rawBody);
    var payload = evt?["payload"];

    if ((string?)evt?["field"] == "message")
    {
        var messageId = (string?)payload?["message_id"];
        var status = (string?)payload?["message_status"];
        switch ((string?)evt?["event"])   // sub-type; omitted for template events
        {
            case "message.delivered":
                app.Logger.LogInformation("Message {MessageId} delivered", messageId);
                break;
            case "message.failed":
                app.Logger.LogError("Message {MessageId} failed (status {Status})", messageId, status);
                break;
            case "message.received":
                app.Logger.LogInformation("Inbound {Channel} from {From}: {Text}",
                    (string?)payload?["channel"], (string?)payload?["inbound_number"], (string?)payload?["text"]);
                break;
            default:
                app.Logger.LogInformation("Message {MessageId} status: {Status}", messageId, status);
                break;
        }
    }

    return Results.Ok(new { received = true });
});

Keep this endpoint outside your authentication middleware, because Sent authenticates with the signature. Then tell Sent where to deliver events. If you prefer a UI, use the webhooks getting started guide; otherwise register over the API:

curl -X POST https://api.sent.dm/v3/webhooks \
  -H "x-api-key: $SENT_DM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "ASP.NET Core integration",
    "endpoint_url": "https://your-domain.example/webhooks/sent",
    "event_types": ["message"]
  }'

Copy two values from the response: the webhook id (used to test delivery in the next step) and signing_secret. Put the secret in SENT_DM_WEBHOOK_SECRET. The message event type covers every message event; the webhook event types reference lists all payload fields.

Verify the integration

Start the app with your credentials loaded:

dotnet run

Send a sandbox message through your new endpoint (adjust the port to match your launch profile). Full validation runs, but nothing is delivered and no credits are consumed:

curl -X POST http://localhost:5000/api/messages/send \
  -H "Content-Type: application/json" \
  -d '{"phoneNumber": "+14155551234", "templateName": "welcome",
       "parameters": {"name": "Ada"}, "sandbox": true}'

The response should contain a message_id and "status": "QUEUED". A 400 here means the request shape is wrong: sandbox requests return real validation errors.

Now confirm webhook delivery end to end. Ask Sent to deliver a signed test event, replacing the ID with the webhook id you copied:

curl -X POST https://api.sent.dm/v3/webhooks/YOUR_WEBHOOK_ID/test \
  -H "x-api-key: $SENT_DM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event_type": "message.delivered"}'

Your application log should show a Message ... delivered line, and the endpoint should have answered 200 {"received": true}. Test events travel the same signed delivery pipeline as real events, so a 401 in your log means the signing secret or verification code is wrong. Sent attempts a test event exactly once, so re-run the command after each fix.

Adapt this to your app

  • If your project uses controllers instead of minimal APIs, move the endpoint bodies into actions. The SDK calls are identical; read the raw body with a StreamReader over Request.Body before model binding touches it.
  • If you bind settings through IOptions, put the API key in a Sent configuration section and validate it at startup. The appendix below shows the pattern.
  • If webhook processing does slow work (database writes, downstream calls), acknowledge with 200 first and hand off to a background service or queue so retries do not pile up; see handling webhook retries.
  • To send free-form text instead of a template, set Text instead of Template, since each send carries exactly one of the two.

Appendix: production scaffolding

The numbered steps stay on the core messaging tasks. The blocks below are optional scaffolding for a production ASP.NET Core stack. Adapt them to your own conventions rather than adopting them wholesale.

Next steps

On this page