Configuration

Webhooks

Configure webhook triggers to route external HTTP events into Compozy workflows with verification, idempotency, filtering, and schema validation.

Overview

Webhooks let external systems trigger your workflows via HTTP. Each webhook belongs to a workflow and exposes a public endpoint at /api/v0/hooks/{slug}. Incoming requests are validated, optionally verified (HMAC/Stripe/GitHub), deduplicated, filtered with CEL expressions, transformed via templates, validated against JSON Schema, and finally dispatched to the workflow as a signal event.

Key Capabilities

Signature Verification

HMAC, Stripe, and GitHub strategies to verify request integrity

Idempotency

Prevent duplicate processing via header or payload key with TTL

Event Routing

Route to named events using CEL filters over payload, headers, and query

Validated Inputs

Render inputs with templates then validate using JSON Schema

Observability

Structured logs and rich OpenTelemetry counters and histograms

Operational Limits

Configurable body size, dedupe TTL, Stripe skew, and 30s processing window

Quick Start

workflow.yaml
id: orders
version: "1.0.0"

triggers:
  - type: webhook
    webhook:
      slug: shopify-events           # Public path: POST /api/v0/hooks/shopify-events
      events:
        - name: order.created        # Will be dispatched as a workflow signal
          filter: payload.type == "order_created"
          input:                     # Map incoming JSON to your workflow inputs
            order_id: "{{ .payload.data.id }}"
            email: "{{ .payload.data.email }}"
          schema:                    # (optional) Validate the rendered input
            type: object
            properties:
              order_id: { type: string }
              email: { type: string, format: email }
            required: [order_id]

Send a request:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"type":"order_created","data":{"id":"o-123","email":"a@x.com"}}' \
  http://localhost:5001/api/v0/hooks/shopify-events

Endpoint & Responses

  • Path: POST /api/v0/hooks/{slug}
  • Method: Public webhook endpoint currently accepts only POST (v0). The method field in config is validated but does not change the public route yet.
  • Request body: JSON (validated as well‑formed; default max body: 1MB)
  • Optional headers:
    • X-Idempotency-Key: Prevents duplicate processing (see Idempotency)
    • X-Correlation-ID or X-Request-ID: For trace correlation (auto‑generated if missing)
    • Signature headers (strategy specific): X-Sig (HMAC – configurable), Stripe-Signature, X-Hub-Signature-256

Responses always use the { data, message } envelope:

202 Accepted
{
  "data": {
    "status": "accepted",
    "event": "..."
  },
  "message": "Success"
}
204/200 No match
{
  "data": {
    "status": "no_matching_event"
  },
  "message": "Success"
}
  • 400 Bad Request (invalid JSON/filters/templates)
  • 401 Unauthorized (verification failed)
  • 404 Not Found (unknown slug)
  • 409 Conflict (duplicate idempotency key)
  • 422 Unprocessable Entity (schema validation failed)
  • 500 Internal Server Error

How It Works

1

Receive

Request hits POST /api/v0/hooks/{slug} and body is validated as JSON (size limit applies).

2

Verify (optional)

If configured, the appropriate verifier runs: HMAC header, Stripe timestamp/skew, or GitHub signature.

3

Idempotency (optional)

Duplicate detection using X-Idempotency-Key or a JSON dot-path key with TTL.

4

Filter & Map

Evaluate CEL filter to select an event. Render input templates into a typed payload.

5

Validate & Dispatch

Validate against JSON Schema (if provided). Dispatch matching event to workflow signal bus.

6

Respond

Return { data, message } envelope with appropriate status (202 on dispatch, 204/200 on no match).

Configuration

Define webhooks under triggers with type: webhook and a webhook object.

triggers:
  - type: webhook
    webhook:
      slug: my-webhook        # required, unique across workflows at runtime
      method: POST            # optional; allowed: POST, PUT, PATCH (default: POST)
      verify:                 # optional signature verification
        strategy: none|hmac|stripe|github
        secret: "{{ .env.WEBHOOK_SECRET }}"   # use env via template
        header: X-Sig                   # required for hmac strategy
        skew: 5m                        # optional for stripe (default 5m)
      dedupe:                 # optional idempotency configuration
        enabled: true
        ttl: 10m              # overrides application default
        key: data.id          # dot-path within JSON body when header is absent
      events:                 # one or more routable events
        - name: user.created
          filter: payload.event == "user.created"
          input:
            id: "{{ .payload.data.id }}"
            email: "{{ .payload.data.email }}"
          schema: { type: object, required: [id] }

