# Management SDK - Webhooks

> Source: https://agilitycms.com/docs/dotNet/management-sdk-dotnet-webhooks

`client.Webhooks` manages an instance's webhooks: creating and changing them, their signing secrets, and their delivery history.

> **Note:** Webhook management is new in 2.0. If you're upgrading from 1.x, see [Migrating to 2.0](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-migrating-to-2).

This article covers the SDK calls. For how webhooks behave (the events, the payload, signed delivery, retries and delivery history), see [Webhooks](https://agilitycms.com/docs/developers/webhooks). For every event and its payload, the request headers and the delivery history fields in one place, see the [Webhook Events and Payload Reference](https://agilitycms.com/docs/developers/webhook-events-and-payloads). To check signatures in the endpoint that receives deliveries, see [Verifying Signed Webhooks](https://agilitycms.com/docs/developers/verifying-signed-webhooks).

The examples assume a `client` created as shown in the [Intro](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-intro), and `guid` set to your instance GUID. The webhook types are in `Agility.Management.Sdk.Models`:

```csharp
using Agility.Management.Sdk;
using Agility.Management.Sdk.Models;
```

A webhook's ID is its `RowKey`, a string. Every method also takes an optional `CancellationToken cancellationToken` as its last parameter. The signatures below leave it out.

## Creating webhooks

### Create a webhook

`SaveWebhookAsync` creates a webhook when `RowKey` is empty, and returns the saved webhook.

```csharp
Webhook webhook = await client.Webhooks.SaveWebhookAsync(guid, new Webhook
{
    Name = "Rebuild site",
    Url = "https://build.example.com/hooks/agility",
    Enabled = true,
    ContentPublishEvents = true,
    ContentSaveEvents = false,
    ContentWorkflowEvents = false,
    SecureDeliveryEnabled = true,   // sign every delivery
});

if (webhook.SigningSecretJustCreated)
{
    StoreSecret(webhook.SigningSecret!);
}

Console.WriteLine($"Created webhook {webhook.RowKey}");
```

Choose which events the webhook receives:

| Property | Sends a delivery when |
|---|---|
| `ContentPublishEvents` | A content item or page is published or unpublished |
| `ContentSaveEvents` | A content item or page is saved or deleted |
| `ContentWorkflowEvents` | Content is requested for approval, approved or declined |

**Signature:** `Task<Webhook> SaveWebhookAsync(string instanceGuid, Webhook webhook)`

---

### Signed delivery

Set `SecureDeliveryEnabled = true` to have Agility sign every delivery with the webhook's signing secret, following the [Standard Webhooks](https://www.standardwebhooks.com/) spec. Your endpoint then checks the signature with a Standard Webhooks library. See [Verifying Signed Webhooks](https://agilitycms.com/docs/developers/verifying-signed-webhooks) for how.

The first save with secure delivery turned on creates the signing secret (`whsec_...`):

- The response from that save has `SigningSecretJustCreated` set to `true` and contains the secret in full. Store it then.
- Later reads return the secret in full to users with full permission on the instance's webhooks, and masked to everyone else.
- The API always creates the secret. A `SigningSecret` you send on a save is ignored.

Secure delivery is off by default, so a webhook stays unsigned until you turn it on.

---

### Retries

By default, Agility tries each delivery once. To retry failed deliveries, set these properties on the webhook:

```csharp
webhook.RetriesEnabled = true;
webhook.RetryCount = 4;            // retries after the first attempt: 1 to 8
webhook.RetrySpeed = "standard";   // "fast", "standard" or "slow"
```

| Property | What it does |
|---|---|
| `RetriesEnabled` | Retry failed deliveries. Default `false`. |
| `RetryCount` | How many retries follow the first attempt, from 1 to 8. |
| `RetrySpeed` | How quickly retries back off: `"fast"` (from about 30 seconds), `"standard"` (about 5 minutes) or `"slow"` (about 30 minutes). Delays grow exponentially, with jitter, up to 24 hours. |

Then save the webhook, as shown under **Update a webhook** below. For what counts as a failed delivery, see [Webhooks](https://agilitycms.com/docs/developers/webhooks).

## Reading webhooks

### Get a webhook

```csharp
Webhook webhook = await client.Webhooks.GetWebhookAsync(guid, webhookId);
Console.WriteLine($"{webhook.Name} -> {webhook.Url} (enabled: {webhook.Enabled})");
```

**Signature:** `Task<Webhook> GetWebhookAsync(string instanceGuid, string webhookId)`

---

### List webhooks

`GetWebhooksAsync` returns one page of webhooks and a continuation token. Pass the token back to get the next page. It's `null` after the last page.

```csharp
string? token = null;
do
{
    WebhookContinuationListing page = await client.Webhooks.GetWebhooksAsync(guid, take: 50, continuationToken: token);

    foreach (var hook in page.Items ?? [])
    {
        Console.WriteLine($"{hook.RowKey}: {hook.Name}");
    }

    token = page.Token;
}
while (token is not null);
```

`take` is the number of webhooks per page. If you leave it `null`, the API returns 20.

**Signature:** `Task<WebhookContinuationListing> GetWebhooksAsync(string instanceGuid, int? take = null, string? continuationToken = null)`

## Updating webhooks

### Update a webhook

To change a webhook, get it, change it, and save it back. Keep the `RowKey` so the save updates the webhook instead of creating a new one.

```csharp
Webhook webhook = await client.Webhooks.GetWebhookAsync(guid, webhookId);

webhook.ContentSaveEvents = true;
webhook.RetriesEnabled = true;
webhook.RetryCount = 4;
webhook.RetrySpeed = "standard";

Webhook updated = await client.Webhooks.SaveWebhookAsync(guid, webhook);
```

> ⚠️ **Start from the saved webhook.** The on/off settings (`Enabled`, the event settings, `SecureDeliveryEnabled`, `RetriesEnabled`) are always sent. A `Webhook` you build from scratch with only a `RowKey` and one change sends `false` for the rest, which turns them off.

---

### Rotate the signing secret

If a secret is exposed, rotate it. `RotateSigningSecretAsync` creates a new secret and returns the webhook with it.

```csharp
Webhook rotated = await client.Webhooks.RotateSigningSecretAsync(guid, webhook.RowKey!);
StoreSecret(rotated.SigningSecret!);
```

For 24 hours afterwards, deliveries are signed with both the new secret and the previous one (`PreviousSigningSecret`), so you can deploy the new secret to your endpoint without dropping deliveries. `SecretRolledUtc` says when the secret was last rotated. Rotating a secret needs **Full Control** permission.

**Signature:** `Task<Webhook> RotateSigningSecretAsync(string instanceGuid, string webhookId)`

## Delivery history

### Get a webhook's deliveries

`GetWebhookHistoryAsync` lists a webhook's deliveries, newest first, a page at a time. Page through it with the continuation token, as with `GetWebhooksAsync`.

```csharp
string? token = null;
do
{
    WebhookHistoryContinuationListing page = await client.Webhooks.GetWebhookHistoryAsync(guid, webhookId,
        fromDate: DateTime.UtcNow.AddDays(-1), take: 100, continuationToken: token);

    foreach (var delivery in page.Items ?? [])
    {
        Console.WriteLine($"{delivery.QueuedDate:u} {delivery.HttpResponseCode} attempt {delivery.AttemptCount} {delivery.LastError}");
    }

    token = page.Token;
}
while (token is not null);
```

| Parameter | What it does |
|---|---|
| `fromDate` | Start of the date range. Default: 7 days before `toDate`. |
| `toDate` | End of the date range. Default: today. The range can span at most 366 days. |
| `take` | Deliveries per page, at most 100. Default: 20. |
| `continuationToken` | The token from the previous page. |

Each delivery is a `WebhookHistory`. Useful properties:

| Property | What it holds |
|---|---|
| `RowKey` | The delivery ID. It's the value your endpoint received in the `webhook-id` header. |
| `QueuedDate`, `LastAttemptDate` | When the delivery was queued, and when it was last attempted |
| `SendDate` | When an attempt succeeded. Set only after a 2xx response. |
| `HttpResponseCode` | The HTTP status of the last attempt. `0` means the request itself failed, for example a timeout. |
| `Success` | Whether the delivery succeeded |
| `AttemptCount` | How many attempts were made. `1` is the first attempt. |
| `NextAttemptUtc` | When the next retry is due, if one is scheduled |
| `LastError` | A short description of the last failure |
| `Payload`, `ResponseText` | The body that was sent, and your endpoint's response |
| `Signed`, `SignatureKeyCount` | Whether the last attempt was signed, and with how many signatures (2 during the 24 hours after a secret rotation) |

For how long history is kept, see [Verifying Signed Webhooks](https://agilitycms.com/docs/developers/verifying-signed-webhooks).

**Signature:** `Task<WebhookHistoryContinuationListing> GetWebhookHistoryAsync(string instanceGuid, string webhookId, DateTime? fromDate = null, DateTime? toDate = null, int? take = null, string? continuationToken = null)`

## Deleting webhooks

### Delete a webhook

```csharp
await client.Webhooks.DeleteWebhookAsync(guid, webhook.RowKey!);
```

The method returns nothing. If the delete fails, it throws an `AgilityManagementException`.

**Signature:** `Task DeleteWebhookAsync(string instanceGuid, string webhookId)`

## Error handling

Every error from the API or the network throws an `AgilityManagementException`. It carries the HTTP status code, the API's own message and a request ID.

```csharp
try
{
    await client.Webhooks.SaveWebhookAsync(guid, webhook);
}
catch (AgilityManagementException ex)
{
    Console.Error.WriteLine($"{ex.StatusCode}: {ex.ApiMessage} (request {ex.RequestId})");
}
```

Reads are retried automatically on temporary failures. Saves, rotations and deletes are never retried. See the [Intro](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-intro) for the full list of exceptions and the retry rules.
