Skip to content

Local webhooks

Receive webhooks on a backend running on your own computer and test your signature check. Time: 15 minutes. You need: a webhook handler on localhost, a tunnel tool (cloudflared or ngrok), and an app in the console.

Why a tunnel

The platform delivers webhooks only to public https:// addresses:

  • the URL is https://, ≤ 2048 characters;
  • the dialled IP is checked after DNS — private networks (10.x, 192.168.x …), loopback (127.0.0.1, localhost) and link-local addresses are refused (the delivery records refused: not a public address);
  • redirects (3xx) are not followed — a 3xx is a failed delivery.

So http://localhost:3000 cannot be the target; use a tunnel with a public https address.

Steps

  1. Run your handler locally (for example POST http://localhost:3000/webhooks/gerege), verifying signatures over the raw body (Verifying signatures).
  2. Open a tunnel:

    # Cloudflare (no account, a temporary address)
    cloudflared tunnel --url http://localhost:3000
    # → https://<random>.trycloudflare.com
    
    # or ngrok
    ngrok http 3000
    # → https://<random>.ngrok-free.app
    
  3. In the console: your app → "Webhook endpoints" → a new endpoint in the test environment: URL https://<random>.trycloudflare.com/webhooks/gerege, and an event filter if you like. The answer shows the whsec_… secret once — store it in your handler's environment. API: POST /api/v1/miniapp/dev/apps/{id}/webhook-endpoints {"environment": "test", "url": "…", "description": "laptop tunnel", "event_types": []} → 201 {"endpoint", "webhook_secret", "notice"} (empty event_types = all). A test endpoint never receives live events — your live endpoints are untouched.

  4. Send a test: POST /api/v1/miniapp/dev/apps/{id}/webhook-endpoints/{eid}/test sends a signed webhook.test to that endpoint while you wait and answers {event_id, endpoint_id, ok, status_code, error, duration_ms, delivery}. ok: true means the path real events take works (same signer, same secret). 20 a minute.
  5. Make real events — the simulation, or create and cancel an intent with your test key.
  6. The console's "Deliveries" (GET /api/v1/miniapp/dev/apps/{id}/webhook/deliveries?endpoint_id=…) — each event's status (pending, delivered, failed, skipped), attempts, last HTTP code, error, body. Fix your handler and "Send again" (POST …/webhook/deliveries/{event_id}/retry?endpoint_id=…) — the event comes again with its original body and a signature over a new t.
  7. When you are done, delete the test endpoint or PATCH {"status": "disabled"} — an endpoint whose tunnel address died fails for five days, is then auto_disabled, and your organisation's admins are notified.

Things to know

  • Rotating the secret: POST …/webhook-endpoints/{eid}/rotate-secret — a new whsec_…, and the old one keeps signing alongside for 24 hours (two v1 per event). If the tunnel's address changes, PATCH {"url": "…"} — the secret stays.
  • The app's original webhook (endpoint №1, POST …/webhook {"url"}) is one URL for both test and live. Do not point a live app's №1 at a local tunnel — add a separate endpoint for test. The old POST …/webhook call still issues a new secret every time.
  • Endpoint №1's URL and secret cannot change while the app is in review or suspended (409 locked_for_review).
  • An event that falls due while its endpoint is off (disabled, auto_disabled) is skipped — fetch what you missed from the Events API.
  • Public "request bin" sites (webhook.site and the like) show every event body to a third party — never use them for live data; use your own tunnel.
  • Answer 2xx within 10 seconds; queue slow work. Stopping on a debugger breakpoint times the delivery out and puts it on the retry schedule (Delivery and retries).