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
Idempotency
Event Routing
Validated Inputs
Observability
Operational Limits
Quick Start
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). Themethod
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
orX-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:
{
"data": {
"status": "accepted",
"event": "..."
},
"message": "Success"
}
{
"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
Receive
Request hits POST /api/v0/hooks/{slug}
and body is validated as JSON (size limit applies).
Verify (optional)
If configured, the appropriate verifier runs: HMAC header, Stripe timestamp/skew, or GitHub signature.
Idempotency (optional)
Duplicate detection using X-Idempotency-Key
or a JSON dot-path key with TTL.
Filter & Map
Evaluate CEL filter
to select an event. Render input
templates into a typed payload.
Validate & Dispatch
Validate against JSON Schema (if provided). Dispatch matching event to workflow signal bus.
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
Property | Required | Description |
---|---|---|
name | Yes | Dispatched signal name for the workflow. |
filter | Yes | CEL expression evaluated against { payload, headers, query } ; typically payload.* . |
input | Yes | Map of template strings rendered with context { payload } . Missing keys render as empty string; other template errors return 400. |
schema | No | JSON 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
: Computesha256(body)
with secret; compare against hex in a custom header.verify: strategy: hmac secret: "{{ .env.WEBHOOK_SECRET }}" header: X-Sig
-
stripe
: UsesStripe-Signature
header (t=..., v1=...
) with timestamp skew check (default 5m, configurable).verify: strategy: stripe secret: "{{ .env.STRIPE_WEBHOOK_SECRET }}" skew: 5m
-
github
: UsesX-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:
webhooks:
default_method: POST
default_max_body: 1048576 # 1 MiB
default_dedupe_ttl: 10m
stripe_skew: 5m
Operational Insights
Item | Description | Details |
---|---|---|
Metrics – Counters | Total events observed by category. | compozy_webhook_received_total , ..._verified_total , ..._duplicate_total , ..._dispatched_total , ..._no_match_total , ..._failed_total (with reason attribute) |
Metrics – Histograms | Latency distributions for key stages. | ..._processing_duration_seconds , ..._verify_duration_seconds , ..._render_duration_seconds , ..._dispatch_duration_seconds |
Logging | Structured logs with correlation IDs and error taxonomy mapped to HTTP responses. | Correlation ID precedence: X-Correlation-ID → X-Request-ID → generated by Compozy. |
Processing Window | Per-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
Workflow
Configure workflow-level settings including inputs, outputs, triggers, scheduling, and environment variables in Compozy
CLI Overview
The Compozy CLI is your primary interface for managing AI workflow orchestration, providing intuitive commands for project management, workflow execution, and system control