# Destinations

Source: https://solidhooks.com/docs/destinations

> Define where SolidHooks delivers events, how each request is shaped and signed, and what happens when an endpoint stops answering.

A destination is an outbound HTTP endpoint that receives your events: your own server, a queue consumer, another SaaS. It holds the URL, the method, any headers you want sent, an optional delivery rate limit and a signing secret. It does not decide which events arrive. [Connections](https://solidhooks.com/docs/connections) do that.

Destination IDs start with `dest_`. Every destination lives in one project.

## Types

| `type`    | URL                                      | When to use it                                                                            |
| --------- | ---------------------------------------- | ----------------------------------------------------------------------------------------- |
| `http`    | A publicly routable URL.                 | The endpoint is on the internet.                                                          |
| `private` | An in-network URL, plus a `connectorId`. | The endpoint sits inside a private network or cluster and is reached through a connector. |

`connectorId` is required for `private` and rejected for `http`. For private destinations the `url` is still validated as a URL, but it only has to resolve inside your network. See [Private delivery](https://solidhooks.com/docs/private-delivery) for connectors.

For an `http` destination, SolidHooks refuses to store a URL that resolves to a private or reserved address.

## Fields

| Field             | Notes                                                                                                                                                                                     |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`            | Required, up to 100 characters.                                                                                                                                                           |
| `description`     | Optional, up to 500 characters.                                                                                                                                                           |
| `type`            | `http` or `private`.                                                                                                                                                                      |
| `connectorId`     | Only for `private`.                                                                                                                                                                       |
| `url`             | Must be a valid URL.                                                                                                                                                                      |
| `method`          | `POST`, `PUT` or `PATCH`.                                                                                                                                                                 |
| `headers`         | Array of `{ "value": "Name: Value", "sensitive": boolean }`.                                                                                                                              |
| `forwardHeaders`  | When true, the inbound source request headers are forwarded, each renamed `x-forwarded-src-<name>`. Sensitive headers such as `Authorization`, `Cookie` and API keys are always excluded. |
| `timeoutMs`       | 1000 to 30000. Defaults to 20000 (20 seconds) when unset.                                                                                                                                 |
| `rateLimit`       | A whole number of 1 or more.                                                                                                                                                              |
| `rateLimitPeriod` | `second`, `minute`, `hour` or `day`.                                                                                                                                                      |

`rateLimit` and `rateLimitPeriod` must be set together or both left empty. Send `null` for both to remove a limit. See [Rate limits](https://solidhooks.com/docs/rate-limits) for how this layers with your plan's delivery throughput.

### Headers

Each header is a single string in `Name: Value` form. Mark a header `sensitive` and it is encrypted at rest and never returned by the API. Non-sensitive headers are returned as written.

On update, resubmitting a sensitive header with an empty value keeps the stored value, so a form that never saw the secret does not wipe it.

Besides your headers, every delivery carries these platform headers: `x-source-id`, `x-event-id`, `x-organization-id`, `x-destination-id` and `x-delivery-id`.

## Create a destination

**API**

```bash
curl -s "$API/v1/destinations" \
  -H "authorization: Bearer $SDHK_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "name": "My app",
    "type": "http",
    "url": "https://myapp.example.com/webhooks",
    "method": "POST",
    "headers": [{ "value": "x-internal-token: supersecret", "sensitive": true }],
    "rateLimit": 10,
    "rateLimitPeriod": "second",
    "timeoutMs": 15000
  }'
```

The response includes the destination `id`. Update with `PATCH /v1/destinations/{id}`, putting only the fields that change inside `data`: `{ "data": { "timeoutMs": 10000 } }`.

**Dashboard**

1. Open **Destinations** and click **New Destination**.
2. Choose the type, fill in the URL, method and any headers. Use the lock on a header to mark it sensitive.
3. Optionally enable a delivery rate limit.
4. Click **Create destination**.

The signing secret is managed from the destination page, not the create form.

## Signing at a glance

Every destination gets a signing secret (`whsec_...`) when it is created, and every delivery it receives is signed. The `signatureScheme` field tells you how:

* `standard_webhooks` is the default for new destinations. Deliveries carry `webhook-id`, `webhook-timestamp` and `webhook-signature`.
* `solidhooks_v1` is the older scheme, kept for destinations created before Standard Webhooks became the default.

Reveal the secret with `GET /v1/destinations/{id}/signing-secret` and rotate it with `POST /v1/destinations/{id}/rotate-signing-secret`. Both need a key scoped `destination: read & write`. How to verify a signature, rotation overlap and switching scheme are covered in [Delivery signatures](https://solidhooks.com/docs/delivery-signatures).

## Send a test delivery

`POST /v1/destinations/{id}/test-delivery` posts a signed sample payload to the destination and reports what came back. It is shaped like a real delivery (same method, headers, signing and timeout) but carries `x-solidhooks-test: true`, records no event or delivery, and is never billed.

```bash
curl -s -X POST "$API/v1/destinations/dest_.../test-delivery" \
  -H "authorization: Bearer $SDHK_KEY"
```

```json
{
  "delivered": true,
  "status": 200,
  "responseTimeMs": 84,
  "responseBody": "ok",
  "error": null,
  "signed": true
}
```

`delivered` means the receiver answered at all, not that it accepted the request. A connection failure comes back in `error` with a 200 response from SolidHooks, not as an API error.

In the dashboard, open the destination and use **Send test** under **Send a test delivery**.

> **DESTINATION_NOT_PUBLIC**
>
> If an `http` destination's URL resolves into a private or reserved range (loopback, RFC1918, CGNAT, link-local including cloud metadata), the test is refused with 400 `DESTINATION_NOT_PUBLIC`. SolidHooks will not dial those addresses on your behalf. Point the destination at a publicly routable address, or make it a `private` destination routed through a connector.

A disabled destination also refuses a test with 400. Enable it first.

## 410 Gone disables a destination

A destination that answers `410 Gone` is treated as retired. Two consecutive 410 responses disable the destination and email the organisation's owners and admins. Any other response in between resets the count to zero, so a 410, 500, 410 sequence is a flapping endpoint, not a retired one.

Editing a destination resets the count when the change affects what is sent or where: `url`, `method`, `headers`, `forwardHeaders`, `type`, `connectorId`, or the signing secret or scheme. A 410 that was already in flight against the old configuration is discarded rather than counted against the new one. Changing only `rateLimit`, `rateLimitPeriod`, `timeoutMs` or the disabled flag does not reset it.

Enabling a destination does not clear the count. If you re-enable one without fixing the cause, the next 410 can disable it again immediately.

How other status codes are handled is on [Retries](https://solidhooks.com/docs/retries).

## Disable and enable

A disabled destination stops receiving deliveries. Its connections stay in place, so enabling it resumes delivery without reconfiguring anything. Deliveries that reach a disabled destination are recorded as `skipped`, and new events no longer match connections that point at it.

**API**

```bash
curl -s -X POST "$API/v1/destinations/dest_.../toggle-disabled" \
  -H "authorization: Bearer $SDHK_KEY" \
  -H 'content-type: application/json' \
  -d '{ "isDisabled": true }'
```

Send `"isDisabled": false` to enable it again.

**Dashboard**

Open the destination and use **Disable** (confirm in the dialog) or **Enable**. A disabled destination shows a banner saying deliveries are paused, and its settings are locked until you enable it.

## Delivery health

`GET /v1/destinations/{id}/delivery-health` returns delivery outcome counts for the destination since an instant (`since`, default the last 24 hours): `total`, `succeeded`, `failed`, `pending`, `skipped` and `since`. Counts are anchored on when each delivery was created.

## Delete a destination

`DELETE /v1/destinations/{id}` fails with 400 while any connection still references the destination. Delete its connections first. Deleting needs a key scoped `destination: read & write`; an unscoped key cannot delete.

In the dashboard, use **Delete** on the destination page and confirm.

## Next steps

* [Connections](https://solidhooks.com/docs/connections) — route a source to this destination
* [Deliveries](https://solidhooks.com/docs/deliveries) — follow what was sent and how it went
* [Delivery signatures](https://solidhooks.com/docs/delivery-signatures) — verify what the destination receives
* [Private delivery](https://solidhooks.com/docs/private-delivery) — reach endpoints inside your network
* [Retries](https://solidhooks.com/docs/retries) — what happens when a destination fails

API reference: [Destinations](https://api.solidhooks.com/v1/#tag/destinations).