---
title: Purge targets
description: Send purges to your frontend as a signed webhook, to Cloudflare, or to anything else through your own target.
seo:
  image: /og/targets.png
---

A target is where the package sends purges. Add targets in the backoffice under **Settings > Advanced > Cache tags > Targets**, or by name under `CacheTags:Targets` in `appsettings.json`. The built-in types are `webhook`, the default, and `cloudflare`. You can register your own.

```json appsettings.json
{
  "CacheTags": {
    "Targets": {
      "nextjs": { "Url": "https://www.example.com/api/cache-tags", "Secret": "<at least 32 characters>" },
      "cdn": {
        "Type": "cloudflare",
        "Settings": { "ZoneId": "<zone id>", "ApiToken": "<token>" }
      }
    }
  }
}
```

Every target gets every purge. Set `"Enabled": false` to pause a target without removing it.

## In the backoffice

Administrators add, edit, test and remove targets under **Settings > Advanced > Cache tags > Targets**. The tab also lists the targets in `appsettings.json`, read-only and without their secrets or header values.

Each value, whether the signing secret, a header or a Cloudflare token, is either typed in or names a secret under `CacheTags:Secrets` in the site's configuration. The package reads a named secret when it sends a purge.

```json appsettings.json
{
  "CacheTags": {
    "Secrets": {
      "VercelSecret": "<at least 32 characters>",
      "VercelBypass": "<bypass secret>"
    }
  }
}
```

```sh Environment variables
CacheTags__Secrets__VercelSecret=<at least 32 characters>
CacheTags__Secrets__VercelBypass=<bypass secret>
```

A target that names `VercelBypass` reads `CacheTags:Secrets:VercelBypass`, and nothing outside that section. A secret's name uses letters, digits and underscores, so it can always be set as an environment variable. So an administrator can send a target only the values you put under `CacheTags:Secrets`, never the database connection string or other settings.

:::caution[Keep secrets out of the database]
The package stores a typed value in the database in plain text, and shows it to administrators. Put secrets under `CacheTags:Secrets`, ideally as environment variables or in your host's secret store, and name them instead.
:::

- **A missing secret fails the purge, not the save.** If a secret that a target names is not set on a server, the target shows a problem. Its purges from that server fail with an error that names the header or signing secret, never the secret's name. The package retries them with backoff, up to 10 tries over about 17 minutes, and then lists them under **Failed purges**, where **Retry** sends them again once the secret is deployed.
- **Send test purge** sends the target a purge of `x-cache-tags-test`, a key nothing carries, and says whether the target accepted it. It works before you save.
- **The name never changes**, because the package queues purges under it. Deleting a target sets its unsent purges aside.
- **A name in `appsettings.json` wins.** The package ignores a backoffice target with the same name, and logs a warning.
- **Other servers** pick up a change to a target within 5 seconds.

### Public addresses only

A backoffice target can only connect to public internet addresses, so an administrator cannot point one at a service inside your network. Set `CacheTags:AllowPrivateNetworkTargets` to `true` to allow private addresses, such as `localhost` in development.

The limit doesn't apply to targets in `appsettings.json`, or to target types you register, which make their own requests.

To check the address it connects to, a backoffice target connects directly, not through `HTTPS_PROXY` or the system proxy, unless `AllowPrivateNetworkTargets` is on. If your site reaches the internet only through a proxy, configure its targets in `appsettings.json`, which use the proxy as usual.

## Request headers

A webhook can send its own headers with every purge, such as a gateway's API key, or `x-vercel-protection-bypass` for a protected Vercel deployment:

```json appsettings.json
"nextjs": {
  "Url": "https://www.example.com/api/cache-tags",
  "Secret": "<at least 32 characters>",
  "Headers": { "x-vercel-protection-bypass": "<bypass secret>" }
}
```

A target sends at most 20 headers, each value on one line. The package refuses headers it sets itself, such as `Host`, `Content-Type`, `User-Agent` and the signature headers. The signature, not a header, proves that a purge came from Umbraco.

## The webhook

