Destinations
What is a destination?
A destination is anywhere a captured webhook gets forwarded — your local handler, a staging server, a queue, another tool. Each endpoint fans out to as many destinations as your plan allows, independently.
Destinations are independent. If one returns 500, the others still receive the webhook. Splithook never short-circuits fan-out on a single failure.
Creating a destination
From an endpoint's page: Add destination → choose HTTP or Tunnel → configure → Save.
Plan limits:
| Plan | Max destinations per endpoint |
|---|---|
| Free | 1 |
| Pro | 5 |
| Team | 25 |
Destination types
HTTP destination
A plain HTTPS URL. Splithook POSTs the webhook body with the same headers the provider sent, plus optional signing headers.
https://staging.acme.dev/webhooks/stripe
HTTP destinations require HTTPS unless your workspace has allow_http_destinations enabled (only available in dev/self-hosted configurations).
Tunnel destination
Connects to a running splithook tunnel CLI process. The URL field takes a local address:
http://localhost:3000/webhooks/stripe
When you save a tunnel destination, Splithook issues a tunnel token (sht_…). You see it once — copy it before closing the dialog. The CLI uses this token to authenticate the Mercure subscription.
Filters
A filter is an ExpressionLanguage expression evaluated against every incoming webhook. Only webhooks where the expression is true are forwarded. Default: no filter (forward everything).
provider == 'stripe' and body.type matches '^charge\.'
Variables available in filter expressions:
| Variable | Type | Example |
|---|---|---|
provider |
string | 'stripe', 'github', 'unknown' |
event_type |
string | 'charge.failed', 'push' |
headers |
map | headers['x-github-event'] |
body |
map | body.data.object.amount |
method |
string | 'POST' |
The filter is evaluated asynchronously in the forwarding worker — it does not block the capture response.
See Filters & expressions for the full operator reference.
Signing modes
| Mode | What Splithook sends | When to use |
|---|---|---|
passthrough |
Original signature header, byte-for-byte | Live traffic, same secret as provider |
strip |
No signature header | Internal services, analytics workers |
re_sign |
Fresh signature with your dev secret | Replays, staging, local development |
To use re_sign, attach a Signing secret to the destination. Go to Settings → Signing secrets → New, then select it in the destination editor.
Upload the secret your staging handler verifies with — not the one Stripe/GitHub configured on their end. These are different keys.
Retry policy
On failure (5xx or connection error), Splithook retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 30s |
| 2nd retry | 5m |
| 3rd retry | 30m |
After 3 failed attempts the delivery moves to dead-letter. You can replay dead-lettered deliveries manually from the Replay Log, or in bulk from the destination settings.
Every attempt — including retries — is logged in the ReplayLog with status, latency, and the first 4 KB of the response body.
Circuit breaker
If a destination's failure rate exceeds 90% over a 5-minute rolling window, the circuit opens automatically and delivery is paused. The dashboard shows the destination as circuit open. Fix the underlying issue, then manually resume from the destination settings.
Idempotency
Every delivery attempt sends the same splithook-delivery-id header. Use it to deduplicate in your handler:
splithook-delivery-id: dlv_01j3k9...
splithook-attempt: 2
splithook-endpoint: ab3dkf7z
Toggling a destination
Pause/resume a destination from its settings without deleting it. Paused destinations accumulate no dead-letter entries — events are simply skipped.
Rotating the tunnel token
If a tunnel token is leaked: destination settings → Rotate token. The old token is invalidated immediately; any connected CLI process disconnects and must re-authenticate with the new token.
Deleting a destination
Deletes the destination and all its ReplayLog entries. Captured webhooks on the parent endpoint are unaffected.