Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

107 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sendly .NET SDK

NuGet license

Sendly .NET SDK

Official .NET SDK for the Sendly SMS API.

Requirements

  • .NET 8.0+

Installation

# .NET CLI
dotnet add package Sendly

# Package Manager Console
Install-Package Sendly

# PackageReference (add to .csproj)
<PackageReference Include="Sendly" Version="3.36.0" />

Quick Start

using Sendly;

using var client = new SendlyClient("sk_live_v1_your_api_key");

// Send an SMS
var message = await client.Messages.SendAsync(
    "+15551234567",
    "Hello from Sendly!"
);

Console.WriteLine(message.Id);     // "msg_abc123"
Console.WriteLine(message.Status); // "queued"

Prerequisites for Live Messaging

Before sending live SMS messages, you need:

  1. Business Verification - Complete verification in the Sendly dashboard

    • International: Instant approval (just provide Sender ID)
    • US/Canada: Requires carrier approval (3-7 business days)
  2. Credits - Add credits to your account

    • Test keys (sk_test_*) work without credits (sandbox mode)
    • Live keys (sk_live_*) require credits for each message
  3. Live API Key - Generate after verification + credits

    • Dashboard → API Keys → Create Live Key

Test vs Live Keys

Key Type Prefix Credits Required Verification Required Use Case
Test sk_test_v1_* No No Development, testing
Live sk_live_v1_* Yes Yes Production messaging

Note: You can start development immediately with a test key. Messages to sandbox test numbers are free and don't require verification.

Configuration

using var client = new SendlyClient("sk_live_v1_xxx", new SendlyClientOptions
{
    BaseUrl = "https://sendly.live/api/v1",
    Timeout = TimeSpan.FromSeconds(60),
    MaxRetries = 5
});

Messages

Send an SMS

// Marketing message (default)
var message = await client.Messages.SendAsync("+15551234567", "Check out our new features!");

// Transactional message (bypasses quiet hours)
var message = await client.Messages.SendAsync(new SendMessageRequest(
    "+15551234567",
    "Your verification code is: 123456"
) { MessageType = "transactional" });

// With custom metadata (max 4KB)
var message = await client.Messages.SendAsync(new SendMessageRequest(
    "+15551234567",
    "Your order #12345 has shipped!"
) { 
    Metadata = new Dictionary<string, object> 
    { 
        { "order_id", "12345" }, 
        { "customer_id", "cust_abc" } 
    } 
});

// Send from one of your owned numbers (or an alphanumeric sender ID).
// Omit From to use your default sender.
var message = await client.Messages.SendAsync(new SendMessageRequest(
    "+15551234567",
    "Hello from our team!"
) { From = "+447111111111" });

Console.WriteLine(message.Id);
Console.WriteLine(message.Status);
Console.WriteLine(message.CreditsUsed);

List Messages

// Basic listing
var messages = await client.Messages.ListAsync();

foreach (var msg in messages)
{
    Console.WriteLine(msg.To);
}

// With options
var messages = await client.Messages.ListAsync(new ListMessagesOptions
{
    Status = "delivered",
    To = "+15551234567",
    Limit = 50,
    Offset = 0
});

// Pagination info
Console.WriteLine(messages.Total);
Console.WriteLine(messages.HasMore);

Get a Message

var message = await client.Messages.GetAsync("msg_abc123");

Console.WriteLine(message.To);
Console.WriteLine(message.Text);
Console.WriteLine(message.Status);
Console.WriteLine(message.DeliveredAt);

Scheduling Messages

// Schedule a message for future delivery
var scheduled = await client.Messages.ScheduleAsync(new ScheduleMessageRequest(
    "+15551234567",
    "Your appointment is tomorrow!",
    "2025-01-15T10:00:00Z"
));

Console.WriteLine(scheduled.Id);
Console.WriteLine(scheduled.ScheduledAt);

// List scheduled messages
var result = await client.Messages.ListScheduledAsync();
foreach (var msg in result)
{
    Console.WriteLine($"{msg.Id}: {msg.ScheduledAt}");
}

// Get a specific scheduled message
var msg = await client.Messages.GetScheduledAsync("sched_xxx");

