C# / .NET SDK

C# / .NET SDK

The official .NET SDK for Sent LogoSent 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
<PackageReference Include="Sentdm" Version="0.17.0" />

Quick Start

Initialize the client

using Sentdm;

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

Send your first message

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

SentDmClient client = new();

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

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

Client configuration

Configure the client using environment variables or explicitly:

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

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

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

// Or a combination
SentDmClient 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" },
    Template = new MessageSendParamsTemplate
    {
        Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        Name = "welcome",
        Parameters = new Dictionary<string, string>()
        {
            { "name", "John Doe" },
            { "order_id", "12345" }
        }
    }
};

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

Test mode

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

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

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

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

Error handling

The SDK throws custom unchecked exception types:

StatusException
400SentDmBadRequestException
401SentDmUnauthorizedException
403SentDmForbiddenException
404SentDmNotFoundException
422SentDmUnprocessableEntityException
429SentDmRateLimitException
5xxSentDm5xxException
try
{
    var response = await client.Messages.SendAsync(parameters);
    Console.WriteLine($"Sent: {response.Data.Messages[0].Id}");
}
catch (SentDmNotFoundException e)
{
    Console.WriteLine($"Not found: {e.Message}");
}
catch (SentDmRateLimitException e)
{
    Console.WriteLine($"Rate limited. Retry after delay");
}
catch (SentDmApiException 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.SendAsync(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
SentDmClient client = new() { MaxRetries = 3 };

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

Timeouts

Requests time out after 1 minute by default.

using System;
using Sentdm;

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

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

Contacts

Create and manage contacts:

using Sentdm.Models.Contacts;

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

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

// List contacts
ContactListParams listParams = new()
{
    Limit = 100
};

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

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

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

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

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

Templates

List and retrieve templates:

using Sentdm.Models.Templates;

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

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

ASP.NET Core Integration

Dependency Injection Setup

// Program.cs
using Sentdm;

var builder = WebApplication.CreateBuilder(args);

// Add Sent client
builder.Services.AddSentDm(options =>
{
    options.ApiKey = builder.Configuration["Sent:ApiKey"]!;
});

var app = builder.Build();

Minimal API Example

// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSentDm(options =>
{
    options.ApiKey = builder.Configuration["Sent:ApiKey"]!;
});

var app = builder.Build();

// Send message endpoint
app.MapPost("/api/send-message", async (
    MessageSendParams req,
    SentDmClient client,
    CancellationToken ct) =>
{
    try
    {
        var result = await client.Messages.SendAsync(req, ct);
        return Results.Ok(new
        {
            MessageId = result.Data.Messages[0].Id,
            Status = result.Data.Messages[0].Status,
        });
    }
    catch (SentDmApiException ex)
    {
        return Results.BadRequest(new { Error = ex.Message });
    }
});

app.Run();

MVC Controller

// Controllers/MessagesController.cs
using Microsoft.AspNetCore.Mvc;
using Sentdm;
using Sentdm.Models.Messages;

namespace MyApp.Controllers;

[ApiController]
[Route("api/[controller]")]
public class MessagesController : ControllerBase
{
    private readonly SentDmClient _client;
    private readonly ILogger<MessagesController> _logger;

    public MessagesController(SentDmClient client, ILogger<MessagesController> logger)
    {
        _client = client;
        _logger = logger;
    }

    [HttpPost("send")]
    public async Task<IActionResult> SendMessage([FromBody] MessageSendParams request)
    {
        try
        {
            var result = await _client.Messages.SendAsync(request);
            return Ok(new
            {
                MessageId = result.Data.Messages[0].Id,
                Status = result.Data.Messages[0].Status
            });
        }
        catch (SentDmApiException ex)
        {
            _logger.LogError(ex, "Failed to send message");
            return BadRequest(new { Error = ex.Message });
        }
    }
}

Client configuration from appsettings.json

{
  "Sent": {
    "ApiKey": "your_api_key_here",
    "BaseUrl": "https://api.sent.dm",
    "Timeout": 30,
    "MaxRetries": 3
  }
}
// Program.cs
builder.Services.AddSentDm(options =>
{
    var config = builder.Configuration.GetSection("Sent");
    options.ApiKey = config["ApiKey"]!;
    options.BaseUrl = config["BaseUrl"];
    options.Timeout = TimeSpan.FromSeconds(config.GetValue<int>("Timeout"));
    options.MaxRetries = config.GetValue<int>("MaxRetries");
});

Webhooks

Recommended pattern: Webhooks are the primary way to track message delivery — 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:

  • messages — Message status changes (SENT, DELIVERED, READ, FAILED, …)
  • 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_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 data  = doc.RootElement.GetProperty("payload");

        if (field == "messages")
        {
            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