Skip to content

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.

A webhook subscription sends an HTTP POST to your URL when matching operations or metadata events occur.

WarmHub supports five event types for webhook subscriptions:

Event typeScopeDescription
commit (default)RepoFires when writes matching a filter land in the repo. Requires filterJson and webhook URL; most webhooks also bind to a target shape via --on <shape> (or shapeName in the SDK/MCP).
repo.renamedRepoFires when the repo itself is renamed. Rejects shapeName, filterJson, and sourceRepoRef.
thing.renamedRepoFires when a thing in the repo is renamed. Rejects shapeName, filterJson, and sourceRepoRef.
shape.renamedRepoFires when a shape in the repo is renamed. Rejects shapeName, filterJson, and sourceRepoRef.
org.renamedOrgFires when the org is renamed. Rejects shapeName, filterJson, sourceRepoRef, and repoName.

For commit subscriptions, most webhooks 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.

Metadata subscriptions (the *.renamed events) use an eventType instead of a filter. Org-scoped metadata subscriptions use orgName without repoName and can be created only by organization owners and admins.

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, or 8443. Other ports (for example 25, 22, or 6379) 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 as 10.0.0.0/8 and 192.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-encoded 127.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.

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.

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.
Terminal window
# Default commit subscription
wh sub create signal-hook \
--on Signal \
--kind webhook \
--filter '{"shape":"Signal"}' \
--webhook-url https://example.com/hook
# Repo rename subscription
wh sub create repo-rename-hook \
--kind webhook \
--event repo.renamed \
--webhook-url https://example.com/hook
# Org rename subscription
wh sub create org-rename-hook \
--org myorg \
--kind webhook \
--event org.renamed \
--webhook-url https://example.com/hook
# Optional: allow same-trace reentry for a self-chaining webhook
wh sub create signal-loop \
--on Signal \
--kind webhook \
--filter '{"shape":"Signal"}' \
--webhook-url https://example.com/hook \
--allow-trace-reentry

The MCP surface splits subscription creation by scope. Repo-scoped event types (commit, repo.renamed, thing.renamed, shape.renamed) use warmhub_subscription_create. Org-scoped event types (org.renamed) use the separate warmhub_org_subscription_create tool.

{
"name": "warmhub_subscription_create",
"arguments": {
"orgName": "myorg",
"repoName": "myrepo",
"name": "signal-hook",
"kind": "webhook",
"shapeName": "Signal",
"filterJson": { "shape": "Signal" },
"webhookUrl": "https://example.com/hook"
}
}

For repo-scoped metadata event types (repo.renamed, thing.renamed, shape.renamed), omit shapeName, filterJson, and sourceRepoRef from the warmhub_subscription_create arguments.

For org.renamed, use warmhub_org_subscription_create instead and omit repoName, shapeName, filterJson, and sourceRepoRef.

The POST body shape depends on the subscription’s event type. Use the event field to branch handler logic.

Note that the event field in the delivered payload is not the same as the create-time event type selector. For example, a subscription created with event type commit delivers payloads whose event field is "warmhub.write" or "warmhub.retract" — not "commit".

FieldDescription
event"warmhub.write" or "warmhub.retract"
traceIdUnique trace identifier for the event chain
runIdAction run identifier
subscriptionNameName of the subscription that fired
callback_urlCallback endpoint to report asynchronous progress or terminal outcome for this run
repo{ "orgName", "repoName" } — the subscription’s home repo
matchedOperationIndexesIndexes of operations that matched the filter
matchedOperationsThe matched operations with their details. Thing-bearing operations include a durableId field — the stable, rename-safe identifier for the thing.

repo.renamed and other repo-scoped metadata subscriptions

Section titled “repo.renamed and other repo-scoped metadata subscriptions”

Repo-scoped metadata deliveries (such as repo.renamed, thing.renamed, and shape.renamed) use a different payload shape. The event field is "warmhub.<eventType>" (for example "warmhub.repo.renamed"). The repo block is present and identifies the repo. The rename payload is delivered under a data field. matchedOperationIndexes and matchedOperations are not present.

