Creating Subscriptions
Subscriptions are created with the wh sub create CLI command, the warmhub_subscription_create MCP tool, or the SDK client.subscription.create(...) method. Subscription management REST endpoints are not currently mounted.
Two pieces of a webhook subscription have their own pages: the Filter JSON that selects which writes fire it, and the credentials and signature verification for securing deliveries. This page covers creating webhook subscriptions; Advanced below covers cross-repo and self-chaining setups.
Webhook Subscriptions
Section titled “Webhook Subscriptions”A webhook subscription sends an HTTP POST to your URL when matching operations are written.
Required: filter, webhook URL. Most webhooks also bind to a target shape via --on <shape> (or shapeName in the SDK/MCP) — that scopes the subscription to things and assertions of that shape. The one exception is shape lifecycle subscriptions: they omit --on and rely on a {"kind":"shape"} filter to subscribe to shape adds, revises, and retracts. See Filter JSON for the full filter grammar.
Webhook URL Requirements
Section titled “Webhook URL Requirements”WarmHub validates webhook URLs at subscription create/update time and again at delivery time. URLs that don’t meet these requirements are rejected with the message “Webhook target is not reachable or not allowed”.
- HTTPS required. Production WarmHub deployments do not deliver to
http://URLs. - Port allowlist:
80,443, or8443. Other ports (for example25,22, or6379) are rejected. - Must resolve to a public IP address. WarmHub rejects loopback addresses like
127.0.0.1, private-network addresses (RFC 1918 ranges such as10.0.0.0/8and192.168.0.0/16), link-local addresses, cloud-provider metadata IPs, and similar reserved ranges. - Encoded IP bypasses are rejected. WarmHub blocks non-canonical numeric host forms even when the scheme is otherwise allowed — examples include
https://2130706433/(decimal-encoded127.0.0.1), hex or octal IP literals, IPv4-mapped IPv6 forms, and 6to4-mapped private IPs. - No credentials in the URL.
https://user:pass@host/is rejected. Use credential binding to attach auth headers instead. - Use a canonical hostname. WarmHub accepts normal DNS hostnames and canonical IP literals only.
The same rules apply to fallbackWebhookUrl — an optional secondary endpoint WarmHub calls after a terminal delivery failure on the primary webhook. Set it via the SDK fallbackWebhookUrl field (see client.subscription), the CLI --fallback-webhook-url flag, or the warmhub_subscription_create / warmhub_subscription_update MCP tools.
For local development, expose your receiver through a public HTTPS tunnel (for example ngrok or Cloudflare Tunnel) and point the subscription at the tunnel URL. WarmHub does not deliver to http:// URLs or to private/loopback IPs.
Redirect behavior
Section titled “Redirect behavior”WarmHub re-validates every redirect hop against the same webhook URL rules above. A URL that passes create-time validation can still fail at delivery time if it redirects to:
- a non-HTTPS target
- a non-allowlisted port
- a private, loopback, link-local, or otherwise reserved address
- a non-canonical numeric host form
WarmHub also enforces a redirect-follow limit. If the target loops or exceeds that cap, the attempt fails with WEBHOOK_REDIRECT_LIMIT.
If a redirect crosses origins, WarmHub does not forward your auth, signature, idempotency, run-tracing, or W3C trace-propagation headers to the new origin. Only body-describing headers needed to preserve the request payload are retained. In practice, that means webhook endpoints behind a vanity redirector or cross-origin bounce URL should terminate on the final receiving origin directly rather than relying on WarmHub to carry credentials across origins.
Two failure modes at validate time
Section titled “Two failure modes at validate time”If validation fails, the outcome falls into one of two buckets:
- Your URL is rejected. The request comes back with the message
Webhook target is not reachable or not allowed. Edit the URL to match the rules above before retrying — the same message is used for every reason (scheme, port, host form, credentials, reserved IP, …) on purpose, so the surface can’t be used to fingerprint internal infrastructure. - WarmHub couldn’t check it right now. The request comes back with the message
Webhook URL validation temporarily unavailable, please retry. This means a transient infrastructure issue (typically a DNS resolver hiccup) interrupted the check before validation could finish. Submit the same request again once the resolver issue clears.
Via CLI
Section titled “Via CLI”wh sub create signal-hook \ --on Signal \ --kind webhook \ --filter '{"shape":"Signal"}' \ --webhook-url https://example.com/hook
# Optional: allow same-trace reentry for a self-chaining webhookwh sub create signal-loop \ --on Signal \ --kind webhook \ --filter '{"shape":"Signal"}' \ --webhook-url https://example.com/hook \ --allow-trace-reentryVia MCP
Section titled “Via MCP”{ "name": "warmhub_subscription_create", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "signal-hook", "kind": "webhook", "shapeName": "Signal", "filterJson": { "shape": "Signal" }, "webhookUrl": "https://example.com/hook" }}Webhook Payload
Section titled “Webhook Payload”The POST body includes:
| Field | Description |
|---|---|
event | "warmhub.write" or "warmhub.retract" |
traceId | Unique trace identifier for the event chain |
runId | Action run identifier |
subscriptionId | Subscription identifier |
callback_url | Callback endpoint to report asynchronous progress or terminal outcome for this run |
repo | { "orgName", "repoName" } — the subscription’s home repo |
matchedOperationIndexes | Indexes of operations that matched the filter |
matchedOperations | The matched operations with their details |
Headers include X-WarmHub-Idempotency-Key, X-WarmHub-Run-Id, and X-WarmHub-Attempt for deduplication and observability.
Deliveries also include World Wide Web Consortium (W3C) trace propagation headers such as traceparent and tracestate when they run inside an active backend trace. When the subscription binds a WEBHOOK_SIGNING_SECRET, deliveries are also signed — see Verifying Signatures.
Use callback_url when your handler accepts the request and finishes work asynchronously. Post processing, success, failure, or retry_requested back to that URL with normal repo:write authentication.
Advanced
Section titled “Advanced”Trace Reentry
Section titled “Trace Reentry”Write-triggered webhook subscriptions support an optional allowTraceReentry setting.
- Default:
false - CLI flag:
--allow-trace-reentry - Meaning when
false: a subscription runs at most once per trace per shape - Meaning when
true: the subscription may run again within the same trace - Hard fuse: a global chain-depth safety limit still applies even when reentry is allowed
You can set allowTraceReentry at create time through any surface: the CLI --allow-trace-reentry flag, the SDK client.subscription.create(...) method, or the MCP warmhub_subscription_create tool. It can also be patched later through MCP with warmhub_subscription_update.
Cross-Repo Subscriptions
Section titled “Cross-Repo Subscriptions”A webhook subscription can watch a source repo that is different from the repo where the subscription lives. When a matching write lands in the source repo, the subscription fires and delivers to the webhook URL configured in its home repo.
Constraints
Section titled “Constraints”- The source repo must be in the same org as the subscription’s home repo. Cross-org source repos are rejected at creation time.
- The subscription creator must have read access to the source repo. Creation fails if this check does not pass.
allowTraceReentryapplies normally to cross-repo subscriptions.
Via CLI
Section titled “Via CLI”# Webhook subscription in myrepo that fires on writes to other-repowh sub create cross-hook \ --on Signal \ --kind webhook \ --source myorg/other-repo \ --filter '{"shape":"Signal"}' \ --webhook-url https://example.com/hookVia SDK
Section titled “Via SDK”Cross-repo subscriptions are also supported through the SDK by setting sourceRepoRef on client.subscription.create(...).
await client.subscription.create({ orgName: 'myorg', repoName: 'myrepo', name: 'cross-hook', kind: 'webhook', shapeName: 'Signal', filterJson: { shape: 'Signal' }, sourceRepoRef: 'myorg/other-repo', webhookUrl: 'https://example.com/hook',})Via MCP
Section titled “Via MCP”Cross-repo subscriptions are also supported through the warmhub_subscription_create MCP tool by setting sourceRepoRef.
{ "name": "warmhub_subscription_create", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "cross-hook", "kind": "webhook", "shapeName": "Signal", "filterJson": { "shape": "Signal" }, "sourceRepoRef": "myorg/other-repo", "webhookUrl": "https://example.com/hook" }}There is no public HTTP subscription creation endpoint for cross-repo or same-repo subscriptions.
Payload Differences
Section titled “Payload Differences”Cross-repo deliveries use the same top-level webhook payload shape as same-repo deliveries. Two details matter:
| Field | Description |
|---|---|
repo | The subscription’s home repo — where the subscription lives |
event | Event discriminator: "warmhub.write" or "warmhub.retract". Use this field to branch webhook handler logic by event type. |
originRepoId | Identifies the repo where the event originated (read-only). Cross-repo deliveries do not currently add a separate expanded sourceRepo object. |
When the subscription is not cross-repo, the payload shape is still the same.
Hit a problem or have a question? Get in touch.