// Cancel a scheduled message (refunds credits)
var cancel = await client.Messages.CancelScheduledAsync("sched_xxx");
Console.WriteLine($"Refunded: {cancel.CreditsRefunded} credits");

Batch Messages

// Send multiple messages in one API call (up to 1000)
var batch = await client.Messages.SendBatchAsync(new SendBatchRequest()
    .AddMessage("+15551234567", "Hello User 1!")
    .AddMessage("+15559876543", "Hello User 2!")
    .AddMessage("+15551112222", "Hello User 3!")
);

Console.WriteLine(batch.BatchId);
Console.WriteLine($"Queued: {batch.Queued}");
Console.WriteLine($"Failed: {batch.Failed}");
Console.WriteLine($"Credits used: {batch.CreditsUsed}");

// Get batch status
var status = await client.Messages.GetBatchAsync("batch_xxx");

// List all batches
var batches = await client.Messages.ListBatchesAsync();

// Preview batch (dry run) - validates without sending
var preview = await client.Messages.PreviewBatchAsync(new SendBatchRequest()
    .AddMessage("+15551234567", "Hello User 1!")
    .AddMessage("+447700900123", "Hello UK!")
);
Console.WriteLine($"Credits needed: {preview.CreditsNeeded}");
Console.WriteLine($"Will send: {preview.WillSend}, Blocked: {preview.Blocked}");

Iterate All Messages

// Auto-pagination with IAsyncEnumerable
await foreach (var message in client.Messages.GetAllAsync())
{
    Console.WriteLine($"{message.Id}: {message.To}");
}

// With options
await foreach (var message in client.Messages.GetAllAsync(new ListMessagesOptions
{
    Status = "delivered"
}))
{
    Console.WriteLine($"Delivered: {message.Id}");
}

Group MMS

// Send a group MMS to 2-8 US/Canada recipients. Everyone sees the others and
// replies fan out to the group. Omit From to use your default sender.
var group = await client.Messages.SendGroupAsync(new SendGroupMessageRequest(
    new[] { "+14155551234", "+14155555678" },
    "Hey team - quick sync at noon?"
));

Console.WriteLine(group.Id);              // "msg_abc123"
Console.WriteLine(group.GroupMessageId);  // "grp_xxx" (on live sends)
Console.WriteLine(group.Status);          // "sent" or "delivered"

AI Message Enhancement

// Rewrite a draft into a single polished SMS segment (<=160 chars).
var result = await client.Messages.EnhanceAsync(new EnhanceMessageRequest(
    text: "hey come check out our sale this weekend",
    messageType: "marketing"
));

Console.WriteLine(result.Enhanced);     // polished rewrite
Console.WriteLine(result.Explanation);  // what changed and why

Message Templates

Reusable SMS templates with {{variables}}, published for use with the Verify API. (Distinct from client.Templates, which manages Verify OTP templates.)

// List presets + custom templates
var listing = await client.MessageTemplates.ListAsync();

// Create a draft, then publish
var template = await client.MessageTemplates.CreateAsync(new CreateMessageTemplateRequest
{
    Name = "Password Reset",
    Text = "{{app_name}}: your reset code is {{code}}. Valid for 10 minutes."
});
await client.MessageTemplates.PublishAsync(template.Id);

// Preview with sample values
var preview = await client.MessageTemplates.PreviewAsync(template.Id, new Dictionary<string, string>
{
    ["app_name"] = "MyApp",
    ["code"] = "123456"
});
Console.WriteLine(preview.PreviewText);

// Clone (including from a preset), update, delete
var clone = await client.MessageTemplates.CloneAsync("tpl_preset_otp", "My Custom OTP");
await client.MessageTemplates.DeleteAsync(clone.Id);

Branded Short Links

Mint branded, owned-domain short links (better carrier deliverability than public shorteners) with click analytics and a per-link kill switch.

Note: URL shortening is gated behind the founder-only url_shortener flag and is not yet publicly stable. Calls raise NotFoundException (not_found) until the flag is on for your account.

// Shorten a URL
var link = await client.Links.CreateAsync("https://example.com/spring-sale");
Console.WriteLine(link.ShortUrl); // "https://sendly.live/l/Ab3xY7"