FieldDescription
event"warmhub.<eventType>" — for example "warmhub.repo.renamed"
traceIdUnique trace identifier for the event chain
runIdAction run identifier
subscriptionNameName of the subscription that fired
callback_urlCallback endpoint to report asynchronous progress or terminal outcome for this run
repo{ "orgName", "repoName" } — the subscription’s home repo
dataEvent-specific payload (for example, rename details)

Org-scoped metadata deliveries use a similar shape, but repo is null and an org block is present instead.

FieldDescription
event"warmhub.org.renamed"
traceIdUnique trace identifier for the event chain
runIdAction run identifier
subscriptionNameName of the subscription that fired
callback_urlCallback endpoint to report asynchronous progress or terminal outcome for this run
reponull
org{ "orgName" } — the org the subscription belongs to
dataEvent-specific payload (for example, rename 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 using a token with the repo:action-callback scope (or repo:write). For org-scoped runs (such as org.renamed), use a token with the org:action-callback scope instead.

Once the subscription is live, you can inspect what each write delivered. How you do that depends on the subscription’s scope:

  • Repo-scoped subscriptions — use wh sub log with the --repo flag to target the correct repo.
  • Org-scoped subscriptions (such as org.renamed) — wh sub log does not support org-scoped subscriptions. Use the Debugging a Failing Subscription runbook instead.

The Action Delivery Lifecycle page explains the run statuses, retry schedule, and error codes you will see along the way.

Organization owners and admins can subscribe to these filter-free events:

EventWhen it firesdata payload
org.member_addedA member or pending invite is added{ role, status }
org.repo_createdA private or public repository is created{ repoName, displayName, visibility }
org.repo_publishedAn existing private repository becomes public{ repoName, displayName, visibility: "public" }
org.renamedThe organization slug changes{ oldName, newName }

Public repository creation emits org.repo_created only. It does not also emit org.repo_published; publication is reserved for a later real transition from private to public.

Terminal window
wh sub create member-audit --org myorg --event org.member_added \
--webhook-url https://example.com/org-activity
wh sub create repo-feed --org myorg --event org.repo_created \
--webhook-url https://example.com/org-activity
await client.subscription.create({
orgName: 'myorg',
name: 'publication-feed',
kind: 'webhook',
eventType: 'org.repo_published',
webhookUrl: 'https://example.com/org-activity',
})
{
"name": "warmhub_org_subscription_create",
"arguments": {
"orgName": "myorg",
"name": "repo-feed",
"eventType": "org.repo_created",
"kind": "webhook",
"webhookUrl": "https://example.com/org-activity"
}
}

Org event bodies use repo: null, org: { "orgName": "myorg" }, and the payload shown above. They never include internal organization, repository, or user IDs. Member events also omit email, member names, inviter, and actor.

Org activity events differ in how WarmHub checks access before each delivery. For full details on org access rules, see [Organization Access](/[fill in: org access rules page]).

  • org.member_added, org.repo_created, and org.repo_published — before every delivery attempt, WarmHub rechecks that the subscription creator is still an owner or admin. If the creator has been downgraded, those deliveries are no longer sent.
  • org.renamed — no per-delivery ownership recheck. These subscriptions remain deliverable with standard org:read access.

These webhooks are organization-owned automation. Following repository activity through your personal web app notification or email preferences is a separate notification feature, not an anonymous or outsider webhook subscription.

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.

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.

  • 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.
  • allowTraceReentry applies normally to cross-repo subscriptions.
Terminal window
# Webhook subscription in myrepo that fires on writes to other-repo
wh sub create cross-hook \
--on Signal \
--kind webhook \
--source myorg/other-repo \
--filter '{"shape":"Signal"}' \
--webhook-url https://example.com/hook

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',
})

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",
"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.

Cross-repo deliveries use the same top-level webhook payload shape as same-repo deliveries. Two details matter:

FieldDescription
repoThe subscription’s home repo — where the subscription lives
eventEvent discriminator: "warmhub.write" or "warmhub.retract". Use this field to branch webhook handler logic by event type.
originRepoNameIdentifies the repo where the event originated (read-only).

When the subscription is not cross-repo, the payload shape is still the same.