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<!-- 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:
| Property | Environment variable | Required | Default value |
|---|---|---|---|
ApiKey | SENT_DM_API_KEY | true | - |
BaseUrl | SENT_BASE_URL | false | "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:
| Status | Exception |
|---|---|
| 400 | SentBadRequestException |
| 401 | SentUnauthorizedException |
| 403 | SentForbiddenException |
| 404 | SentNotFoundException |
| 422 | SentUnprocessableEntityException |
| 429 | SentRateLimitException |
| 5xx | Sent5xxException |
| others | SentUnexpectedStatusCodeException |
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). Usemessage.receivedto 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
- Releases: GitHub Releases
- 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
Sending messages from Spring Boot with the Sent Java SDK
Wire the Sent Java SDK into a Spring Boot app: install, register a client bean, send messages from a controller, verify webhooks, and test with sandbox mode.
Sending messages from ASP.NET Core with the Sent C# SDK
Wire the Sent C# SDK into an ASP.NET Core app: install, register the client with DI, send from an endpoint, verify webhooks, and test with sandbox mode.