Skip to content
Login

Webhooks

HLE webhook tunnels let external services (GitHub, Stripe, GitLab, etc.) deliver HTTP callbacks to your home lab without opening ports or configuring dynamic DNS.

How it works

Webhook tunnels are a specialized mode of HLE tunnels designed for incoming HTTP callbacks:

  • No authentication gate — external services can reach your endpoint directly (no SSO prompt)
  • HTTP-only — WebSocket is disabled (webhooks are simple POST/GET requests)
  • Path-restricted — only requests matching your webhook path prefix are forwarded
  • Randomized URL — the subdomain includes a cryptographic token to prevent enumeration

When you create a webhook tunnel, HLE generates a URL like:

https://wh-a3f7b2c9e1d0f485-x7k.hle.world/hook/github

The wh- prefix and 16-character hex token make the URL unguessable. Your 3-character user code (x7k) is appended so the URL updates automatically if you purchase a custom tunnel code.

Quick start

Terminal window
# Forward GitHub webhooks to a local service
hle webhook --path /hook/github --forward-to http://localhost:3000 --label github-hook
# Forward Stripe webhooks
hle webhook --path /stripe --forward-to http://localhost:4242 --label stripe-hook

The CLI prints the public URL you can paste into your webhook provider’s settings.

Understanding URL and path routing

The --path and --forward-to flags work together to control how requests are routed:

  1. --path sets the path prefix on the public tunnel URL that external services must include
  2. The full incoming path (including the prefix) is forwarded to your local service
  3. --forward-to is the base URL of your local service

Example flow

Terminal window
hle webhook --path /hook --forward-to http://localhost:3000 --label gh
# Tunnel URL: https://wh-a3f7b2c9-x7k.hle.world
External service sends toYour local service receives
https://wh-…x7k.hle.world/hookGET http://localhost:3000/hook
https://wh-…x7k.hle.world/hook/githubPOST http://localhost:3000/hook/github
https://wh-…x7k.hle.world/other404 Not Found (doesn’t match /hook prefix)

Security

Randomized subdomains

Unlike regular tunnels (e.g. myapp-x7k.hle.world), webhook tunnels use a randomized subdomain with 16 hex characters (2^64 possibilities). This prevents attackers from guessing your webhook URL.

Server-side path enforcement

The HLE relay server enforces that incoming requests match the registered webhook path prefix. Even if someone discovers your webhook URL, they can only reach the specific path you configured — not your entire local service.

Access control and the delivery log

A webhook receiver is a tunnel with a path on it, so it has everything a tunnel has. Open it with Manage from the Webhooks list to reach its Access Control tab.

Two of the access rules apply to a sender:

  • Password (HTTP Basic Auth) — for a provider that can send credentials
  • Address — an allow-list, when the provider publishes fixed egress IPs

Sign-in rules and the PIN do not: there is nobody at a browser to sign in, and the last line cannot be switched between public and gated for the same reason.

Every delivery is recorded on the same access log as any other tunnel, per rule and in full, so you can see what reached the endpoint and what was turned away without reading server logs.

Signature verification

Give a receiver the same secret you gave the provider, and HLE checks every delivery at the relay. What fails never crosses the tunnel, never wakes your home service, and never costs you a delivery.

Set it on the receiver: pick the provider, paste the secret. The secret is write-only — the dashboard will tell you one is configured, and will never show it back.

ProviderWhat is checkedProvider’s docs
GitHubX-Hub-Signature-256 — HMAC-SHA256 of the raw bodyValidating deliveries
StripeStripe-Signature — HMAC of timestamp.body, within 5 minutesCheck signatures
GitLabX-Gitlab-Token — the secret itself, compared in constant timeWebhook secrets
SlackX-Slack-Signature — HMAC of v0:timestamp:body, within 5 minutesVerifying requests
HMAC-SHA256X-Signature — plain HMAC of the body, for anything else

A delivery that does not verify gets HTTP 401, is recorded as rejected on the receiver’s chart, and triggers one email per receiver per day so a rotated secret announces itself rather than looking like the provider went quiet.

Verification runs before the rate limit and before billing, so forged traffic cannot spend your allowance or your credits.

Receivers with no provider set are unchanged: all headers, including the signature ones, are forwarded and your application decides.

Keeping deliveries, and sending them again

Turn on Keep deliveries for a receiver and each one that arrives is stored: method, path, headers and body. Open one to read exactly what a provider sent, and press Replay to send that same request to your service again — after you have fixed the bug it exposed, or while you are still writing the handler.

  • Bodies are kept up to 64 KB; longer ones are stored as a prefix and marked as trimmed.
  • Authorization, Cookie and friends are never stored. Signature headers are, because they are digests rather than credentials — and because a replay should still verify.
  • Captures are dropped after 7 days.
  • A replayed delivery carries X-HLE-Replay: 1 and X-HLE-Attempt: n, and is recorded as its own entry rather than editing the original.

It is off by default. Storing your payloads is a decision worth making deliberately rather than one you discover later.

Retrying what your service could not take

