# Create a gateway

Step by step, from an ingest URL that only stores events to a gateway that forwards them to your app, with filters, transformations, signing, timeouts and retries.

Source: https://docs.webhooker.eu/guides/create-gateway/

A **gateway** is the route from one ingest URL to one endpoint on your side. It
is made of two parts, and you configure both in the same flow:

- a **destination** — the endpoint that receives events: its URL, headers,
  authentication, timeout and retry schedule,
- a **connection** — the link between a source and that destination, plus the
  optional filter and transformation applied on the way.

One source can feed several gateways, and one destination can be reused by
several sources. Every event that passes the filter is delivered to every
connected destination independently: one slow endpoint never holds up another.

## Before you start

- A source with its [ingest URL](/guides/create-url/), and at least one event in
  its log. Delivering an event you can see is far easier to debug than
  delivering a hypothetical one.
- A publicly reachable HTTPS endpoint on your side. Private and internal
  addresses are rejected when you save the destination — `localhost`,
  `*.local`, `*.internal` and private IP ranges. For local development use the
  [CLI](/tutorials/local-development/) instead.

## 1. Open the source's Destinations tab

Open **Sources**, click the source, and select the **Destinations** tab. This is
where the source's gateways live. An empty tab has one button: **Add
destination**.

![The Destinations tab of a source](https://docs.webhooker.eu/screenshots/source-destinations-light.webp "A source's gateways: each row is one destination, its URL and the state of its circuit breaker.")

## 2. Choose new or existing

The **Add destination** dialog has two tabs:

- **Create new** — define a fresh endpoint. The dialog opens here while the
  workspace has no endpoint left to reuse, which is the case the first time.
- **Connect existing** — reuse an endpoint the workspace already has, so two
  sources feed the same handler without duplicating its settings. Once at least
  one endpoint exists, the dialog opens on this tab.

## 3. Fill in the endpoint

On **Create new**:

- **Name** — required. What you will see in the event log and in alerts.
  `billing-api`, `orders-worker`, `slack-relay`.
- **URL** — required, absolute, `http` or `https`. This is where Webhooker
  `POST`s each event.
- **Response timeout (seconds)** — how long Webhooker waits for your response
  before the attempt counts as failed. Default 10 seconds, allowed 1 to 300.
  Answer fast and do the work asynchronously: a timeout is a failure, and a
  failure costs you a retry.

## 4. Set authentication (recommended)

**Sign outbound requests** decides how your endpoint can tell that a request
really came from Webhooker and not from anyone who found the URL.

| Method | What Webhooker sends | When to use it |
| --- | --- | --- |
| Off | Nothing. | Only if the endpoint is already protected some other way. |
| HMAC signature | `X-Webhooker-Signature`, `X-Webhooker-Timestamp`, `X-Webhooker-Event-Id`. | The default choice. Your handler recomputes the signature. |
| API Key | A fixed secret in the header you name, `X-Api-Key` by default. | Your endpoint already authenticates by header. |
| Basic Auth | `Authorization: Basic …`. | Your endpoint is behind basic auth. |

Pick **HMAC signature**, generate a long random secret, paste it into **Signing
secret**, and keep a copy — after saving, the field shows `***` and the value is
never returned to the browser. The verification code for your handler is in
[verify our signature](/deliver/verify-signatures/).

Authentication headers are applied last, after any transformation, so a
transformation can never overwrite or strip them.

Save. The source's **Destinations** tab now lists the endpoint, and the
connection between the two is created with it. The gateway is live: the next
event that arrives is forwarded.

## 5. Tune retries and fixed headers (API only)

Two destination settings have no field in the dashboard yet and are set through
the [management API](/reference/api/#destinations):

- **`retry_policy`** — the backoff schedule as a list of delays. The default is
  `30s, 2m, 10m, 1h, 4h`: five retries after the first attempt, spanning about
  five and a half hours. Up to 10 intervals, each between 1 second and 24 hours.
  Every delay gets ±20% jitter, so a destination that comes back up is not hit by
  all queued retries in the same instant.
- **`custom_headers`** — headers sent verbatim on every delivery to this
  destination, for routing or tenancy hints such as
  `X-Environment: production`. Do not put secrets here; that is what
  authentication is for.

```bash
curl -X PATCH https://app.webhooker.eu/api/v1/destinations/$DESTINATION_ID \
  -H "Authorization: Bearer $WEBHOOKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "retry_policy": { "intervals_seconds": [10, 60, 300, 1800] },
        "custom_headers": { "X-Environment": "production" }
      }'
```

## 6. Confirm the first delivery

Send a test event from the provider, or replay one you already have from the
event log. Open the event and look at its delivery row:

| Status | Meaning |
| --- | --- |
| `pending` | Queued, waiting for its attempt. |
| `delivering` | An attempt is in flight. |
| `succeeded` | Your endpoint answered `2xx`. |
| `failed` | Your endpoint answered a `4xx` other than 408 or 429. Not retried. |
| `exhausted` | Every retry in the schedule failed. |
| `filtered` | The connection's filter did not match, so nothing was sent. |

Expand the row for the attempt history: attempt number, target URL, response
status, response body, error message and latency for each try. This is the
screen to open when a customer says "the webhook never arrived".

## 7. Filter what this gateway receives (optional)

By default a gateway receives every event the source accepts. To narrow it, open
the **Edit routing** action on the row in the **Destinations** tab and add
**Filter rules**: a field path, an operator and a value per row.

![The connection routing screen with filter rules](https://docs.webhooker.eu/screenshots/connection-editor-light.webp "Filter rules and header transforms are configured per connection, so the same destination can behave differently per source.")

A rule addresses one value in the request by path. Two roots are available:
`body.` for the JSON payload and `headers.` for the inbound headers.

```json
{
  "operator": "and",
  "rules": [
    { "path": "body.type", "op": "eq", "value": "invoice.paid" },
    { "path": "body.data.object.amount", "op": "gt", "value": 5000 }
  ]
}
```

Operators: `eq`, `ne`, `contains`, `exists`, `gt`, `lt`, `in`. Groups combine
with `and` or `or` and can be nested one level deep.

An event that does not match is recorded as `filtered` on that connection: it is
visible in the log and never sent. Other connections on the same source are
unaffected.

## 8. Reshape the payload (optional)

The same editor holds the transformation, applied per gateway just before
delivery:

- **Set headers** — add or overwrite a header. Values support the placeholders
  `{{event.id}}`, `{{source.name}}` and `{{timestamp}}`.
- **Rename headers** — rename a header that is already being sent, keeping its
  value.
- **Remove headers** — a comma-separated list of names to strip from the
  outbound request.
- **Merge body fields** — JSON object of top-level fields added to a JSON body.
  Existing fields with the same name are overwritten; the rest of the payload is
  untouched.

A non-JSON body is forwarded byte for byte; header rules still apply.

## 9. Fan out to more endpoints

Repeat from step 2 to add another destination to the same source. Each gateway
keeps its own filter, transformation, signing secret, timeout, retry schedule and
delivery history.

To feed an existing endpoint from a second source, use **Connect existing** on
that source instead of recreating it. A source and a destination can only be
connected once; a second attempt is rejected as a conflict.

The toggle on each row enables or disables a gateway without deleting it:
disabled means events are stored and simply not forwarded here.

## 10. Know when it breaks

Two safety nets are worth setting up now:

- **Alerts** — email or Telegram on delivery failure, per source or workspace
  wide. See [alerts](/account/alerts/).
- **The circuit breaker** — after 5 consecutive retryable failures, the
  destination's circuit opens and deliveries pause for a cooldown that starts at
  60 seconds and doubles up to 10 minutes. One probe is then sent; if it fails,
  the circuit re-opens. The state is shown on the destination row as `closed`,
  `open` or `half_open`. This protects a struggling endpoint from the full retry
  load, and it clears itself as soon as one attempt succeeds.

Deliveries that end as `exhausted` or `failed` collect in the source's **Dead
letters** tab, where you can replay them in bulk once the endpoint is healthy.

## Troubleshooting

| Symptom | Cause |
| --- | --- |
| "url must not point to a private or internal address" | The endpoint is not publicly reachable. Use the [CLI](/tutorials/local-development/) for localhost. |
| Deliveries stay `pending` | The source is paused, or the destination's circuit is open. |
| Everything is `filtered` | A filter path does not match the payload. Check a real body in the event log and remember the `body.` / `headers.` prefix. |
| `failed` on the first attempt, never retried | Your endpoint answered a `4xx`. Only 408 and 429 are retried. |
| Signature check fails in your handler | Sign `{timestamp}.{raw body}` and use the raw bytes, not a re-serialised object. |
| "re-enter the secret to change it" | The masked `***` placeholder was submitted. Type the secret in full. |

## Next

- [Verify our signature](/deliver/verify-signatures/) — working handler code.
- [Retries and replay](/deliver/retries-and-replay/) — failures, the DLQ, replay.
- [Filters and transformations](/deliver/filters-and-transformations/) — the full rule syntax.