// List your links with click counts
var links = await client.Links.ListAsync(new ListShortLinksOptions { Limit = 20 });
foreach (var l in links.Links)
{
    Console.WriteLine($"{l.ShortUrl} -> {l.DestinationUrl} ({l.ClickCount} clicks)");
}

// Kill / re-enable a link
await client.Links.DisableAsync(link.Code);
await client.Links.EnableAsync(link.Code);

Numbers

// Browse coverage and search for an available number
var countries = await client.Numbers.ListCountriesAsync();
var available = await client.Numbers.ListAvailableAsync(new ListAvailableNumbersOptions
{
    Country = "GB",
    Type = "mobile"
});

// List the numbers you own
var owned = await client.Numbers.ListAsync();
foreach (var n in owned.Numbers)
{
    Console.WriteLine($"{n.PhoneNumber}{n.Status}");
}

// Get one by id (includes IsDefault)
var number = await client.Numbers.GetAsync("num_xxx");
Console.WriteLine($"{number.PhoneNumber} — default: {number.IsDefault}");

// Make it the workspace default sender (must be active)
var updated = await client.Numbers.UpdateAsync("num_xxx", new UpdateNumberRequest { IsDefault = true });
// (or the convenience wrapper)
await client.Numbers.SetDefaultAsync("num_xxx");

// Cancel a scheduled release ("keep this number")
await client.Numbers.UpdateAsync("num_xxx", new UpdateNumberRequest { PendingCancellation = false });
await client.Numbers.KeepAsync("num_xxx"); // convenience wrapper

// Release a number. A live paid purchase is cancelled at period end.
var release = await client.Numbers.ReleaseAsync("num_xxx");
if (release.Scheduled == true)
    Console.WriteLine($"Releases at {release.ScheduledReleaseAt}");
else
    Console.WriteLine("Released");

WhatsApp

Connect a number you own to WhatsApp ($19 one-time setup, no monthly fee), create Meta-reviewed message templates, and send with client.Messages.SendAsync(new SendWhatsAppMessageRequest(...)). Free-form text and media only deliver inside an open 24-hour customer-service window (the recipient messaged you in the last 24h); an approved template works anytime. WhatsApp requires a live API key.

// 1. Connect a number. The connect URL must be opened by a human — they log
//    in with Facebook in a browser to link their WhatsApp Business Account.
var signup = await client.WhatsApp.Signup.CreateAsync("+15559876543");
Console.WriteLine($"Have your user open: {signup.ConnectUrl}");

// 2. Poll until active
var status = await client.WhatsApp.Signup.GetAsync(signup.Id);
Console.WriteLine(status.Status); // "initiated" -> "registering" -> "active"

// List your connected senders
var senders = await client.WhatsApp.Senders.ListAsync();
foreach (var s in senders.Senders)
{
    Console.WriteLine($"{s.PhoneNumber} ({s.DisplayName ?? "no name yet"}) — {s.Status}");
}

// 3. Create a template (Meta reviews it, usually 24-48h)
var template = await client.WhatsApp.Templates.CreateAsync(new CreateWhatsAppTemplateRequest
{
    Sender = "+15559876543",
    Name = "order_shipped",
    Language = "en_US",
    Category = "UTILITY",
    Body = "Hi {{1}}, your order {{2}} has shipped!",
    Examples = new() { ["1"] = "Sam", ["2"] = "#4821" }
});
Console.WriteLine(template.Status); // "PENDING"

// List templates; edit a rejected one and resubmit (template names are locked
// for ~30 days after deletion, so editing is the recovery path)
var templates = await client.WhatsApp.Templates.ListAsync();
await client.WhatsApp.Templates.UpdateAsync(template.Id, new UpdateWhatsAppTemplateRequest
{
    Body = "Hi {{1}}, your order {{2}} is on its way!",
    Examples = new() { ["1"] = "Sam", ["2"] = "#4821" }
});
await client.WhatsApp.Templates.DeleteAsync("wat_xxx");

// 4. Check the 24-hour window, then send
var window = await client.WhatsApp.WindowAsync("+15559876543", "+15551234567");

