C# / .NET SDK

C# / .NET SDK

The official .NET SDK for Sent provides first-class C# support with native async/await, dependency injection integration, and full compatibility with .NET Standard 2.0+.

Requirements

This library requires .NET Standard 2.0 or later.

Installation

dotnet add package Sentdm
Install-Package Sentdm
<!-- Use the latest version from https://github.com/sentdm/sent-dm-csharp/releases -->
<PackageReference Include="Sentdm" Version="x.y.z" />

Quick Start

Initialize the client

using Sentdm;

// Configured using the SENT_DM_API_KEY environment variable
SentClient client = new();

Send your first message

using Sentdm;
using Sentdm.Models.Messages;
using System.Collections.Generic;

SentClient client = new();

MessageSendParams parameters = new()
{
    To = new List<string> { "+1234567890" },
    Channel = new List<string> { "sms", "whatsapp", "rcs" },
    Template = new Sentdm.Models.Messages.Template
    {
        ID = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        Name = "welcome",
        Parameters = new Dictionary<string, string>()
        {
            { "name", "John Doe" },
            { "order_id", "12345" }
        }
    }
};

var response = await client.Messages.Send(parameters);
Console.WriteLine($"Sent: {response.Data.Recipients[0].MessageID}");
Console.WriteLine($"Status: {response.Data.Status}");

Client configuration

Configure the client using environment variables or explicitly:

PropertyEnvironment variableRequiredDefault value
ApiKeySENT_DM_API_KEYtrue-
BaseUrlSENT_BASE_URLfalse"https://api.sent.dm"
using Sentdm;

// Using environment variables
SentClient client = new();

// Or explicit configuration
SentClient client = new()
{
    ApiKey = "your_api_key",
};

// Or a combination
SentClient client = new()
{
    ApiKey = "your_api_key",  // Explicit
    // Other settings from environment
};

Modifying configuration

To temporarily use a modified client configuration, while reusing the same connection and thread pools, call WithOptions:

var clientWithOptions = client.WithOptions(options =>
    options with
    {
        BaseUrl = "https://example.com",
        MaxRetries = 5,
    }
);

Using a with expression makes it easy to construct the modified options.

Send Messages

Send a message

using Sentdm.Models.Messages;

MessageSendParams parameters = new()
{
    To = new List<string> { "+1234567890" },
    Channel = new List<string> { "sms", "whatsapp", "rcs" },
    Template = new Sentdm.Models.Messages.Template
    {
        ID = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        Name = "welcome",
        Parameters = new Dictionary<string, string>()
        {
            { "name", "John Doe" },
            { "order_id", "12345" }
        }
    }
};

var response = await client.Messages.Send(parameters);
Console.WriteLine($"Message ID: {response.Data.Recipients[0].MessageID}");
Console.WriteLine($"Status: {response.Data.Status}");

Sandbox mode

Use Sandbox = true to validate requests without sending real messages:

MessageSendParams parameters = new()
{
    To = new List<string> { "+1234567890" },
    Template = new Sentdm.Models.Messages.Template
    {
        ID = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        Name = "welcome"
    },
    Sandbox = true // Validates but doesn't send
};

var response = await client.Messages.Send(parameters);

// Response will have test data
Console.WriteLine($"Validation passed: {response.Data.Recipients[0].MessageID}");

Check message status

Retrieve the current status of a sent message. The Direction field indicates whether the message is "OUTBOUND" (sent by you) or "INBOUND" (a reply or opt-out keyword received from an end user):

using Sentdm.Models.Messages;

var status = await client.Messages.RetrieveStatus("msg-uuid");

Console.WriteLine($"Status: {status.Data.Status}");      // e.g. "DELIVERED"
Console.WriteLine($"Channel: {status.Data.Channel}");    // e.g. "sms"
Console.WriteLine($"Direction: {status.Data.Direction}"); // "OUTBOUND" | "INBOUND"

Message activities

Retrieve the full activity log for a message, useful for auditing delivery attempts across carriers:

var activities = await client.Messages.RetrieveActivities("msg-uuid");

