Skip to content

Webhooks

Set up signed CompozyOS webhook triggers for CI, deployment, and external event integrations.

For people running agent work5 pages in this section

Webhooks are the external ingress path for automation. They accept signed HTTP requests, normalize the request into a webhook activation envelope, and dispatch every enabled webhook trigger that matches the endpoint, scope, workspace, and filters.

There is no separate webhook CLI group. Webhooks are configured as triggers with event = "webhook".

Routes

Webhook endpoint paths include the endpoint slug and webhook ID in one segment:

<endpoint_slug>--<webhook_id>

The webhook ID starts with wbh_.

ScopeRoute
GlobalPOST /api/webhooks/global/<endpoint_slug>--<webhook_id>
WorkspacePOST /api/webhooks/workspaces/<workspace_id>/<endpoint_slug>--<webhook_id>

CompozyOS returns a result object with the number of matched triggers and the runs created from the delivery.

{
  "result": {
    "matched": 1,
    "runs": [
      {
        "id": "run_...",
        "trigger_id": "trg_...",
        "status": "scheduled"
      }
    ]
  }
}

Configure a webhook trigger

Config-backed webhook triggers need an endpoint slug and a webhook_secret_ref. Use env:NAME for operator-managed environment variables or a vault:automation/... ref for CompozyOS-managed encrypted secret storage. CompozyOS resolves the secret only when validating signed webhook requests. Store CompozyOS-managed values with compozy vault put, the Vault page, or the write-only API before enabling the trigger.

[[automation.triggers]]
scope = "workspace"
workspace = "/Users/you/src/checkout-api"
name = "deploy-webhook"
event = "webhook"
endpoint_slug = "deploy"
webhook_secret_ref = "env:COMPOZY_DEPLOY_WEBHOOK_SECRET"
agent = "deployer"
prompt = "Validate deployment {{ index .Data \"sha\" }} on {{ index .Data \"branch\" }}."
filter = { "data.action" = "deploy", "data.branch" = "main" }
retry = { strategy = "backoff", max_retries = 2, base_delay = "5s" }
fire_limit = { max = 6, window = "1h" }

Dynamic webhook triggers can be created with the CLI by passing the secret directly. The secret is write-only through the API surface: CompozyOS uses it for signature validation, but it is not returned by read endpoints.

compozy automation triggers create \
  --name deploy-webhook \
  --scope workspace \
  --workspace /Users/you/src/checkout-api \
  --event webhook \
  --endpoint-slug deploy \
  --webhook-secret "$COMPOZY_DEPLOY_WEBHOOK_SECRET" \
  --agent deployer \
  --prompt 'Validate deployment {{ index .Data "sha" }} on {{ index .Data "branch" }}.' \
  --filter data.action=deploy,data.branch=main \
  --retry backoff:2:5s

Read the trigger to get its webhook path:

compozy automation triggers get <trigger-id>

Publish the delivery URL

When the public gateway ingress surface has a verified address, trigger reads include an ingress object. Its url is the complete delivery URL. reachability tells you whether it is live, unconfirmed, off, or needs reconfirmation_required; do not treat the presence of a URL as proof that delivery is enabled.

Confirm the trigger against the current public endpoint through the private HTTP listener or UDS:

POST /api/gateway/ingress-bindings
Content-Type: application/json

{
  "subject_kind": "webhook_trigger",
  "subject_id": "<trigger-id>",
  "confirmed": true
}

The daemon resolves the trigger's global or workspace owner. A caller cannot select or override that scope. If the public address changes, the endpoint generation changes and the trigger reports reconfirmation_required until it is confirmed again. Remove a confirmation with DELETE /api/gateway/ingress-bindings/webhook_trigger/<trigger-id>.

Authentication

Every webhook request must include these headers:

HeaderRequiredMeaning
X-Compozy-Webhook-TimestampYesUnix seconds or RFC3339 timestamp. Must be within the freshness window.
X-Compozy-Webhook-SignatureYessha256= followed by the HMAC-SHA256 digest.
X-Compozy-Webhook-Delivery-IDYesIdempotency key. Replays are rejected while the delivery ID is inside the freshness window.

The freshness window is 5m. Requests older than the window, too far in the future, or signed with the wrong secret are rejected.

The signature message is:

<unix_timestamp>.<raw_request_body>

