> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hookie.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Destination options

> Send custom headers, authentication and your own JSON body to a destination, and know when one keeps failing.

A destination sends every matching event to an HTTPS URL, signed and retried. By default the body is Hookie's envelope, `{id, dataset, received_at, data}`. Each destination can also carry its own headers, a credential and a body template, so you can send straight to services such as Slack, Datadog or an email API without a relay in between.

Set these on the destination's tile under **Request options**, or when you create it. **From preset** fills them in for Slack, Datadog and email.

## Custom headers

Add up to 20 headers. Mark a header **Secret** when its value is a credential: it is stored encrypted and is never shown again, in the console, the API or the audit log. To keep a saved secret header while you edit others, leave its value blank.

Hookie sets some headers itself, and you cannot override them:

* `Hookie-*`, `X-Hookie-*` and `webhook-*`
* `Content-Type`, `Content-Length`, `Host`, `User-Agent` and `Idempotency-Key`
* hop-by-hop headers such as `Connection`, `Transfer-Encoding` and `Upgrade`

## Authentication

Choose one:

| Type | What is sent |
| - | - |
| **Bearer token** | `Authorization: Bearer <token>` |
| **Basic** | `Authorization: Basic <base64 of username:password>` |
| **API key in a header** | `<the header you name>: <key>`, for example `DD-API-KEY` |

The credential is stored encrypted and is never returned. The destination shows only the type, and for an API key, the header name. To change the header without re-entering the key, leave the key blank.

## Body

| Body | What is sent |
| - | - |
| **Hookie envelope** (default) | `{"id", "dataset", "received_at", "data"}` |
| **JSON template** | Your JSON, with `{{path}}` placeholders filled from the envelope |
| **Field mappings** | A flat object, one `key = path` per line, built the way routing rules build a record |

A placeholder is a dotted path into the envelope: `{{dataset}}`, `{{id}}`, `{{received_at}}`, `{{data.customer.email}}`. `{{data}}` alone is the whole payload.

```json theme={"dark"}
{
  "text": "Order {{data.order_id}} paid by {{data.customer.email}}",
  "amount": "{{data.total}}",
  "raw": "{{data}}"
}
```

A string that is exactly one placeholder keeps the value's type, so `"{{data.total}}"` sends a number. A placeholder inside a longer string becomes text, and a missing value becomes an empty string.

Templates are data, never code. A placeholder is a path lookup, and nothing is evaluated.

<Note>
  The body is always JSON, and `Hookie-Signature` is computed over the body exactly as sent. Verify a templated body the same way as the envelope (see [Verify signatures](/signatures)).
</Note>

## When a destination keeps failing

A delivery goes **dead** when its retries run out, or at once when your receiver refuses it with a 4xx that is never retried, such as 400, 401 or 403. Retrying deliveries do not count.

1. At the first dead delivery, the destination is flagged as **failing**, with the last error, on its tile and on **Home**.
2. After **10 dead deliveries in a row over at least an hour**, Hookie switches the destination off. The reason is shown on its tile and on Home, and the audit log records `destination_auto_disabled`.
3. While it is off, new deliveries are **held**, not lost. Click **Re-enable** on Home or on the tile to send them and start the count again.

A single delivery that arrives ends a failing streak at any point.

### Alert webhook

Set an **Alert URL** on the destination to be told when Hookie switches it off. Hookie POSTs one notice to it:

```json theme={"dark"}
{
  "type": "destination.disabled",
  "occurred_at": "2026-09-29T12:00:00.000Z",
  "destination": { "id": "…", "name": "Datadog events", "url": "https://api.datadoghq.com/api/v1/events", "project_id": "…" },
  "reason": "Disabled automatically: …",
  "consecutive_dead": 10,
  "failing_since": "2026-09-29T10:41:07.000Z",
  "last_failure": "HTTP 403 (not retried: the receiver refused the request)"
}
```

The notice carries `Hookie-Signature`, signed with the destination's own signing secret, and `Hookie-Alert: destination.disabled`. It is sent once and is not retried. The alert URL must be a public `https` address.

Hookie does not send email alerts.

## When your plan changes

Each plan caps how many destinations and database sources a workspace runs, across all its projects, and how often a source may poll:

| Plan | Destinations | Database sources | Fastest source poll |
| - | - | - | - |
| Free | 1 | 0 | every hour |
| Pro | 10 | 3 | every 60 seconds |
| Team | 25 | 10 | every 30 seconds |

When the workspace moves to a plan with lower caps (a downgrade, a cancellation, or a trial ending), Hookie brings what is running in line with the new plan straight away:

* **Destinations and sources over the cap are paused, newest first.** The oldest keep running. A paused one shows a **Paused: over plan limit** tag, with a link to billing. A paused destination's deliveries wait as `paused`, like any disabled destination's, rather than being lost.
* **Poll intervals below the new minimum are raised to it.** They are not lowered again if you upgrade later; set them yourself.
* One `plan_limits_applied` entry in **Settings → Audit log** lists everything that was paused, resumed or slowed.

When the plan rises again, only what Hookie paused is resumed, oldest first, as far as the new caps allow. A resumed destination sends its waiting deliveries, and a resumed source starts polling again.

**What you switched off stays off.** Hookie never turns on a destination or source you disabled yourself, on any plan change. Switching one on or off yourself also takes it out of Hookie's hands, so it is not resumed automatically later. Turning one on while the workspace already runs its cap is refused with `402`: disable another first, or upgrade.

A destination that belongs to a [customer portal](/customer-portal) customer is paused the same way. The customer cannot turn it back on from the portal while it is over the limit, and is told to contact you.

Over the API, destinations and sources carry `paused_reason`: `"over_plan_limit"` when Hookie paused them for the plan, `null` otherwise.

## API

`POST` and `PATCH /admin/api/projects/{pid}/destinations` take `headers`, `auth`, `transform` and `alert_url`. `PATCH` with `{"enabled": true}` re-enables a destination and clears its failing state.

**Edit or delete.** A destination's **Edit…** changes its name, URL and dataset filter; over the API, `PATCH` takes `name` and `url` too. It stays the same destination, with the same id and signing secret, so your receiver keeps verifying. A new URL must be `https`, and a destination that belongs to a [customer portal](/customer-portal) customer keeps its URL (`409`): it is theirs to change. **Delete** removes a destination that has never had a delivery outright. One with delivery history leaves the project for good, and its past deliveries keep its name, so the delivery log still says where each one went.

`GET /admin/api/dashboard/summary` lists failing destinations in `failing_destinations`. See the [API reference](/api-reference).
