-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathwebhook.mdoc
More file actions
218 lines (167 loc) · 9.2 KB
/
Copy pathwebhook.mdoc
File metadata and controls
218 lines (167 loc) · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
---
title: "Webhook"
description: "Deliver events via HTTP POST to any URL endpoint, with customizable headers and HMAC signature verification."
---
Send events via HTTP POST to a URL endpoint. Outpost supports two webhook modes:
- **Default mode** — Customizable headers and signature format
- **Standard Webhooks mode** — Follows the [Standard Webhooks](https://www.standardwebhooks.com/) specification
## Creating a Webhook Destination
```sh
curl '{% $OUTPOST_API_BASE_URL %}/tenants/<TENANT_ID>/destinations' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"type": "webhook",
"topics": ["user.created", "user.updated"],
"config": {
"url": "https://example.com/webhooks"
}
}'
```
## Configuration
### Config
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `config.url` | string | Yes | The URL to send events to |
| `config.custom_headers` | string | No | JSON object of custom HTTP headers to include |
### Credentials
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `credentials.secret` | string | No | Signing secret — auto-generated if not provided |
| `credentials.previous_secret` | string | No | Previous secret during a rotation window |
| `credentials.previous_secret_invalid_at` | string | No | RFC 3339 timestamp when the previous secret expires |
If `secret` is not provided, one is auto-generated. Tenants can trigger secret rotation but cannot set secrets directly.
## Event Format
When you publish an event:
```json
{
"topic": "user.created",
"data": { "user_id": "usr_123", "email": "user@example.com" },
"metadata": { "source": "signup-service" }
}
```
Outpost sends an HTTP POST request:
```
POST /webhooks HTTP/1.1
Content-Type: application/json
x-outpost-event-id: evt_abc123
x-outpost-topic: user.created
x-outpost-timestamp: 2024-06-01T08:23:36Z
x-outpost-signature: v0=abc123def456...
x-outpost-source: signup-service
{"user_id": "usr_123", "email": "user@example.com"}
```
The request body contains the event's `data` field as JSON. The `metadata` field is translated to headers using the configured prefix.
### Event ID header and idempotency
Webhook delivery is **at-least-once** (see [Event delivery & retries](/docs/outpost/features/event-delivery)). Your handler should deduplicate using the **event id** — the same stable `id` you set when [publishing](/docs/outpost/publishing/events) (Outpost may redeliver on retries).
In **default** mode, that id is sent as the system **`event-id`** metadata field. The HTTP header name is **`{header_prefix}{metadata-key}`** with **no extra separator** — the prefix and key are concatenated as-is. So the default prefix `x-outpost-` plus `event-id` yields **`X-Outpost-Event-Id`**; a prefix without a trailing separator (for example `x-acme`) would produce `x-acme` + `event-id` → **`X-Acmeevent-Id`** (Go canonicalizes the wire name). Include a trailing hyphen in the prefix when you want a conventional shape like **`X-Acme-Event-Id`**. Change the prefix with **`DESTINATIONS_WEBHOOK_HEADER_PREFIX`**, or omit this header with **`DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_EVENT_ID_HEADER`**.
In **Standard Webhooks** mode, the same value is sent as the **`webhook-id`** header (default prefix `webhook-`, so typically **`Webhook-Id`**) per the [Standard Webhooks](https://www.standardwebhooks.com/) specification.
## Signatures
### Default Mode
The signature is computed over the timestamp and request body:
```
HMAC-SHA256(secret, "${body}")
```
The `x-outpost-signature` header value follows the format: `v0=${signature}`
To verify:
1. Extract the timestamp and signature from the header
2. Compute the expected signature using your secret
3. Compare signatures using a constant-time comparison
4. Optionally reject requests with old timestamps to prevent replay attacks
### Standard Webhooks Mode
Follows the [Standard Webhooks specification](https://www.standardwebhooks.com/):
```
base64(HMAC-SHA256(secret, "${webhook-id}.${timestamp}.${body}"))
```
Use the official [Standard Webhooks SDK](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries) to verify signatures. Secrets use the `whsec_<base64>` format.
{% tabs tabGroup="deployment" %}
{% tab label="Managed" %}
Enable Standard Webhooks mode by setting `DESTINATIONS_WEBHOOK_MODE=standard` in the Config API or in [Hookdeck Destinations settings](https://dashboard.hookdeck.com/settings/project/destinations).
{% /tab %}
{% tab label="Self-Hosted" %}
Enable Standard Webhooks mode:
```
DESTINATIONS_WEBHOOK_MODE=standard
```
{% /tab %}
{% /tabs %}
## Secret Rotation
Rotate a webhook secret without downtime. During the rotation window, both the old and new secrets produce valid signatures.
```sh
curl --request PATCH \
'{% $OUTPOST_API_BASE_URL %}/tenants/<TENANT_ID>/destinations/<DESTINATION_ID>' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"credentials": {
"rotate_secret": "true",
"previous_secret_invalid_at": "2025-01-15T00:00:00Z"
}
}'
```
When rotation is triggered:
1. The current secret becomes `previous_secret`
2. A new secret is generated
3. The previous secret remains valid until `previous_secret_invalid_at` (default: 24 hours)
4. Both secrets appear in the signature header during the rotation window
## Custom Headers
Tenants can add custom HTTP headers to webhook requests for authentication or routing:
```sh
curl '{% $OUTPOST_API_BASE_URL %}/tenants/<TENANT_ID>/destinations' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"type": "webhook",
"topics": ["*"],
"config": {
"url": "https://example.com/webhooks",
"custom_headers": "{\"x-api-key\": \"secret123\"}"
}
}'
```
Header names must start with a letter or digit and may contain letters, digits, underscores, and hyphens. The following headers cannot be overridden: `content-type`, `content-length`, `host`, `connection`, `user-agent`.
{% tabs tabGroup="deployment" %}
{% tab label="Managed" %}
Custom webhook headers in the tenant portal are disabled by default. Enable them in [Hookdeck User Portal settings](https://dashboard.hookdeck.com/settings/project/user-portal).
{% /tab %}
{% tab label="Self-Hosted" %}
Custom headers are disabled in the tenant portal by default. Enable with:
```
PORTAL_ENABLE_WEBHOOK_CUSTOM_HEADERS=true
```
{% /tab %}
{% /tabs %}
## Forward Proxy
Webhook deliveries can be routed through an HTTP forward proxy — useful for static-IP egress, network isolation, or centralized egress policy.
{% tabs tabGroup="deployment" %}
{% tab label="Managed" %}
Configure the proxy URL in [Hookdeck Destinations settings](https://dashboard.hookdeck.com/settings/project/destinations), or via the [Config API](/docs/outpost/api#configuration) using `DESTINATIONS_WEBHOOK_PROXY_URL`.
{% /tab %}
{% tab label="Self-Hosted" %}
Set `DESTINATIONS_WEBHOOK_PROXY_URL` to your proxy URL (basic auth supported). See [Webhook Forward Proxy](/docs/outpost/self-hosting/guides/webhook-proxy) for failure attribution, Envoy support, and a reference Envoy config.
{% /tab %}
{% /tabs %}
## Operator Configuration
{% tabs tabGroup="deployment" %}
{% tab label="Managed" %}
Configure webhook operator behavior using these keys in the Config API or in [Hookdeck Destinations settings](https://dashboard.hookdeck.com/settings/project/destinations): `DESTINATIONS_WEBHOOK_HEADER_PREFIX`, `DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_EVENT_ID_HEADER`, `DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TIMESTAMP_HEADER`, `DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TOPIC_HEADER`, `DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_SIGNATURE_HEADER`, `DESTINATIONS_WEBHOOK_MODE`, `DESTINATIONS_WEBHOOK_SIGNATURE_ALGORITHM`, `DESTINATIONS_WEBHOOK_SIGNATURE_ENCODING`, `DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE`, and `DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE`.
{% /tab %}
{% tab label="Self-Hosted" %}
### Header Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `DESTINATIONS_WEBHOOK_HEADER_PREFIX` | `x-outpost-` / `webhook-` | Prefix for system webhook headers (event id, topic, timestamp, signature). Unless overridden, defaults to **`x-outpost-`** when `DESTINATIONS_WEBHOOK_MODE` is `default` and **`webhook-`** when `standard`. |
| `DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_EVENT_ID_HEADER` | `false` | Disable the event ID header |
| `DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TIMESTAMP_HEADER` | `false` | Disable the timestamp header |
| `DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TOPIC_HEADER` | `false` | Disable the topic header |
| `DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_SIGNATURE_HEADER` | `false` | Disable the signature header |
### Signature Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `DESTINATIONS_WEBHOOK_MODE` | `default` | Set to `standard` for Standard Webhooks compliance |
| `DESTINATIONS_WEBHOOK_SIGNATURE_ALGORITHM` | `hmac-sha256` | Signature algorithm |
| `DESTINATIONS_WEBHOOK_SIGNATURE_ENCODING` | `hex` | Encoding: `hex` or `base64` |
| `DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE` | `{{.Body}}` | Template for signed content |
| `DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE` | `v0={{.Signatures \| join ","}}` | Template for signature header value |
{% /tab %}
{% /tabs %}