// Free-form text inside an open window
var message = await client.Messages.SendAsync(new SendWhatsAppMessageRequest(
    "+15551234567",
    "+15559876543",
    text: "Your table is ready!"
));

// Media with a caption (window-bound; WhatsApp accepts exactly one attachment)
await client.Messages.SendAsync(new SendWhatsAppMessageRequest(
    "+15551234567",
    "+15559876543",
    text: "Here's your receipt",
    mediaUrls: new() { "https://example.com/receipt.pdf" }
));

// Template send — works regardless of the window
await client.Messages.SendAsync(new SendWhatsAppMessageRequest(
    "+15551234567",
    "+15559876543",
    template: new WhatsAppTemplateSendParams
    {
        Name = "order_shipped",
        Language = "en_US",
        Variables = new() { ["1"] = "Sam", ["2"] = "#4821" }
    }
));

Console.WriteLine(message.WhatsApp.Kind); // "text", "media", or "template"
Console.WriteLine(message.CreditsUsed);   // priced by country + category

// 5. Read and edit a sender's business profile — the contact card recipients
//    see. Supply only the fields to change; omitted fields keep their value.
var profile = await client.WhatsApp.Senders.GetProfileAsync("+15559876543");
Console.WriteLine($"{profile.DisplayName}{profile.About}");

await client.WhatsApp.Senders.UpdateProfileAsync("+15559876543",
    new UpdateWhatsAppSenderProfileRequest
    {
        About = "Fast delivery, friendly service",  // max 139 chars
        Description = "Acme sells everything.",     // max 512 chars
        Website = "https://example.com"
    });

RCS

Send branded rich messaging — cards and suggestion chips — with client.Messages.SendAsync(new SendRcsMessageRequest(...)). Plain-text RCS sends fall back to SMS automatically for recipients whose device can't receive RCS. RCS is gated behind the rcs_channel rollout flag (default-dark) and requires a live API key.

// 1. Find your agent — the brand identity your messages are sent as. Contact
//    support to register one; "testing" reaches invited test numbers only,
//    "approved" reaches everyone.
var agents = await client.Rcs.Agents.ListAsync();
foreach (var agent in agents.Agents)
{
    Console.WriteLine($"{agent.Name} ({agent.Status}, sendable={agent.Sendable})");
}

// 2. Optional pre-flight: can this recipient receive RCS?
var capability = await client.Rcs.CapabilityAsync("+15551234567");
Console.WriteLine(capability.Capable);  // false -> text falls back to SMS
Console.WriteLine(string.Join(", ", capability.Features));

// 3. Send text, optionally with suggestion chips. A reply chip's tap comes
//    back as an inbound message carrying your postbackData; an action chip
//    opens a URL.
var message = await client.Messages.SendAsync(new SendRcsMessageRequest(
    "+15551234567",
    text: "Your order #4821 has shipped!",
    suggestions: new()
    {
        RcsSuggestion.CreateReply("Thanks", "thanks"),
        RcsSuggestion.CreateAction("Track", "track", "https://example.com/track/4821")
    }
));

// The response tells you which leg delivered
if (message.FellBackToSms)
{
    // Not RCS-capable: sent and billed as SMS, chips dropped
    Console.WriteLine(message.Channel);                  // "sms"
    Console.WriteLine(message.Rcs.RequestedChannel);     // "rcs"
    Console.WriteLine(message.Rcs.SuggestionsDropped);   // True
}
else
{
    Console.WriteLine(message.Channel);        // "rcs"
    Console.WriteLine(message.Rcs.Kind);       // "text"
    Console.WriteLine(message.Rcs.AgentName);  // "Acme Inc"
}

// 4. Send a rich card. Cards have no SMS form — a card to a non-RCS recipient
//    fails with rcs_not_supported_for_recipient rather than falling back.
await client.Messages.SendAsync(new SendRcsMessageRequest(
    "+15551234567",
    card: new RcsCard
    {
        Title = "Order #4821 shipped",
        Description = "Arriving Thursday",
        MediaUrl = "https://example.com/package.jpg",  // public JPEG, PNG, or GIF
        Orientation = "vertical",                      // or "horizontal"
        Suggestions = new()
        {
            RcsSuggestion.CreateAction("Track", "track", "https://example.com/track/4821")
        }
    }
));

