Webhook relay
The webhook relay is how an external system (Stripe, GitHub, a PMS like Hostaway, anything that
sends webhooks) reaches a Cronable webhook trigger — without your server opening a
single inbound port.
Your daemon binds to 127.0.0.1 and never listens on a public port. Instead it holds
an outbound connection to the relay at hooks.cronable.ai. The relay owns the public URL and the
TLS; when a webhook arrives it pushes the event down your existing connection and your job runs.
Because the connection is outbound, it works behind NAT, a firewall, or a home router with nothing
forwarded.
Stripe / GitHub / your PMS
│ POST https://hooks.cronable.ai/<hookId> (+ per-hook token)
▼
hooks.cronable.ai ──pushes the event down──▶ your daemon (127.0.0.1) ──▶ the job runs
▲ │
└──────────── outbound WebSocket, held open ─────┘Turning it on
- In Settings, set the relay URL and enable it (
relayEnabled+relayUrl). The daemon opens the connection immediately — no restart. - Create a
webhookjob (it has ahookIdbut no schedule). - Open the job in the Inspector → Webhook section and generate a token. The plaintext is
shown once — copy it then. The public URL is
https://hooks.cronable.ai/<hookId>. - Give the caller the URL and the token. It presents the token as an
X-Webhook-Tokenheader (preferred) or a?token=…query parameter on each call.
The relay is opt-in and license-gated: it only runs if you enable it and your daemon presents a valid license. Turn it off and no relay connection is ever made.
Can someone else trigger — or read — my webhooks?
No. Isolation between tenants is the relay’s first-class requirement, enforced in layers:
- Your license gates the connection. Only a daemon presenting your valid license gets a relay connection at all. An unlicensed or someone else’s daemon is rejected at connect.
- Two secrets, not one. The public URL carries an unguessable
hookId(at least 32 random URL-safe characters on the relay) and a per-hook token. The relay verifies the token (constant-time, against a stored hash) before it routes anything — a guessed URL without the matching token goes nowhere. - No “does this hook exist?” oracle. An unknown hook, a wrong token, a disabled hook, and a
malformed URL all return the same generic
401. A scanner learns nothing about which hooks exist, and repeated failures from an IP earn a temporary ban, so brute force is cut off fast. - Your hooks are bound to your license — in both directions. A hook is claimed by exactly one
license, and event delivery is scoped to that verified license. So even another fully licensed
Cronable customer who somehow learned your
hookIdcould neither take it over nor receive its events — they only ever see their own. - Nothing sensitive is logged. Your license, the token, and the request body are kept out of logs on both the relay and your daemon.
hookIds live only in your private jobs repo and your daemon — they are not published anywhere — so
in practice an attacker has nothing to guess from.
What actually crosses the relay
- Only the inbound event, in flight. The relay forwards the request (method, headers, body, query) to your daemon and does not store it. Nothing else — your jobs, secrets, credentials, AI accounts, logs, and run output never leave your machine.
- Original signatures are preserved. Provider signature headers (
stripe-signature,x-hub-signature-256, …) pass through unchanged, so your job can verify them itself for end-to-end authenticity on top of the relay’s own token check. - A relayed call is identical to a direct one. Your job sees the same
{{ $parent.json.* }}/{{ $parent.text }}/{{ $parent.method }}/{{ $parent.query.* }}whether the event arrived through the relay or a direct local POST.
Availability
If the relay is briefly unreachable or you’re mid-deploy, your daemon reconnects automatically (with backoff so a fleet never reconnects in lockstep), and an event that arrived while you were disconnected is held and replayed when you come back — a trigger isn’t silently dropped. The service is built to scale horizontally as the number of connected daemons grows.
Prefer not to use it?
The relay is optional. You can instead expose POST /engine/api/hooks/<hookId> yourself behind your
own authenticated reverse proxy and TLS — see the self-hosted inbound note in
Triggers — or avoid inbound entirely with the polling triggers
(inbox, folder-watch, calendar-watch, telegram-watch, sheet-watch, hostaway-watch), which reach out from
your server and never touch the relay.