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 SentdmInstall-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:
| Property | Environment variable | Required | Default value |
|---|---|---|---|
ApiKey | SENT_DM_API_KEY | true | - |
BaseUrl | SENT_DM_BASE_URL | true | "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:
| Status | Exception |
|---|---|
| 400 | SentDmBadRequestException |
| 401 | SentDmUnauthorizedException |
| 403 | SentDmForbiddenException |
| 404 | SentDmNotFoundException |
| 422 | SentDmUnprocessableEntityException |
| 429 | SentDmRateLimitException |
| 5xx | SentDm5xxException |
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
- Version: 0.17.0
- GitHub: sentdm/sent-dm-csharp
- NuGet: Sentdm
- Issues: Report a bug
Getting Help
- Documentation: API Reference
- Troubleshooting: Common Issues
- Support: Email support@sent.dm with your request ID