// Opt out of the SMS fallback to get a 422 instead of an SMS charge
await client.Messages.SendAsync(new SendRcsMessageRequest(
    "+15551234567",
    text: "RCS only, please",
    fallbackToSms: false
));

// Pass agentId when your workspace has more than one agent (otherwise the
// send fails with rcs_agent_ambiguous)
await client.Messages.SendAsync(new SendRcsMessageRequest(
    "+15551234567",
    text: "Your order #4821 has shipped!",
    agentId: "rag_abc123"
));

Webhooks

// Create a webhook endpoint
var webhook = await client.Webhooks.CreateAsync(new CreateWebhookOptions
{
    Url = "https://example.com/webhooks/sendly",
    Events = new List<string> { "message.delivered", "message.failed" }
});

Console.WriteLine(webhook.Id);
Console.WriteLine(webhook.Secret); // Store securely!

// List all webhooks
var webhooks = await client.Webhooks.ListAsync();

// Get a specific webhook
var wh = await client.Webhooks.GetAsync("whk_xxx");

// Update a webhook
await client.Webhooks.UpdateAsync("whk_xxx", new UpdateWebhookOptions
{
    Url = "https://new-endpoint.example.com/webhook",
    Events = new List<string> { "message.delivered", "message.failed", "message.sent" }
});

// Test a webhook
var result = await client.Webhooks.TestAsync("whk_xxx");

// Rotate webhook secret
var rotation = await client.Webhooks.RotateSecretAsync("whk_xxx");

// Delete a webhook
await client.Webhooks.DeleteAsync("whk_xxx");

// List available webhook event types
var eventTypes = await client.Webhooks.ListEventTypesAsync();
foreach (var eventType in eventTypes)
{
    Console.WriteLine($"Event: {eventType}");
}

Account & Credits

// Get account information
var account = await client.Account.GetAsync();
Console.WriteLine(account.Email);

// Check credit balance
var credits = await client.Account.GetCreditsAsync();
Console.WriteLine($"Available: {credits.AvailableBalance} credits");
Console.WriteLine($"Reserved: {credits.ReservedBalance} credits");
Console.WriteLine($"Total: {credits.Balance} credits");

// View credit transaction history
var transactions = await client.Account.ListTransactionsAsync();
foreach (var tx in transactions)
{
    Console.WriteLine($"{tx.Type}: {tx.Amount} credits - {tx.Description}");
}

// List API keys
var keys = await client.Account.ListApiKeysAsync();
foreach (var key in keys)
{
    Console.WriteLine($"{key.Name}: {key.Prefix}*** ({key.Type})");
}

// Get a specific API key
var key = await client.Account.GetApiKeyAsync("key_xxx");

// Get API key usage stats
var usage = await client.Account.GetApiKeyUsageAsync("key_xxx");
Console.WriteLine($"Messages sent: {usage.MessagesSent}");

// Create a new API key
var newKey = await client.Account.CreateApiKeyAsync(new CreateApiKeyOptions
{
    Name = "Production Key"
});
Console.WriteLine($"New key: {newKey.Key}"); // Only shown once!

// Revoke an API key
await client.Account.RevokeApiKeyAsync("key_xxx");

// Rotate a key. Mints a replacement and keeps the old one working for a grace
// period (24-168 hours, default 24) so you can roll the new key out first.
var rotation = await client.Account.RotateApiKeyAsync("key_xxx", gracePeriodHours: 48);
Console.WriteLine(rotation.NewKey.Key);     // full raw sk_ value — shown once!
Console.WriteLine(rotation.NewKey.Warning);
Console.WriteLine(rotation.OldKey.ExpiresAt); // when the old key stops working
Console.WriteLine(rotation.Message);

Error Handling

using Sendly.Exceptions;