```http
POST https://www.example.com/api/cache-tags
Content-Type: application/json
User-Agent: TheBuilder.CacheTags/<version>
X-Cache-Tags-Timestamp: 1792663200
X-Cache-Tags-Signature: sha256=<lowercase hex>

{
  "version": 1,
  "id": "<purge id>",
  "changeId": "<change id>",
  "expire": ["content-e000...", "x-dictionary-da-dk"],
  "stale": ["type-article", "children-e000..."]
}
```

- **`X-Cache-Tags-Timestamp`** · header · `integer`

  When the package signed the purge, in Unix seconds. Refuse a purge more than 5 minutes off your clock.

- **`X-Cache-Tags-Signature`** · header · `string`

  `sha256=` and the lowercase hex HMAC-SHA256 of `POST\n<path>\n<timestamp>\n<raw body>`, keyed with the secret's UTF-8 bytes.

- **`version`** · body · `integer`

  Always `1` for now. A receiver that does not know the version must answer with an error, so the purge fails and shows under **Failed purges** instead of being lost.

- **`id`** · body · `string`

  The purge. A retry sends the same id, so a receiver can ignore a purge it has already handled.

- **`changeId`** · body · `string`

  The change the purge is for. One change can send several purges.

- **`expire`** · body · `string[]`

  Keys that name one item or a site's own output: `content-`, `media-` and `x-` keys. Purge them now.

- **`stale`** · body · `string[]`

  Listing keys (`type-`, `children-`, `search-`) and `umbraco`. Serve them stale while they refresh if the receiver can, or purge them too.

Any 2xx answer counts as delivered. Anything else, or no answer within 10 seconds, counts as a failure, and the package tries again after 2, 4, 8 and more seconds.

- Use a secret of at least 32 characters, such as one from `openssl rand -base64 32`, and a different one in each environment. The signature does not cover the host, so with the same secret and path, a purge for one receiver is valid at another.
- The URL must be https, except on localhost. The package does not follow redirects.

The [Next.js guide](/nextjs#the-purge-webhook) has a receiver to copy.

## Cloudflare

The `cloudflare` type purges a Cloudflare zone by cache tag. Create an API token with the **Zone > Cache Purge** permission for the zone.

Cloudflare reads tags from a `Cache-Tag` response header. The package posts the keys to `POST /client/v4/zones/{zone}/purge_cache` as `tags`, at most 100 per call. If a call fails, for example because of a bad token or a rate limit, the package retries it like any purge, and the backoffice shows the error with the purge.

:::note
Cloudflare has no "serve stale" purge, so the package purges the `stale` keys at once too.
:::

### Cloudflare in front of your frontend

This is the setup to prefer, with the Delivery API called from the frontend's server only. The frontend copies each response's keys into its own `Cache-Tag` header. Add Cloudflare as a target alongside your frontend's webhook, since both caches hold the page.

### Cloudflare in front of the Delivery API

This works for a public Delivery API only, since Cloudflare would serve a response fetched with an API key to anyone, and browsers can still call the API directly. Set `CacheTags:HeaderName` to `Cache-Tag`, and raise `CacheTags:MaxHeaderKeys` to about 300: Cloudflare keeps 16 KB of tags per response. A proxy between Cloudflare and Umbraco must then allow response headers of about 15 KB, such as nginx with a `proxy_buffer_size` of 16k.

## Your own target

Implement `IPurgeTarget` for another CDN or cache, and register it as a singleton:

```csharp
public sealed class FastlyTarget(IHttpClientFactory http) : IPurgeTarget
{
    public string Type => "fastly";

    public async Task<string?> SendAsync(PurgeRequest request, CacheTagsTargetOptions target, CancellationToken cancellationToken)
    {
        var keys = request.Expire.Concat(request.Stale);
        // Send the keys with target.Settings["ServiceId"] and target.Settings["ApiToken"].
        // Return null on success, or what went wrong, and the package retries with backoff.
        return null;
    }
}

builder.Services.AddSingleton<IPurgeTarget, FastlyTarget>();
```

Then configure a target with `"Type": "fastly"` and its `Settings`, in `appsettings.json` or the backoffice. The target's `Headers` are available too, if your type uses them.
