Webhooks — providers/webhooks
# GitHub, Stripe snapshot events, Paddle Billing, Slack Events API, and Polar ingress.
# ACK follows a RabbitMQ publisher confirm; consumer deduplication is required.
# Secrets use mounted files so rotation can refresh without restarting Hookway.
# Required variables: RABBITMQ_URL and {GITHUB,STRIPE,PADDLE,SLACK,POLAR}_SECRET_FILE.
# See docs/guides/provider-compatibility.md for contracts, recovery, and tested limits.
server:
addr: ":8080"
max_body_bytes: 1048576
max_inflight_requests: 256
publish_timeout: 2s
write_timeout: 5s
security:
forwarded_header_allowlist: [Content-Type, X-GitHub-Delivery, X-GitHub-Event, X-Slack-Retry-Num, X-Slack-Retry-Reason, webhook-id]
destinations:
events:
type: rabbitmq
rabbitmq:
url: "${RABBITMQ_URL}"
exchange: hookway.providers
exchange_type: direct
durable: true
declare_queues: true
publish_timeout: 2s
flows:
polar:
responses:
published: {status: 200, body: accepted}
nodes:
- {id: in, type: http_trigger, path: /webhooks/polar, methods: [POST]}
- {id: body, type: read_body, max_bytes: 1048576}
- id: auth
type: auth
scheme: hmac_sha256_polar
signature_header: webhook-signature
timestamp_header: webhook-timestamp
message_id_header: webhook-id
timestamp_unit: seconds
timestamp_tolerance: 5m
secret_ref: {provider: file, name: "${POLAR_SECRET_FILE}", refresh: 1m, overlap: 10m}
- {id: env, type: envelope, body_format: json}
- {id: out, type: publish, destination: events, target: polar-events, create: true}
edges: [in>body, body>auth, auth>env, env>out]
github:
responses:
published: {status: 200, body: accepted}
nodes:
- {id: in, type: http_trigger, path: /webhooks/github, methods: [POST]}
- {id: body, type: read_body, max_bytes: 1048576}
- id: auth
type: auth
scheme: hmac_sha256_hex
signature_header: X-Hub-Signature-256
secret_ref: {provider: file, name: "${GITHUB_SECRET_FILE}", refresh: 1m, overlap: 10m}
- {id: env, type: envelope, body_format: json}
- {id: out, type: publish, destination: events, target: github-events, create: true}
edges: [in>body, body>auth, auth>env, env>out]
stripe:
responses:
published: {status: 200, body: accepted}
nodes:
- {id: in, type: http_trigger, path: /webhooks/stripe, methods: [POST]}
- {id: body, type: read_body, max_bytes: 1048576}
- id: auth
type: auth
scheme: hmac_sha256_ts_dot
signature_header: Stripe-Signature
timestamp_tolerance: 5m
secret_ref: {provider: file, name: "${STRIPE_SECRET_FILE}", refresh: 1m, overlap: 10m}
- {id: env, type: envelope, body_format: json}
- {id: out, type: publish, destination: events, target: stripe-events, create: true}
edges: [in>body, body>auth, auth>env, env>out]
paddle:
responses:
# Paddle explicitly requires 200, not Hookway's default 202.
published: {status: 200, body: accepted}
nodes:
- {id: in, type: http_trigger, path: /webhooks/paddle, methods: [POST]}
- {id: body, type: read_body, max_bytes: 1048576}
- id: auth
type: auth
scheme: hmac_sha256_ts_colon
signature_header: Paddle-Signature
timestamp_tolerance: 5s
secret_ref: {provider: file, name: "${PADDLE_SECRET_FILE}", refresh: 1m, overlap: 10m}
- {id: env, type: envelope, body_format: json}
- {id: out, type: publish, destination: events, target: paddle-events, create: true}
edges: [in>body, body>auth, auth>env, env>out]
slack:
responses:
published: {status: 200, body: accepted}
nodes:
- {id: in, type: http_trigger, path: /webhooks/slack, methods: [POST]}
- {id: body, type: read_body, max_bytes: 1048576}
- id: auth
type: auth
scheme: hmac_sha256_slack_v0
signature_header: X-Slack-Signature
timestamp_header: X-Slack-Request-Timestamp
timestamp_unit: seconds
timestamp_tolerance: 5m
secret_ref: {provider: file, name: "${SLACK_SECRET_FILE}", refresh: 1m, overlap: 10m}
- {id: env, type: envelope, body_format: json}
- id: kind
type: switch
cases:
- {port: challenge, when: {source: body, path: type, operator: eq, value: url_verification}}
default: event
- id: challenge_value
type: code
timeout: 50ms
script: |
if (typeof input.body.challenge !== 'string' || input.body.challenge.length > 4096) throw new Error('invalid challenge');
return {body: input.body, vars: {challenge: input.body.challenge}};
- {id: challenge, type: respond, status: 200, body: "{challenge}", headers: {Content-Type: text/plain}}
- {id: out, type: publish, destination: events, target: slack-events, create: true}
edges: [in>body, body>auth, auth>env, env>kind, kind:challenge>challenge_value, challenge_value>challenge, kind:event>out]