foreach (var activity in activities.Data.Activities)
{
    Console.WriteLine($"{activity.Timestamp}: {activity.Status} via {activity.From}");
    Console.WriteLine($"  Price: {activity.Price}");
    Console.WriteLine($"  Active contact price: {activity.ActiveContactPrice}");
}

Numbers

Look up carrier and line-type information for any phone number before sending:

var result = await client.Numbers.Lookup("+12025551234");

Console.WriteLine($"Valid: {result.Data.IsValid}");
Console.WriteLine($"Carrier: {result.Data.CarrierName}");
Console.WriteLine($"Line type: {result.Data.LineType}"); // "mobile", "landline", "voip"
Console.WriteLine($"VoIP: {result.Data.IsVoip}");

Error handling

The SDK throws custom unchecked exception types. SentApiException is the base class for all API errors; the following subclasses are thrown per HTTP status code:

StatusException
400SentBadRequestException
401SentUnauthorizedException
403SentForbiddenException
404SentNotFoundException
422SentUnprocessableEntityException
429SentRateLimitException
5xxSent5xxException
othersSentUnexpectedStatusCodeException

All 4xx exceptions also inherit from Sent4xxException. Catch SentApiException to handle any API error, as the last catch block below does:

try
{
    var response = await client.Messages.Send(parameters);
    Console.WriteLine($"Sent: {response.Data.Recipients[0].MessageID}");
}
catch (SentNotFoundException e)
{
    Console.WriteLine($"Not found: {e.Message}");
}
catch (SentRateLimitException e)
{
    Console.WriteLine($"Rate limited. Retry after delay");
}
catch (SentApiException e)
{
    Console.WriteLine($"API Error: {e.Message}");
}

Raw responses

To access response headers, status code, or raw body, prefix any HTTP method call with WithRawResponse:

var response = await client.WithRawResponse.Messages.Send(parameters);
var statusCode = response.StatusCode;
var headers = response.Headers;

// Deserialize if needed
var deserialized = await response.Deserialize();

Retries

The SDK automatically retries 2 times by default, with a short exponential backoff between requests.

Only the following error types are retried:

  • Connection errors
  • 408 Request Timeout
  • 409 Conflict
  • 429 Rate Limit
  • 5xx Internal
using Sentdm;

// Configure for all requests
SentClient client = new() { MaxRetries = 3 };

// Or per-request
await client
    .WithOptions(options => options with { MaxRetries = 3 })
    .Messages.Send(parameters);

Timeouts

Requests time out after 1 minute by default.

using System;
using Sentdm;

// Configure for all requests
SentClient client = new() { Timeout = TimeSpan.FromSeconds(30) };

// Or per-request
await client
    .WithOptions(options => options with { Timeout = TimeSpan.FromSeconds(30) })
    .Messages.Send(parameters);

Contacts

Create and manage contacts:

using Sentdm.Models.Contacts;

// Create a contact
ContactCreateParams createParams = new()
{
    PhoneNumber = "+1234567890"
};

var contact = await client.Contacts.Create(createParams);
Console.WriteLine($"Contact ID: {contact.Data.ID}");

// List contacts
ContactListParams listParams = new()
{
    Page = 1,
    PageSize = 100
};

var contacts = await client.Contacts.List(listParams);
foreach (var c in contacts.Data.Contacts)
{
    Console.WriteLine($"{c.PhoneNumber} - {c.AvailableChannels}");
}

// Get a contact
var retrieved = await client.Contacts.Retrieve("contact-uuid");

// Update a contact
ContactUpdateParams updateParams = new()
{
    PhoneNumber = "+1987654321"
};

var updated = await client.Contacts.Update("contact-uuid", updateParams);

// Delete a contact
await client.Contacts.Delete("contact-uuid");

Templates

List and retrieve templates:

using Sentdm.Models.Templates;

// List templates
var templates = await client.Templates.List();
foreach (var template in templates.Data.Templates)
{
    Console.WriteLine($"{template.Name} ({template.Status}): {template.ID}");
}

