Skip to content

.NET SDK

Last updated

The Whisperr NuGet package sends events and identify calls from a .NET backend: ASP.NET Core apps, workers and serverless handlers. Use it for facts only your server knows, such as payment failures, cancellations and plan changes, and to confirm contact consent with the server key.

dotnet add package Whisperr

The package is Whisperr on NuGet. The current version is 1.1.0. It targets net8.0 and netstandard2.0 (.NET Framework 4.7.2 and later).

Program.cs
using Whisperr;
// WHISPERR_API_KEY is the server key (wrk_…).
var whisperr = new WhisperrClient(Environment.GetEnvironmentVariable("WHISPERR_API_KEY")!);
// A server-side event:
whisperr.Track("user_8842", "payment_failed", new Dictionary<string, object?>
{
["amount_cents"] = 4900,
["reason"] = "card_declined"
});
// Traits and a contact the user agreed to:
whisperr.Identify("user_8842", new IdentifyOptions
{
Traits = new Dictionary<string, object?>
{
["first_name"] = "Ada",
["plan"] = "pro"
},
Channels = new[]
{
new WhisperrChannel(WhisperrChannelType.Email, "ada@example.com", optedIn: true, verified: true)
}
});
// Before the process exits:
await whisperr.ShutdownAsync();

Create the server key as a Server key in Settings → API keys. Hold one client for the life of the process.

Track(externalUserId, eventType, properties, context) always takes the user id first. Pass the same id your app passes to identify(), and backend and frontend events land on one timeline. Event names must be lowercase snake_case. Track and Identify return false when they do not queue the call, for example for an invalid event name, which is also reported to OnError as dropped.

Section titled “How do I confirm contact consent with the .NET SDK?”

The .NET SDK runs with the server key, so its Identify is how you confirm consent. Pass optedIn: true only when the user agreed to messages on that channel, and verified: true only for an email address your app confirmed. Pass optedIn: false to record an opt-out.

See Contact consent.

Install the companion package for dependency injection and middleware:

dotnet add package Whisperr.AspNetCore

Whisperr.AspNetCore 1.1.0 targets net8.0 and brings in the Whisperr package.

AddWhisperr binds the Whisperr configuration section, falls back to the WHISPERR_API_KEY environment variable for the key, and registers WhisperrClient as a singleton. The host disposes it on shutdown, which runs a final flush.

Program.cs
using Whisperr.AspNetCore;
builder.Services.AddWhisperr(builder.Configuration, options =>
{
options.OnError = error => Console.Error.WriteLine($"whisperr:{error.Code} {error.Message}");
});
appsettings.json
{
"Whisperr": {
"ApiKey": "wrk_your_server_key",
"AppVersion": "2026.10.1",
"FlushInterval": "00:00:10"
}
}

Inject WhisperrClient wherever you have the user id. Track and Identify return at once. Let the background flush send them; do not await FlushAsync on the request path, because a flush can wait through retry backoff.

Program.cs
app.MapPost("/billing/webhook", (BillingWebhook webhook, WhisperrClient whisperr) =>
{
whisperr.Track(webhook.UserId, "payment_failed", new Dictionary<string, object?>
{
["amount_cents"] = webhook.AmountCents,
["provider"] = "stripe"
});
return Results.Ok();
});

For endpoints with a signed-in user, add the middleware after authentication and call HttpContext.Whisperr(). It uses the user id from the NameIdentifier claim, then the sub claim. When no user is signed in, the calls do nothing.

Program.cs
using Whisperr.AspNetCore;
app.UseAuthentication();
app.UseWhisperr();
// To read the user id from another claim:
// app.UseWhisperr(http => http.User.FindFirst("user_id")?.Value);
app.MapPost("/plan/upgrade", (HttpContext http) =>
{
http.Whisperr().Track("plan_upgraded", new Dictionary<string, object?> { ["plan"] = "pro" });
return Results.Ok();
});

HttpContext.Whisperr() throws InvalidOperationException when UseWhisperr() is not in the pipeline.

How do I set the message language from .NET?

Section titled “How do I set the message language from .NET?”

Pass the language the user picked in your product as the app_language trait. Use a BCP 47 tag such as de or pt-BR. The .NET SDK has no separate method for it and never guesses a language.

whisperr.Identify("user_8842", new IdentifyOptions
{
Traits = new Dictionary<string, object?> { ["app_language"] = "pt-BR" }
});
context keyValue
sdk_namewhisperr-dotnet
sdk_versionThe package version, for example 1.1.0
app_versionThe AppVersion option (Whisperr:AppVersion in configuration). Not set: no key.

A key you pass in the context argument of Track wins over these.

When Whisperr has no billing source connected for your app (such as RevenueCat), send membership changes from your backend with the reserved names in WhisperrBillingEvents. Never send them from the browser or the app.

whisperr.Track("user_8842", WhisperrBillingEvents.MembershipStarted, new Dictionary<string, object?>
{
["plan_id"] = "pro_monthly",
["price"] = 9.99m, // major units, not cents
["currency"] = "USD", // ISO 4217, upper case
["plan_period"] = "P1M", // ISO 8601 duration
["transaction_id"] = "txn_123",
["store"] = "stripe",
["is_first_ever"] = true,
["access_source"] = "self_subscription",
["current_period_ends_at"] = "2026-11-12T10:00:00Z"
});

WhisperrBillingEvents has TrialStarted, TrialConverted, TrialAutoRenewDisabled, MembershipStarted, RenewalSucceeded, AutoRenewDisabled, AutoRenewEnabled, MembershipEnded, BillingIssueStarted, BillingRecovered and RefundGranted. The Node SDK page lists the properties of each event and the closed value lists.

Register these event names for your app; setup with your AI agent registers the events your app sends. Whisperr stores an event with an unregistered name but does not map it to your app’s events.

  • Track and Identify add to an in-memory queue and return at once.
  • Events go to POST /v1/events/batch and identify calls to POST /v1/identify, in order, with the X-API-Key header.
  • Every event has a $message_id that stays the same across retries, so Whisperr removes duplicates.
  • 429, 5xx, timeouts and network errors are retried with exponential backoff, up to 30 seconds between tries. After MaxRetries, the batch stays queued for the next flush. The .NET SDK does not read Retry-After.
  • On 401 or 403 the SDK stops sending and keeps the batch. Other 4xx responses drop the batch and report dropped.
  • The queue is not written to disk. In short-lived processes and serverless handlers, await whisperr.ShutdownAsync() or FlushAsync() before you return.

See Delivery, retries, idempotency.

OptionDefaultNotes
ApiKey—Server key (wrk_…). Required unless Disabled.
BaseUrlhttps://api.whisperr.netIngestion base URL.
AppVersion—Your release version. Sent as context.app_version.
FlushAt20Send when this many operations are queued.
FlushInterval10 secondsBackground send interval. TimeSpan.Zero turns the timer off.
MaxQueueSize10000Oldest operations drop when the queue is full.
MaxBatchSize500Events per batch request. 500 is the maximum.
MaxRetries6Retries before the batch waits for the next flush.
RequestTimeout10 secondsPer-request timeout.
DisabledfalseCaptures and sends nothing. Useful in tests.
DebugfalseWrites warnings to Logger, or to stderr.
Logger—Action<string> used when Debug is on.
OnError—Action<WhisperrError>. error.Code is auth, dropped or retry_exhausted.
HttpClientprivate clientPass your own for custom handlers or tests.

Common questions

Which .NET versions does the Whisperr package support?

The Whisperr package targets net8.0 and netstandard2.0, which covers .NET Framework 4.7.2 and later. Whisperr.AspNetCore targets net8.0.

Does the .NET SDK support Native AOT?

Not as a guarantee. The SDK uses reflection-based System.Text.Json. Trimmed deployments are supported; prefer JIT or ReadyToRun for now.

Do I need to flush the client in ASP.NET Core?

No. AddWhisperr registers WhisperrClient as a singleton, and the host disposes it on shutdown, which runs a final flush. Do not await FlushAsync on the request path.

Which API key does the .NET SDK use?

The server key (wrk_…), shown as a Server key in Settings → API keys. Keep it in configuration or an environment variable on your server.