dotnet add package Signalbird.SdkSignalbird.Sdk - .NET / ASP.NET Core
It carries three surfaces: Radio (logs), Messaging (email/SMS/push) and Management (projects, chat inbox, apps).
Install
dotnet add package Signalbird.SdkIt targets net8.0 and netstandard2.1. The only external dependency is for
the DI extension (Microsoft.Extensions.Http) and it is already present in an
ASP.NET Core application; the core clients do not use it, so a console app or
an Azure Function pulls in nothing extra.
Registration (DI)
// Program.cs
builder.Services.AddSignalbird(options =>
{
options.DomainKey = builder.Configuration["Signalbird:RadioKey"]!; // sb_secret_live_…
options.Source = "api-01";
});
builder.Services.AddSignalbirdMessaging(options =>
options.DomainKey = builder.Configuration["Signalbird:DomainKey"]!); // sb_…
builder.Services.AddSignalbirdManagement(options =>
options.DomainKey = builder.Configuration["Signalbird:DomainKey"]!);All three are built through IHttpClientFactory. Every client new-ing its own
HttpClient is the classic cause of socket exhaustion and the most common
mistake in .NET; the SDK does this correctly for you.
If you are not using DI:
var radio = new SignalbirdClient(new SignalbirdOptions { DomainKey = "sb_secret_live_…" });Radio - logs and events
public class PaymentService(SignalbirdClient signalbird)
{
public async Task ChargeAsync(Order order, CancellationToken ct)
{
try
{
await _gateway.ChargeAsync(order, ct);
await signalbird.InfoAsync("payment", $"Order #{order.Id} succeeded", ct: ct);
}
catch (Exception error)
{
await signalbird.CriticalAsync("critical", error.Message,
new Dictionary<string, object?>
{
["order"] = order.Id,
["gateway"] = "iyzico",
}, ct);
throw; // shipping a log does not swallow the error
}
}
}Five levels: DebugAsync, InfoAsync, WarnAsync, ErrorAsync,
CriticalAsync. If you call LogAsync without a level, the channel's own
default applies.
Batch sending
await signalbird.BatchAsync(new[]
{
new LogEvent { Channel = "deploy", Message = "v2.4.0 is live", Level = SignalbirdLevel.Info },
new LogEvent { Channel = "critical", Message = "the queue filled up" },
});At most 100 records; partial success is normal and failed rows are not retried.
Messaging - email, SMS, push
var result = await messaging.SendEmailAsync(new
{
to = "ahmet@example.com",
@class = "transactional", // `class` is a reserved word, escaped with @
subject = "Your order has shipped",
body = "<p>Tracking number: 1234</p>",
});
if (!result.Ok)
{
logger.LogWarning("signalbird: {Code} - {Message}", result.Code, result.Message);
}The
classfield is required and has no default:transactionalorcommercial. This is a legal gate, and the SDK cannot make that call for you.
await messaging.SendSmsAsync(new { to = "+905551112233", @class = "transactional", body = "Your code: 123456" });
await messaging.PreviewSmsAsync("Hello, your order is ready."); // spends no quota
await messaging.SendPushAsync(new { to = "contact:42", @class = "transactional", subject = "Order", body = "It has shipped" });Contacts
await messaging.ListContactsAsync(new Dictionary<string, object?> { ["q"] = "ahmet" });
await messaging.CreateContactAsync(new { email = "ahmet@example.com", first_name = "Ahmet" });
await messaging.UpdateContactAsync(contactId, new { phone = "+905551112233" });
await messaging.DeleteContactAsync(contactId);
// chunks of 1000, SEQUENTIALLY
await messaging.BulkContactsAsync(rows, new Dictionary<string, object?>
{
["list_id"] = 42,
["consent_source"] = "web_form",
});Campaigns
await messaging.CreateCampaignAsync(new
{
name = "August newsletter",
channel = "email",
list_id = 42,
subject = "What happened this month?",
body = "<h1>Hello</h1>",
});
// Walks page by page; never pulls the whole list into memory
await foreach (var message in messaging.IterateCampaignMessagesAsync(batchId))
{
Reconcile(message.GetProperty("external_ref").GetString());
}Webhook verification
// Program.cs - so the raw body can be read
app.Use(async (context, next) =>
{
context.Request.EnableBuffering();
await next();
});
// Controller
[HttpPost("/webhooks/signalbird")]
public async Task<IActionResult> Handle()
{
Request.Body.Position = 0;
using var reader = new StreamReader(Request.Body);
var rawBody = await reader.ReadToEndAsync();
// The RAW body: deserializing the model and re-serializing it breaks the signature
if (!Webhook.Verify(rawBody, Request.Headers["X-Signalbird-Signature"], _secret))
{
return Unauthorized();
}
return Ok();
}Management - set up your own project
// Environment setup (a deploy step or a management command)
var created = await management.CreateModuleKeyAsync("logger", new Dictionary<string, object?> { ["title"] = "Payment errors" });
// `secret` is returned HERE ONLY; the server keeps only its digest
var secret = created.Data?.GetProperty("secret").GetString();
var projectId = created.Data?.GetProperty("project").GetProperty("id").GetInt32();
await management.CreateRadioChannelAsync(projectId!, new
{
key = "payment",
name = "Payment",
level = "critical",
notify_push = true,
quiet_from = 0,
quiet_to = 7,
});
// A chat inbox bot
var inbox = await management.ListConversationsAsync(
new Dictionary<string, object?> { ["status"] = "open" });
await management.ReplyAsync(conversationId, new { body = "Hello! We will be right with you." });
await management.SetConversationStatusAsync(conversationId, "pending");Your key must carry the radio:*, chat:* and apps:* scopes. Details:
the Management document.
Result shape
public sealed class SbResult
{
public bool Ok { get; }
public int Status { get; }
public JsonElement? Data { get; }
public string? Code { get; }
public string? Message { get; }
public T? Into<T>(); // decodes the body into your type
}var result = await management.GetAppAsync(id);
var app = result.Into<AppRecord>();With ThrowOnError = true a SignalbirdException is raised instead (carrying
Code, Status, Body).
Visitor identity verification - identityHash
The chat widget and the app SDK introduce a signed-in user by external_id.
Because the public key sits in the page, Signalbird trusts that identity only
when it comes with a valid hash generated on your server (2.7.0). Without
it the visitor is not linked to the contact record, the device is not bound to
that user as a push target, and the channel agent's tools see the identity
only under visitor.unverified.
// SignalbirdClient from DI
var hash = signalbird.IdentityHash(user.Id);It lives on the Radio client, which already holds the secret key; no extra setup. The algorithm (same output in every language):
identity_hash = hex( HMAC-SHA256( hex(SHA-256(sb_secret_live_…)), external_id ) )The key is the SHA-256 digest of the secret key (the server never stores the key in plain text). Usage: Widget - identity verification, App SDK.
Version notice
The SDK reports its version on every request: X-Signalbird-Sdk: dotnet/<version>.
When a newer version is released the API responds with
Signalbird-Sdk-Status: outdated (or unsupported once it is no longer
supported) and the SDK writes a warning once per process (Console.Error).
The warning is not an error: the request result does not change.
In the panel, Domains → [domain] → Keys shows which SDK version each key is used with; keys on an old version also get a banner at the top of the panel and a notification. To update:
dotnet add package Signalbird.SdkError codes
| Code | Meaning |
|---|---|
NO_KEY / WRONG_KEY_TYPE |
Caught at construction time |
INVALID_KEY |
The key is missing, wrong, or the project is inactive |
API_KEY_SCOPE |
The key lacks the required scope |
MODULE_DISABLED |
Your package does not include this module |
LIMIT_REACHED |
Your monthly limit is full |
VALIDATION_ERROR |
422 |
NETWORK_ERROR / TIMEOUT |
Network (status 0) |
Rules of behaviour
- Silent failure is the default. If Signalbird cannot be reached you get
Ok = falseand no exception. - There is no retry. That decision is yours; if you use
Polly, add your own policy to theIHttpClientFactoryregistration. - Field names match the API exactly (snake_case): the SDK renames nothing.
CancellationTokenis the last parameter of every method.