nuget.org ↗
Installdotnet add package Signalbird.Sdk

Signalbird.Sdk - .NET / ASP.NET Core

It carries three surfaces: Radio (logs), Messaging (email/SMS/push) and Management (projects, chat inbox, apps).

Install

bash
dotnet add package Signalbird.Sdk

It 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)

csharp
// 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:

csharp
var radio = new SignalbirdClient(new SignalbirdOptions { DomainKey = "sb_secret_live_…" });

Radio - logs and events

csharp
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

csharp
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

csharp
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 class field is required and has no default: transactional or commercial. This is a legal gate, and the SDK cannot make that call for you.

csharp
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

csharp
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

csharp
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

csharp
// 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

csharp
// 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

csharp
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
}
csharp
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.

csharp
// 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):

text
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:

bash
dotnet add package Signalbird.Sdk

Error 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 = false and no exception.
  • There is no retry. That decision is yours; if you use Polly, add your own policy to the IHttpClientFactory registration.
  • Field names match the API exactly (snake_case): the SDK renames nothing.
  • CancellationToken is the last parameter of every method.