# examples/destinations/webhook.yaml## Reference config for a Webhook destination. Hookway delivers each inbound# webhook event to a configured HTTPS endpoint.## DELIVERY MODE (delivery_mode: async | sync; default async)# async (default): Publish() enqueues the job and returns immediately (the# caller's inbound request receives hook_way's own 202 Accepted — NOT the# downstream response). Delivery outcome is observable via logs, metrics,# and (optionally) a dead_letter destination.# sync: Publish() delivers inline (with retries) and returns the outcome, so# the inbound request only succeeds when the downstream delivery does. This# adds delivery/retry latency within the finite publish budget. Use it for# critical, low-volume hooks where the sender# should see delivery failures directly. dead_letter does not apply to sync.## Each webhook destination has its OWN delivery engine (worker pool + queue), so# a slow target cannot block deliveries to other destinations. The pool size is# controlled by server.fanout_concurrency; max_queue bounds queued job count# (per-destination, default 1000), and max_queue_bytes bounds queued plus active# payload bytes (default 64 MiB). When a budget is full, Publish fails and the# upstream sender receives a non-2xx response, creating backpressure.## On shutdown, hook_way attempts to drain the queue within drain_timeout# (defaults to server.shutdown_timeout). An expired deadline or forced exit can# abandon queued and in-flight deliveries.## NON-DURABLE (async): a hard crash (kill -9, OOM, host failure) can lose events# already acknowledged to the sender. dead_letter handles observed terminal# failures while the process runs; it cannot recover jobs lost with the process.# Publish directly to a durable broker when accepted work must survive a crash.# See ../reliable-ingestion.yaml and docs/operations/delivery-guarantees.md.# Retries after a destination commits but loses its response may repeat work.# Preserve a stable provider identity and deduplicate atomically at the consumer;# Hookway's envelope ID changes on each new inbound provider request.# hook_way is not a transparent proxy — the downstream response is not returned# to the inbound sender in async mode.## `on_existing_conflict` is NOT valid for webhook destinations — the field is# rejected at config validation time. Do not set it on webhook entries.## LOOP PROTECTION: every outbound delivery is stamped with an X-Hookway-Hops# header (incremented per Hookway hop). If this URL points back at a Hookway# flow, put a `loop_guard` on that flow — `security.loop_protection` is leftover# YAML and is not wired to inbound rejection. See docs/reference/configuration.md.server: addr: ":8088" max_body_bytes: 1048576 # fanout_concurrency sets the worker count for each destination's own pool. fanout_concurrency: 8destinations: forward: type: webhook webhook: # Required: the target endpoint URL. Must be http:// or https://. url: "${WEBHOOK_URL}" # Optional: delivery mode. "async" (default) enqueues and returns 202; # "sync" delivers inline and returns the downstream outcome to the caller. delivery_mode: "async" # Optional: per-request HTTP timeout (default 10s). timeout: "10s" # Optional (async only): max number of pending deliveries in this # destination's queue (default 1000, now truly per-destination). When the # queue is full, inbound Publish calls fail (backpressure). max_queue: 1000 # Optional (async only): how long to drain queued deliveries on shutdown # (defaults to server.shutdown_timeout). drain_timeout: "30s" # Optional (async only): dead-letter routing for terminally-failed # deliveries (4xx, or 5xx/timeout after retries are exhausted). The # referenced destination MUST exist and MUST NOT be a webhook destination # (this makes delivery loops structurally impossible). The failed message # is enriched with a "dead_letter" block (destination, target, outcome, # attempts, failed_at) and published to the named destination/target. # dead_letter: # destination: "dlq-rabbit" # id of a non-webhook destination below # target: "webhook-dead-letters" # Optional retry policy. Retries are attempted on HTTP 429, 5xx, and # network-level errors (connection refused, timeout). Other 4xx responses # are terminal — no retry is attempted. 2xx is success. retry: max_attempts: 5 initial_backoff: "500ms" max_backoff: "30s" # --- Auth: choose ONE type; comment out the others --- # auth.type: hmac (default) # Signs each request with HMAC-SHA256. The default scheme # (hmac_sha256_ts_dot) sends: # X-Hookway-Signature: t=<unix_ts>,v1=<hex_HMAC_SHA256> # where the signing string is "<unix_ts>.<raw_body>". # hook_way also sends an X-Hookway-Event-Id header (the event id) so the # receiver can deduplicate replayed requests. auth: type: "hmac" secret: "${WEBHOOK_SECRET}" # signing secret; never log or expose # scheme: "hmac_sha256_ts_dot" # default; see docs for other schemes # header: "X-Hookway-Signature" # default signature header # event_id_header: "X-Hookway-Event-Id" # default event-id header # auth.type: api_key # Sets a static header on every outbound request. # auth: # type: "api_key" # header: "X-Api-Key" # value: "${WEBHOOK_API_KEY}" # auth.type: none # No authentication headers are added. Suitable for internal/trusted # networks only. # auth: # type: "none" # auth.type: proxy # Forwards the ORIGINAL inbound raw body and headers (minus hop-by-hop # headers) without adding any hook_way auth. The vendor's original auth # headers (e.g. X-Stripe-Signature) pass through to the downstream # endpoint. The event envelope is NOT used; the raw body is forwarded. # auth: # type: "proxy" # tls: # optional; only needed for a custom CA / mTLS # ca_cert: "" # path to PEM bundle for private/self-signed CAs # client_cert: "" # path to PEM cert for mutual TLS # client_key: "" # path to PEM key for mutual TLS (must pair with client_cert) # server_name: "" # override SNI/verification hostname # insecure_skip_verify: false # dev-only; disables cert verification; never use in production # Example dead-letter destination for the webhook above. Uncomment together # with the dead_letter block on `forward`. Must be a NON-webhook destination. # dlq-rabbit: # type: rabbitmq # rabbitmq: # url: "${RABBITMQ_URL}" # exchange: "hookway.dlq" # exchange_type: "direct" # declare_queues: true # declares the dead-letter target queue on startupflows: order_events: nodes: - {id: in, type: http_trigger, path: /webhooks/orders, methods: [POST]} - {id: hops, type: loop_guard, max_hops: 3} - {id: body, type: read_body, max_bytes: 1048576} - {id: auth, type: no_auth} - {id: env, type: envelope, body_format: json} # target is passed in the X-Hookway-Target header on envelope deliveries # and is used in log fields / metrics labels. - {id: out, type: publish, destination: forward, target: orders} edges: [in>hops, hops>body, body>auth, auth>env, env>out]