# .NET

> Read identification events with a secret key and verify signed webhooks from .NET 6 and newer, with async APIs and dependency injection.

Last updated: 2026-09-17

The `Fingerly` NuGet package reads stored events by request ID and verifies webhook signatures. Every call is asynchronous and accepts a `CancellationToken`.

## Requirements

- .NET 6 or newer.
- A [secret key](https://docs.fingerly.io/docs/api-keys), and a webhook signing secret if you receive [webhooks](https://docs.fingerly.io/docs/webhooks).

## Install

```bash .NET CLI
dotnet add package Fingerly
```

```xml PackageReference
<PackageReference Include="Fingerly" Version="0.1.0" />
```

## Read an event

Create one client with your secret key and reuse it. The key decides the regional API and the environment the client reads.

```csharp .NET
using Fingerly;

var fingerly = new FingerlyClient(Environment.GetEnvironmentVariable("FINGERLY_SECRET_KEY"));
var ev = await fingerly.Events.GetAsync("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4");
```

An event has the fields listed in [Get an event](https://docs.fingerly.io/reference/get-event#response). `suspect_score` is `null` when the request was not scored.

## Verify a checkout

Read the event your client identified, check it belongs to this action and is recent, then decide on its level. See [server-side verification](https://docs.fingerly.io/docs/server-side-verification).

```csharp .NET
public async Task<string> DecideAsync(string orderId, string requestId, CancellationToken ct)
{
    Event ev;
    try
    {
        ev = await _fingerly.Events.GetAsync(requestId, ct);
    }
    catch (FingerlyApiException e) when (e.Status == 404)
    {
        return "refuse";
    }

    if (ev.Tag != $"checkout:{orderId}") return "refuse";
    if (DateTimeOffset.UtcNow - ev.OccurredAt > TimeSpan.FromMinutes(2)) return "refuse";

    return ev.SuspectLevel switch
    {
        "high" => "review",
        "medium" => "challenge",
        _ => "allow",
    };
}
```

## Verify a webhook

Check the signature over the raw request body before parsing it. The helper rejects timestamps more than five minutes from now.

```csharp .NET
app.MapPost("/webhooks/fingerly", async (HttpRequest request, IEventQueue queue) =>
{
    using var reader = new StreamReader(request.Body);
    var body = await reader.ReadToEndAsync();

    var valid = FingerlyWebhook.Verify(
        secret: builder.Configuration["Fingerly:WebhookSecret"]!,
        payload: body,
        timestamp: request.Headers["x-fingerly-timestamp"],
        signature: request.Headers["x-fingerly-signature"]);
    if (!valid) return Results.BadRequest();

    var ev = FingerlyWebhook.Parse(body);
    await queue.EnqueueAsync(ev.Id, body);   // deduplicate on the event ID
    return Results.NoContent();
});
```

## Dependency injection

```csharp Program.cs
builder.Services.AddFingerly(options =>
{
    options.SecretKey = builder.Configuration["Fingerly:SecretKey"];
});

// Inject FingerlyClient wherever it is needed. It is registered as a singleton.
```

## API

| Member | Returns | Notes |
| --- | --- | --- |
| `new FingerlyClient(secretKey)` | `FingerlyClient` | Or `new FingerlyClient(new FingerlyClientOptions { ... })` for `Endpoint` and `HttpClient`. |
| `Events.GetAsync(requestId, ct)` | `Task<Event>` | Throws `FingerlyApiException` with `Status` for a non-2xx response. |
| `Events.ListAsync(EventListOptions, ct)` | `Task<EventPage>` | Options: `From`, `To` (`DateTimeOffset`), `Page`, `Limit`, `Visitor`, `Level`. |
| `FingerlyWebhook.Verify(secret, payload, timestamp, signature)` | `bool` | Five minutes of tolerance. `FingerlyWebhook.Parse` reads the envelope. |

Event properties are PascalCase: `ev.RequestId`, `ev.SuspectLevel`, `ev.OccurredAt`.