Use Unix seconds for the timestamp header unless your integration has a reason to send RFC3339. When CompozyOS receives RFC3339, it parses the timestamp and validates the signature against the parsed Unix seconds value.

Payload format

Request bodies are limited to 1MiB.

If the body is a JSON object, CompozyOS exposes its fields under .Data for filters and prompt templates. If the JSON object does not already include payload, CompozyOS also adds the raw request body as .Data.payload.

CompozyOS adds webhook metadata to every delivery:

Data keyMeaning
endpointFull endpoint segment.
endpoint_slugHuman-readable slug from the endpoint.
webhook_idStable webhook ID.
delivery_idSender delivery ID, also stored in the run metadata.
timestampParsed delivery timestamp.

If the body is empty or not a JSON object, CompozyOS exposes the raw body as .Data.payload alongside the metadata fields.

Send a signed request

This example posts a deployment payload to a workspace webhook. It uses openssl to calculate the HMAC signature.

body='{"action":"deploy","branch":"main","repository":"checkout-api","sha":"abc123"}'
timestamp="$(date -u +%s)"
signature="sha256=$(printf '%s.%s' "$timestamp" "$body" | openssl dgst -sha256 -hmac "$COMPOZY_DEPLOY_WEBHOOK_SECRET" -hex | awk '{print $2}')"

curl -sS -X POST "http://localhost:2123/api/webhooks/workspaces/ws_123/deploy--wbh_abc123" \
  -H "Content-Type: application/json" \
  -H "X-Compozy-Webhook-Timestamp: $timestamp" \
  -H "X-Compozy-Webhook-Signature: $signature" \
  -H "X-Compozy-Webhook-Delivery-ID: deploy-$timestamp" \
  --data "$body"

With the trigger above, CompozyOS only dispatches when both filters match:

  • data.action = deploy
  • data.branch = main

Worked example: CI deployment review

This GitHub Actions step notifies CompozyOS after a deployment run reaches the point where an agent should validate the deployment request.

name: Notify CompozyOS deployment automation

on:
  workflow_dispatch:

jobs:
  notify-compozy:
    runs-on: ubuntu-latest
    steps:
      - name: Send signed deployment webhook
        env:
          COMPOZY_WEBHOOK_URL: ${{ secrets.COMPOZY_WEBHOOK_URL }}
          COMPOZY_WEBHOOK_SECRET: ${{ secrets.COMPOZY_WEBHOOK_SECRET }}
          BRANCH: ${{ github.ref_name }}
          REPOSITORY: ${{ github.repository }}
          SHA: ${{ github.sha }}
        run: |
          body=$(jq -nc \
            --arg action deploy \
            --arg branch "$BRANCH" \
            --arg repository "$REPOSITORY" \
            --arg sha "$SHA" \
            '{action:$action, branch:$branch, repository:$repository, sha:$sha}')

          timestamp=$(date -u +%s)
          signature="sha256=$(printf '%s.%s' "$timestamp" "$body" | openssl dgst -sha256 -hmac "$COMPOZY_WEBHOOK_SECRET" -hex | awk '{print $2}')"

          curl -sS -X POST "$COMPOZY_WEBHOOK_URL" \
            -H "Content-Type: application/json" \
            -H "X-Compozy-Webhook-Timestamp: $timestamp" \
            -H "X-Compozy-Webhook-Signature: $signature" \
            -H "X-Compozy-Webhook-Delivery-ID: deploy-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
            --data "$body"

Store the full webhook URL in COMPOZY_WEBHOOK_URL, for example:

https://gateway.example.com/api/webhooks/workspaces/ws_123/deploy--wbh_abc123

Errors

StatusWhen CompozyOS returns it
400Invalid endpoint shape, missing required webhook headers, oversized body, or missing webhook secret configuration.
401Stale timestamp, future timestamp, or invalid webhook signature.
404No trigger registration matches the route.
409Disabled trigger, replay delivery ID, fire limit, duplicate registration, or read-only definition conflict.
429The delivery exceeded the per-endpoint and transport-source request budget; retry after Retry-After.
503Automation manager is not running.

Monitoring deliveries

List failed webhook-triggered runs:

compozy automation runs --status failed --last 20

Read one run:

compozy automation runs get <run-id>

Use trigger history when you know which webhook trigger owns the endpoint:

compozy automation triggers history <trigger-id> --last 20

On this page