try
{
    var message = await client.Messages.SendAsync("+15551234567", "Hello!");
}
catch (AuthenticationException e)
{
    // Invalid API key
}
catch (RateLimitException e)
{
    // Rate limit exceeded
    Console.WriteLine($"Retry after: {e.RetryAfter?.TotalSeconds} seconds");
}
catch (InsufficientCreditsException e)
{
    // Add more credits
}
catch (ValidationException e)
{
    // Invalid request
}
catch (NotFoundException e)
{
    // Resource not found
}
catch (NetworkException e)
{
    // Network error
}
catch (SendlyException e)
{
    // Other error
    Console.WriteLine(e.Message);
    Console.WriteLine(e.ErrorCode);
    Console.WriteLine(e.StatusCode);
}

Message Object

message.Id           // Unique identifier
message.To           // Recipient phone number
message.Text         // Message content
message.Status       // queued, sending, sent, delivered, failed
message.CreditsUsed  // Credits consumed
message.CreatedAt    // DateTime
message.UpdatedAt    // DateTime
message.DeliveredAt  // DateTime? (nullable)
message.ErrorCode    // string? (nullable)
message.ErrorMessage // string? (nullable)

// Helper properties
message.IsDelivered  // bool
message.IsFailed     // bool
message.IsPending    // bool

Message Status

Status Description
queued Message is queued for delivery
sending Message is being sent
sent Message was sent to carrier
delivered Message was delivered
failed Message delivery failed

Pricing Tiers

Tier Countries Credits per SMS
Domestic US, CA 2
Tier 1 GB, PL, IN, etc. 8
Tier 2 FR, JP, AU, etc. 12
Tier 3 DE, IT, MX, etc. 16

Sandbox Testing

Use test API keys (sk_test_v1_xxx) with these test numbers:

Number Behavior
+15005550000 Success (instant)
+15005550001 Fails: invalid_number
+15005550002 Fails: unroutable_destination
+15005550003 Fails: queue_full
+15005550004 Fails: rate_limit_exceeded
+15005550006 Fails: carrier_violation

Enterprise

The Enterprise API lets you programmatically manage workspaces, verification, credits, and API keys for multi-tenant platforms. Requires an enterprise master key (sk_live_v1_master_*).

Quick Provision

Create a fully configured workspace in a single call:

var client = new SendlyClient("sk_live_v1_master_YOUR_KEY");

var result = await client.Enterprise.ProvisionAsync(new ProvisionWorkspaceOptions
{
    Name = "Acme Insurance - Austin",
    SourceWorkspaceId = "ws_verified",
    CreditAmount = 5000,
    CreditSourceWorkspaceId = "SOURCE_WORKSPACE_ID",
    KeyName = "Production",
    KeyType = "live",
    GenerateOptInPage = true
});

Console.WriteLine(result.Workspace.Id);
Console.WriteLine(result.Key?.Key);

Three provisioning modes:

Mode Params Description
Inherit SourceWorkspaceId Shares toll-free number from verified workspace
Inherit + New Number SourceWorkspaceId + InheritWithNewNumber = true Copies business info, purchases new number
Fresh Verification = new VerificationData{...} Full business details, new number + carrier approval

Workspace Management

var ws = await client.Enterprise.Workspaces.CreateAsync("Acme Insurance");
var list = await client.Enterprise.Workspaces.ListAsync();
var detail = await client.Enterprise.Workspaces.GetAsync("ws_xxx");
await client.Enterprise.Workspaces.DeleteAsync("ws_xxx");

Credits & API Keys

await client.Enterprise.Workspaces.TransferCreditsAsync("ws_dest", "ws_source", 5000);

var key = await client.Enterprise.Workspaces.CreateKeyAsync("ws_xxx", new CreateWorkspaceKeyOptions
{
    Name = "Production",
    Type = "live"
});
Console.WriteLine(key.Key);

await client.Enterprise.Workspaces.RevokeKeyAsync("ws_xxx", "key_abc");

Webhooks & Analytics

await client.Enterprise.Webhooks.SetAsync("https://yourapp.com/webhooks");
var overview = await client.Enterprise.Analytics.OverviewAsync();
var messages = await client.Enterprise.Analytics.MessagesAsync(new EnterpriseAnalyticsOptions { Period = "30d" });
var delivery = await client.Enterprise.Analytics.DeliveryAsync();

Full enterprise docs: sendly.live/docs/enterprise


License

MIT

About

Official .NET SDK for Sendly SMS & MMS API

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages