# .NET SDK

> Send events and confirm contact consent from .NET with the Whisperr NuGet package, including ASP.NET Core DI and request-scoped tracking.

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.

## Install

```bash
dotnet add package Whisperr
```

The package is [`Whisperr`](https://www.nuget.org/packages/Whisperr) on NuGet. The current version is 1.1.0. It targets `net8.0` and `netstandard2.0` (.NET Framework 4.7.2 and later).

## Quickstart

`Program.cs`

```csharp
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](/concepts/events/#one-user-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`.

## 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.

> The .NET SDK sends `opted_in: true` for a channel when `OptedIn` is null. The `Email`, `Phone` and `PushToken` shortcuts on `IdentifyOptions`, and the `WhisperrChannel.Email(…)` and `WhisperrChannel.Push(…)` helpers, do the same. Send a contact only when you hold consent for it, or pass `optedIn: false`.

See [Contact consent](/concepts/consent/).

## ASP.NET Core

Install the companion package for dependency injection and middleware:

```bash
dotnet add package Whisperr.AspNetCore
```

[`Whisperr.AspNetCore`](https://www.nuget.org/packages/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`

```csharp
using Whisperr.AspNetCore;


builder.Services.AddWhisperr(builder.Configuration, options =>
{
    options.OnError = error => Console.Error.WriteLine($"whisperr:{error.Code} {error.Message}");
});
```

`appsettings.json`

```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`

```csharp
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();
});
```

### Request-scoped tracking

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`

```csharp
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?

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.

```csharp
whisperr.Identify("user_8842", new IdentifyOptions
{
    Traits = new Dictionary<string, object?> { ["app_language"] = "pt-BR" }
});
```

## What the .NET SDK adds to every event

| `context` key | Value                                                                              |
| ------------- | ---------------------------------------------------------------------------------- |
| `sdk_name`    | `whisperr-dotnet`                                                                  |
| `sdk_version` | The package version, for example `1.1.0`                                           |
| `app_version` | The `AppVersion` option (`Whisperr:AppVersion` in configuration). Not set: no key. |

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

## How do I send billing events from .NET?

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

```csharp
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](/sdks/node/#how-do-i-send-billing-events-from-node) lists the properties of each event and the closed value lists.

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

## How does the .NET SDK deliver 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](/api/delivery/).

## Options

| Option           | Default                    | Notes                                                                            |
| ---------------- | -------------------------- | -------------------------------------------------------------------------------- |
| `ApiKey`         | —                          | Server key (`wrk_…`). Required unless `Disabled`.                                |
| `BaseUrl`        | `https://api.whisperr.net` | Ingestion base URL.                                                              |
| `AppVersion`     | —                          | Your release version. Sent as `context.app_version`.                             |
| `FlushAt`        | `20`                       | Send when this many operations are queued.                                       |
| `FlushInterval`  | 10 seconds                 | Background send interval. `TimeSpan.Zero` turns the timer off.                   |
| `MaxQueueSize`   | `10000`                    | Oldest operations drop when the queue is full.                                   |
| `MaxBatchSize`   | `500`                      | Events per batch request. 500 is the maximum.                                    |
| `MaxRetries`     | `6`                        | Retries before the batch waits for the next flush.                               |
| `RequestTimeout` | 10 seconds                 | Per-request timeout.                                                             |
| `Disabled`       | `false`                    | Captures and sends nothing. Useful in tests.                                     |
| `Debug`          | `false`                    | Writes 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`. |
| `HttpClient`     | private client             | Pass 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.

Source: https://docs.whisperr.net/sdks/dotnet/
