Webhook
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 specification
Creating a Webhook Destination
curl 'https://api.outpost.hookdeck.com/2025-07-01/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:
{
"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). Your handler should deduplicate using the event id — the same stable id you set when publishing (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 specification.
Signatures
Default Mode
The signature is computed over the request body:
HMAC-SHA256(secret, "${body}")
The x-outpost-signature header value follows the format: v0=${signature}
If the destination has an unexpired previous_secret during secret rotation, Outpost includes one signature for each valid secret in the same header. The current secret's signature is first, followed by the previous secret's signature: v0=<signature-from-current-secret>,<signature-from-previous-secret>
To verify:
- Extract the signature header
- Split the
v0=value on commas - Compute the expected signature using each active secret you accept
- Accept the request if any signature matches using a constant-time comparison
- Optionally reject requests with old timestamps to prevent replay attacks
Custom signature templates
In default mode, operators can customize both the signed content and the signature header value:
DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATEcontrols the string passed to HMAC.DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATEcontrols the value written to the signature header.
These use the same configuration key names in both deployment models, but they are applied differently: Managed Outpost stores them through the Config API or Hookdeck dashboard, while self-hosted Outpost reads them from environment variables or YAML configuration.
Templates use Go template syntax with helper functions such as join. The signature content template can use:
| Field | Description |
|---|---|
.EventID | Event id used for the delivery |
.Topic | Event topic |
.Timestamp | Delivery timestamp |
.Body | Raw request body |
The signature header template can use the same metadata plus .Signatures, which is the list of generated signatures for all valid secrets. During secret rotation, .Signatures contains the current secret's signature first and the previous secret's signature second.
For example, to include a Unix timestamp in the signed content and header:
Set the values in the Config API or in Hookdeck Destinations settings. For the Config API, send the configuration keys in the JSON body:
{
"DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE": "{{.Timestamp.Unix}}.{{.Body}}",
"DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE": "t={{.Timestamp.Unix}},v0={{.Signatures | join \",\"}}"
}
Set the values as environment variables:
DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE="{{.Timestamp.Unix}}.{{.Body}}"
DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE="t={{.Timestamp.Unix}},v0={{.Signatures | join \",\"}}"
You can also set signature_content_template and signature_header_template in YAML under destinations.webhook.
With one secret, the header looks like:
x-outpost-signature: t=1717249416,v0=<signature-from-current-secret>
During rotation, the same template preserves both signatures:
x-outpost-signature: t=1717249416,v0=<signature-from-current-secret>,<signature-from-previous-secret>
If you customize DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE, keep .Signatures in the output so receivers can verify requests during secret rotation.
Standard Webhooks Mode
Follows the Standard Webhooks specification:
base64(HMAC-SHA256(secret, "${webhook-id}.${timestamp}.${body}"))
Use the official Standard Webhooks SDK to verify signatures. Secrets use the whsec_<base64> format.
During secret rotation, Outpost includes one Standard Webhooks signature entry per valid secret in the webhook-signature header. The current secret's signature is first, followed by the previous secret's signature:
webhook-signature: v1,<signature-from-current-secret> v1,<signature-from-previous-secret>
Enable Standard Webhooks mode by setting DESTINATIONS_WEBHOOK_MODE=standard in the Config API or in Hookdeck Destinations settings.
Enable Standard Webhooks mode:
DESTINATIONS_WEBHOOK_MODE=standard
Secret Rotation
Rotate a webhook secret without downtime. During the rotation window, both the old and new secrets produce valid signatures.
curl --request PATCH \
'https://api.outpost.hookdeck.com/2025-07-01/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:
- The current secret becomes
previous_secret - A new secret is generated
- The previous secret remains valid until
previous_secret_invalid_at(default: 24 hours) - During the rotation window, the signature header contains signatures generated with both valid secrets
- After
previous_secret_invalid_at, the previous secret is no longer included and the signature header returns to a single signature
Signature header format depends on the webhook mode. With the default header prefix:
| Mode | Header | During rotation |
|---|---|---|
| Default | x-outpost-signature | v0=<signature-from-current-secret>,<signature-from-previous-secret> |
| Standard Webhooks | webhook-signature | v1,<signature-from-current-secret> v1,<signature-from-previous-secret> |
Receivers should treat rotation as an allow-list period: verify the request against the current secret and the previous secret, and accept the request if any signature in the header matches one of those secrets. Outpost signs with the current secret first.
Custom Headers
Tenants can add custom HTTP headers to webhook requests for authentication or routing:
curl 'https://api.outpost.hookdeck.com/2025-07-01/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.
Custom webhook headers in the tenant portal are disabled by default. Enable them in Hookdeck User Portal settings.
Custom headers are disabled in the tenant portal by default. Enable with:
PORTAL_ENABLE_WEBHOOK_CUSTOM_HEADERS=true
Operator Configuration
Configure webhook operator behavior using these keys in the Config API or in Hookdeck Destinations settings: 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.
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 in default mode |
DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE | v0={{.Signatures | join ","}} | Template for signature header value in default mode |