With Retry failures on, a delivery your home service refused — or that arrived while your agent was offline — is held and tried again: 1m, 2m, 4m, 8m, 16m, up to five attempts by default, then marked as given up and left in the list where you can replay it by hand.

This is the part a hosted receiver cannot do for you. GitHub does not retry at all; if your machine was restarting at the wrong moment, the event is simply gone. Because the relay is already in front of your network, it can hold on to it and hand it over when your service comes back.

Retry requires Keep deliveries — there is nothing to resend without a stored body — so turning capture off turns retry off too.

Rate limits

Two limits apply, and they answer different questions.

The free allowance is 10 requests per minute, on every plan. It is counted per account, across all of your webhook receivers. What happens on the eleventh request in a minute is what differs:

PlanFree allowanceBeyond itWebhook receivers
Free10 req/minBlocked (HTTP 429) + one email alert per dayUnlimited
PAYG10 req/min€0.50 per 1,000 requests, from your credit balanceUnlimited

The per-receiver ceiling is a separate abuse guard, applied per tunnel rather than per account: 10 req/min on Free, 1,000 req/min on PAYG. It exists so a single misconfigured sender cannot spend a credit balance in a loop. Reaching it returns 429 regardless of credits.

Rate limit headers are included in every webhook response:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 942

When the per-minute limit is exceeded, the server returns HTTP 429 with a Retry-After: 60 header.

Billing beyond the free allowance

The allowance is per minute, not per day — there is no daily quota, only a daily counter shown in the dashboard. Past the allowance:

  • Free tier: Requests are blocked (HTTP 429) and you receive one email alert per day. Add credits to your account to unlock PAYG and continue.
  • PAYG tier: Requests are charged from your credit balance. If credits are depleted, requests are blocked and you receive an email alert.

All webhook rate limits and pricing are configurable by the admin from the dashboard.

Payload limits

  • Maximum body size: 10 MB per request
  • Requests exceeding this limit receive HTTP 413

CLI reference

hle webhook

Create a webhook tunnel.

FlagTypeDefaultDescription
--pathstringrequiredWebhook path prefix (e.g. /hook/github). Cannot be /.
--forward-tostringrequiredLocal URL to forward webhooks to
--labelstringrequiredWebhook label, e.g. github-hook
--api-keystringAPI key (also checked in HLE_API_KEY and config file)

Examples

GitHub → self-hosted Gitea

Forward push events from GitHub to a Gitea mirror:

Terminal window
hle webhook --path /hook/github --forward-to http://localhost:3000 --label gh

In GitHub repository settings → Webhooks → Add webhook:

  • Payload URL: paste the full HLE URL from the CLI output (e.g. https://wh-…x7k.hle.world/hook/github)
  • Content type: application/json
  • Secret: set a secret and configure the same secret in your Gitea instance

Stripe → local dev server

Test Stripe webhooks against your development environment:

Terminal window
hle webhook --path /stripe --forward-to http://localhost:4242 --label stripe-hook

In the Stripe Dashboard → Developers → Webhooks → Add endpoint:

  • Endpoint URL: paste the full HLE URL (e.g. https://wh-…x7k.hle.world/stripe)
  • Events: select the events you need (e.g. checkout.session.completed)

n8n workflow triggers

Receive webhook triggers for n8n automations:

Terminal window
hle webhook --path /n8n --forward-to http://localhost:5678 --label n8n

In n8n, create a Webhook node and set its path to /n8n to match the HLE tunnel path.

Renovate bot

Self-hosted Renovate with GitHub webhook delivery:

Terminal window
hle webhook --path /renovate --forward-to http://localhost:8080 --label renovate

Configure your Renovate instance to accept webhooks on the /renovate path, and set the GitHub webhook URL to the full HLE URL.

Troubleshooting

404 Not Found

The request path doesn’t match the configured webhook path prefix. If you registered with --path /hook/github, only requests to /hook/github and /hook/github/* are forwarded.

Common cause: The external service sends to the tunnel URL without the path prefix. Make sure the full URL (including the path) is pasted into the webhook provider settings.

Path appears duplicated (e.g. /webhook/webhook)

This usually means the external service is appending its own webhook path to the URL you provided. For example, if you gave a service https://wh-…x7k.hle.world/webhook as the base URL, and the service also appends /webhook, the final request goes to /webhook/webhook.

Fix: Use a different --path (e.g. --path /hook) so it doesn’t collide with the external service’s own path. Or configure the external service with the tunnel base URL only (without the path) if it adds its own path automatically.

429 Rate Limit Exceeded

You’ve exceeded your plan’s webhook rate limit. Wait 60 seconds for the per-minute limit to reset, or upgrade your plan for higher limits.

If you’re hitting the daily limit, check your email for an alert with details. Add credits or upgrade to PAYG for higher allowances.

413 Payload Too Large

The webhook payload exceeds 10 MB. This is uncommon for webhooks — check if the sender is including large attachments.

Webhook secret not working

HLE forwards all HTTP headers (including X-Hub-Signature-256, Stripe-Signature, etc.). The webhook secret itself is never transmitted over the wire — it’s used locally by your application to verify signatures. Make sure the same secret is configured in both the webhook provider and your local application.