Skip to main content

WEBHOOKS

The --webhook flag on swamp serve registers an HTTP endpoint that triggers a workflow when an incoming request arrives. The flag is repeatable — pass it multiple times to register several endpoints.

Spec format

On the CLI, webhooks use a colon-delimited string:

--webhook '<route>:<workflow>:<secret>[:<scheme>[:<header>[:<prefix>]]]'

In a config file, each webhook is a YAML object with named fields instead of the colon-delimited format.

Field Required Description
route Yes URL path for the endpoint (e.g., /hooks/github)
workflow Yes Workflow to trigger on valid requests
secret Yes Signing secret or secret indirection reference
scheme No Provider scheme (default: github). See Provider schemes
header No Custom header name (generic scheme only)
prefix No Expected value prefix (generic scheme only)

Provider schemes

Each scheme defines how the incoming request signature is verified.

Scheme Algorithm Signature header Notes
github HMAC-SHA256 X-Hub-Signature-256 Default when scheme is omitted
linear HMAC-SHA256 Linear-Signature
stripe HMAC-SHA256 Stripe-Signature Stripe signing protocol (timestamp + payload)
slack HMAC-SHA256 X-Slack-Signature
jira HMAC-SHA256 X-Hub-Signature sha256= prefix (same HMAC as GitHub)
generic HMAC-SHA256 Custom (via <header>) Requires a header name; optional prefix

Secret sources

The <secret> field supports indirection so signing secrets do not appear as literal values in process arguments.

Prefix Source Example
@env=VAR_NAME Environment variable VAR_NAME @env=WEBHOOK_SECRET
@file=/path File contents (trailing newline trimmed) @file=/run/secrets/webhook
@vault=<name>:<key> Named vault key @vault=prod-secrets:webhook-key
(plain string) Literal value (backward compatible) mysecret

The server reads the resolved secret at startup. If the environment variable is unset or the file cannot be read, the server refuses to start.


CEL context

When a workflow is triggered by a webhook, the webhook.* namespace is available in CEL expressions within that workflow.

Field Type Description
webhook.body dyn Parsed JSON body (object or array), or raw string for non-JSON payloads
webhook.headers map<string, string> Request headers
webhook.route string Matched route path
webhook.body.action == "opened"
webhook.headers["X-GitHub-Event"] == "issues"

Examples

GitHub (default scheme)

swamp serve --webhook '/hooks/github:on-push:@env=GITHUB_WEBHOOK_SECRET'

Linear

swamp serve --webhook '/hooks/linear:on-issue:@env=LINEAR_SECRET:linear'

Stripe

swamp serve --webhook '/hooks/stripe:on-payment:@env=STRIPE_WEBHOOK_SECRET:stripe'

Slack

swamp serve --webhook '/hooks/slack:on-command:@env=SLACK_SIGNING_SECRET:slack'

Jira

swamp serve --webhook '/hooks/jira:on-issue:@env=JIRA_WEBHOOK_SECRET:jira'

Generic

swamp serve --webhook '/hooks/custom:on-event:@env=CUSTOM_SECRET:generic:X-Custom-Token:Bearer'

The generic scheme computes an HMAC-SHA256 digest of the request body using the resolved secret as the key, then compares it against the value of the X-Custom-Token header (after stripping the Bearer prefix).

Extension webhook schemes

Webhook schemes of the form @collective/name are resolved from installed extensions. The extension must export a webhook handler (see Webhook kind). Trusted collectives are auto-pulled on first reference.

serve.yaml format

webhooks:
  - route: /hooks/telegram
    workflow: on-message
    secret: "@env=TELEGRAM_SECRET"
    scheme: "@swamp/telegram"
    config:
      botName: my-bot

The config object is passed to the extension handler's createHandler(config). When configSchema is declared in the handler, the config object is validated at startup.

CLI format

swamp serve --webhook '/hooks/telegram:on-message:@env=TELEGRAM_SECRET:@swamp/telegram'

The CLI format does not support the config object. Use a config file when the handler requires configuration.

Handler lifecycle

  1. The server resolves the extension scheme and calls createHandler(config).
  2. On each incoming request, verify(body, headers, secret) runs first. A false return or a thrown error responds 401. Verification is fail-closed: any error rejects the request.
  3. Hooks fire only after successful verification — a failed verify never triggers hooks.
  4. If transform(body, redactedHeaders) is defined, the transformed payload replaces the raw body for downstream processing.
  5. If respond(payload) is defined, its return value ({ status, headers?, body?, enqueue }) controls the HTTP response and whether the workflow run is enqueued. Without respond, the default 200 response and enqueue behaviour applies.

There is no handler timeout — the server does not impose a deadline on verify, transform, or respond. Response headers returned by respond are passed through unfiltered.

@swamp/telegram

First-party extension for receiving Telegram bot updates.

Telegram uses a static secret token sent in the X-Telegram-Bot-Api-Secret-Token header. The handler compares it to the configured secret using constant-time comparison. This is weaker than HMAC — the secret is transmitted in the clear (not derived from the body), so TLS is required. Do not use over plain HTTP.

webhooks:
  - route: /hooks/telegram
    workflow: on-telegram
    secret: "@env=TELEGRAM_BOT_SECRET"
    scheme: "@swamp/telegram"

Set the webhook on the Telegram side with the matching secret_token:

curl -X POST "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \
  -d "url=https://swamp.example.com/hooks/telegram" \
  -d "secret_token=$TELEGRAM_BOT_SECRET"