Skip to content
Back to site

Destinations

A destination is a URL of yours that receives a source’s webhooks, together with how to authenticate to it. Every incoming event creates one forward for each enabled destination of its source. Each destination has its own retries, timeout and credentials, so a slow service does not hold up the others.

const destination = await client.incomingSources.createDestination(projectId, sourceId, {
url: 'https://billing.internal.example.com/webhooks/stripe',
enabled: true,
maxAttempts: 5,
timeoutSeconds: 30,
});
Field What it does
url Required. Where forwards are sent. Checked against private address ranges like an endpoint URL, see Endpoint security.
enabled Whether the destination receives forwards.
authType, authConfig How Railhook authenticates, see below. authConfig is a JSON string, up to 4096 characters, encrypted at rest.
customHeadersJson Extra HTTP headers, as a JSON object in a string.
maxAttempts Attempts before the forward is abandoned. Default 5.
retryDelays Comma-separated waits in seconds. Default 60,300,900,3600,21600.
retryableStatuses Which HTTP statuses are worth another attempt. Default 408,429,500-599. See Which statuses are worth retrying.
autoDisabledAt Read-only. Set when Railhook turned this destination off after a window of nothing but failures. See When Railhook turns an endpoint off; a destination is turned off on the same terms.
timeoutSeconds How long one attempt waits for a response. Default 30.
transformationId A saved transformation applied before forwarding.
payloadTransform A JSONPath expression, such as $.data, used when no transformationId is set.

Forwards are not signed. Railhook proves who it is to your service with the destination’s credentials:

authType authConfig Sent as
NONE none Nothing.
BEARER {"token": "…"} Authorization: Bearer <token>
BASIC {"username": "…", "password": "…"} Authorization: Basic <base64>
CUSTOM_HEADER {"headerName": "…", "headerValue": "…"} The named header

Credentials are masked in the request headers the dashboard shows.

The body is the webhook exactly as the provider sent it, byte for byte, with its original Content-Type and, when it had one, Content-Encoding, unless a transformation is configured. A transformed body is sent as UTF-8. Railhook adds these headers:

Header Value
X-Incoming-Event-Id The incoming event.
X-Incoming-Request-Id The request id the ingress returned to the provider.
X-Forward-Attempt The attempt number, starting at 1.
Idempotency-Key <incoming event id>-<destination id>, stable across attempts.

Of the provider’s own headers, only those that describe the event are passed through, transformed or not, because some providers name the event nowhere else: a GitHub push and a GitHub issue cannot be told apart by their bodies.

Provider Headers passed through
GitHub X-GitHub-Event, X-GitHub-Delivery, X-GitHub-Hook-ID, X-GitHub-Hook-Installation-Target-Type, X-GitHub-Hook-Installation-Target-ID
GitLab X-Gitlab-Event, X-Gitlab-Event-UUID, X-Gitlab-Instance
Shopify X-Shopify-Topic, X-Shopify-Webhook-Id, X-Shopify-Shop-Domain, X-Shopify-API-Version, X-Shopify-Event-Id, X-Shopify-Triggered-At

A header of the same name set by Railhook, the destination’s credentials or its custom headers takes precedence. Every other original header is not forwarded, and signatures and tokens never are (X-Hub-Signature-256, X-Shopify-Hmac-Sha256, X-Gitlab-Token, Stripe-Signature and the like). Verification happened at the ingress; your service should rely on the destination credentials instead.

Railhook uses the first of these that is set: the saved transformation transformationId, then the JSONPath expression payloadTransform, then the body unchanged. If a configured transformation cannot be applied, the attempt fails and is retried; the untransformed body is never forwarded in its place. See Transformations.

Forwards follow the incoming retry ladder: 5 attempts with waits of 1 min, 5 min, 15 min and 1 h, jittered between 50% and 150%. 408, 429, any 5xx, a connection error, a timeout and a destination host that does not resolve are retried; any other 4xx or a 3xx moves the forward to Failed Forwards at once, without the rest of the ladder, since it will not change until someone fixes the destination. Redirects are not followed. A forward still outstanding 24 hours after it started is abandoned whatever its attempt count. A retry from Failed Messages or a replay starts a new forward, so its 24 hours count from the retry or replay, not from when the webhook arrived.

Abandoned forwards appear under Failed Forwards. Retrying one creates a new forward to the same destination, starting again from attempt 1. See Retries and failed messages.