---
title: "Verify webhooks"
description: "Check the signature of a delivery before you trust what it says."
url: "https://openemail.uk/docs/csharp/webhooks/verify"
area: "C#"
category: "Guides"
---

# Verify webhooks

Check the signature of a delivery before you trust what it says.

## Verify a delivery

**Program.cs**

```
using OpenEmail;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var secret = builder.Configuration["OPENEMAIL_WEBHOOK_SECRET"] ?? string.Empty;

app.MapPost("/webhooks/openemail", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    var payload = await reader.ReadToEndAsync();

    try
    {
        var delivery = OpenEmailClient.VerifyWebhookSignature(payload, request.Headers["X-OpenEmail-Signature"], secret);

        app.Logger.LogInformation("{Type} {Id}", (string?)delivery["type"], (string?)delivery["id"]);

        return Results.NoContent();
    }
    catch (OpenEmailWebhookException)
    {
        return Results.BadRequest();
    }
});

app.Run();
```

`OpenEmailClient.VerifyWebhookSignature()` checks the HMAC of the header over the timestamp and the body in constant time, and returns the event as a `JsonObject`. The second argument is the value of the `X-OpenEmail-Signature` header, and other overloads take the request headers themselves. The secret is the one `client.Webhooks.CreateAsync()` returned.

> Pass the raw body, exactly as it arrived. A body that was parsed and encoded again no longer matches its signature.

## Old deliveries

**tolerance.cs**

```
var payload = """{"id":"evt_1","type":"email.received"}""";

try
{
    OpenEmailClient.VerifyWebhookSignature(payload, "t=1767225600,v1=5f2d", "whsec_example", toleranceSeconds: 60);
}
catch (OpenEmailWebhookException error)
{
    Console.WriteLine($"refused: {error.Message}");
}
```

A delivery more than five minutes old is refused, so a captured request cannot be replayed later. `toleranceSeconds:` changes the five minutes, and zero accepts a delivery of any age.

- A missing or malformed header, a signature that does not match and a delivery that is too old all throw `OpenEmailWebhookException`.
- After a secret is rotated, a delivery can carry more than one signature, and any one that matches passes.
- Answer with a 2xx status quickly. A delivery that gets any other answer is tried again later.