// Get a specific template
var template = await client.Templates.Retrieve("template-uuid");
Console.WriteLine($"Name: {template.Data.Name}");
Console.WriteLine($"Status: {template.Data.Status}");

Framework Integration

A dedicated guide covers client registration, message sending, validation, and testing:

Webhooks

Recommended pattern: Webhooks are the primary way to track message delivery, so don't poll the API. Save the message ID when you send, then update your database as webhook events arrive.

Sent delivers signed POST requests to your endpoint for every status change. Two event types exist:

  • message: Message status changes (QUEUED, ROUTED, SCHEDULED, SENT, DELIVERED, READ, FAILED, FILTERED, BLOCKED, RECEIVED); each fires as a sub-type (for example, message.delivered, message.filtered). Use message.received to receive inbound messages from contacts.
  • templates: WhatsApp template approval/rejection

The signing secret (from the Sent Dashboard) has a whsec_ prefix. Strip it and base64-decode the remainder to obtain the raw HMAC key. The signed content is {X-Webhook-ID}.{X-Webhook-Timestamp}.{rawBody} and the signature format is v1,{base64(hmac)}.

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
    [HttpPost("sent")]
    public async Task<IActionResult> HandleWebhook()
    {
        // 1. Read raw body — do NOT use [FromBody] here
        using var ms = new MemoryStream();
        await Request.Body.CopyToAsync(ms);
        var payload   = ms.ToArray();
        var webhookId = Request.Headers["X-Webhook-ID"].ToString();
        var timestamp = Request.Headers["X-Webhook-Timestamp"].ToString();
        var signature = Request.Headers["X-Webhook-Signature"].ToString();

        // 2. Verify: signed content = "{webhookId}.{timestamp}.{rawBody}"
        var secret    = Environment.GetEnvironmentVariable("SENT_DM_WEBHOOK_SECRET")!; // "whsec_..."
        var keyBase64 = secret.StartsWith("whsec_") ? secret[6..] : secret;
        var keyBytes  = Convert.FromBase64String(keyBase64);
        var signed    = Encoding.UTF8.GetBytes($"{webhookId}.{timestamp}.{Encoding.UTF8.GetString(payload)}");
        var expected  = $"v1,{Convert.ToBase64String(HMACSHA256.HashData(keyBytes, signed))}";

        if (!CryptographicOperations.FixedTimeEquals(
                Encoding.UTF8.GetBytes(signature),
                Encoding.UTF8.GetBytes(expected)))
        {
            return Unauthorized(new { error = "Invalid signature" });
        }

        // 3. Optional: reject replayed events older than 5 minutes
        if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(timestamp)) > 300)
            return Unauthorized(new { error = "Timestamp too old" });

        // 4. Handle events — update message status in your own database
        var doc       = JsonDocument.Parse(payload);
        var field     = doc.RootElement.GetProperty("field").GetString();
        var eventType = doc.RootElement.TryGetProperty("event", out var et) ? et.GetString() : null;
        var data      = doc.RootElement.GetProperty("payload");

        if (field == "message")
        {
            if (eventType == "message.received")
            {
                // Inbound message from a contact
                var inboundNumber  = data.GetProperty("inbound_number").GetString();
                var outboundNumber = data.GetProperty("outbound_number").GetString();
                var text           = data.TryGetProperty("text", out var t) ? t.GetString() : null;
                var channel        = data.GetProperty("channel").GetString();
                var receivedAt     = data.GetProperty("received_at").GetString();
                // await db.InboundMessages.AddAsync(new InboundMessage { From = inboundNumber, To = outboundNumber, Text = text, ... });
            }
            else
            {
                // Outbound message status update
                var messageId = data.GetProperty("message_id").GetString();
                var status    = data.GetProperty("message_status").GetString();
                // await db.Messages.Where(m => m.SentId == Guid.Parse(messageId!)).ExecuteUpdateAsync(...)
            }
        }

        // 5. Always return 200 quickly
        return Ok(new { received = true });
    }
}

See the Webhooks reference for the full payload schema and all status values.

Source & Issues

Getting Help


On this page