Events

PropertyRequiredDescription
nameYesDispatched signal name for the workflow.
filterYesCEL expression evaluated against { payload, headers, query }; typically payload.*.
inputYesMap of template strings rendered with context { payload }. Missing keys render as empty string; other template errors return 400.
schemaNoJSON Schema validating the rendered input; validation errors return 422.

Examples:

filter: payload.type in ["order_created", "order_updated"]
input:
  order_id: "{{ .payload.data.id }}"
  total: "{{ .payload.data.total }}"

Verification

verify:
  strategy: hmac
  secret: "{{ .env.WEBHOOK_SECRET }}"
  header: X-Sig
curl -X POST http://localhost:5001/api/v0/hooks/hmac \
  -H "Content-Type: application/json" \
  -H "X-Sig: <hex_sha256(body)>" \
  -d '{"event":"x","data":{}}'

Choose one strategy per webhook:

  • none: No verification.

  • hmac: Compute sha256(body) with secret; compare against hex in a custom header.

    verify:
      strategy: hmac
      secret: "{{ .env.WEBHOOK_SECRET }}"
      header: X-Sig
  • stripe: Uses Stripe-Signature header (t=..., v1=...) with timestamp skew check (default 5m, configurable).

    verify:
      strategy: stripe
      secret: "{{ .env.STRIPE_WEBHOOK_SECRET }}"
      skew: 5m
  • github: Uses X-Hub-Signature-256: sha256=... with HMAC‑SHA256.

    verify:
      strategy: github
      secret: "{{ .env.GITHUB_WEBHOOK_SECRET }}"

Example requests:

# GitHub
curl -X POST http://localhost:5001/api/v0/hooks/github-issues \
  -H "Content-Type: application/json" \
  -H "X-GitHub-Event: push" \
  -H "X-Hub-Signature-256: sha256=..." \
  -d '{"ref":"refs/heads/main","repository":{...}}'

# Stripe
curl -X POST http://localhost:5001/api/v0/hooks/stripe \
  -H "Content-Type: application/json" \
  -H "Stripe-Signature: t=...,v1=..." \
  -d '{"id":"evt_...","object":"event","type":"payment_intent.succeeded","data":{...}}'

Idempotency

Prevent duplicate processing of retried deliveries:

  • If X-Idempotency-Key header is present, it is used directly.
  • Otherwise, when dedupe.key is set, a key is derived from that JSON path in the request body.
  • Keys are namespaced per slug internally. Default TTL is 10m (configurable).
dedupe:
  enabled: true
  ttl: 30m
  key: data.id

Limits & Defaults (application config)

Global defaults are controlled by the server configuration:

config.yaml
webhooks:
  default_method: POST
  default_max_body: 1048576    # 1 MiB
  default_dedupe_ttl: 10m
  stripe_skew: 5m

Operational Insights

ItemDescriptionDetails
Metrics – CountersTotal events observed by category.compozy_webhook_received_total, ..._verified_total, ..._duplicate_total, ..._dispatched_total, ..._no_match_total, ..._failed_total (with reason attribute)
Metrics – HistogramsLatency distributions for key stages...._processing_duration_seconds, ..._verify_duration_seconds, ..._render_duration_seconds, ..._dispatch_duration_seconds
LoggingStructured logs with correlation IDs and error taxonomy mapped to HTTP responses.Correlation ID precedence: X-Correlation-IDX-Request-ID → generated by Compozy.
Processing WindowPer-request processing timeout.~30 seconds maximum event processing time per webhook request.

End-to-End Examples

GitHub Issues Comment

triggers:
  - type: webhook
    webhook:
      slug: github-issues
      verify:
        strategy: github
        secret: "{{ .env.GITHUB_WEBHOOK_SECRET }}"
      dedupe:
        enabled: true
        key: delivery.id
      events:
        - name: issue.commented
          filter: payload.action == "created" && payload.issue != null
          input:
            issue_id: "{{ .payload.issue.id }}"
            body: "{{ .payload.comment.body }}"

Stripe Payment Succeeded

triggers:
  - type: webhook
    webhook:
      slug: stripe
      verify:
        strategy: stripe
        secret: "{{ .env.STRIPE_WEBHOOK_SECRET }}"
      events:
        - name: payment.succeeded
          filter: payload.type == "payment_intent.succeeded"
          input:
            id: "{{ .payload.data.object.id }}"
            amount: "{{ .payload.data.object.amount_received }}"

Next Steps