Configure outbound webhooks
Send signed, durable notifications when Helix creates a spec task, changes a task status, or publishes an artifact.
Outbound webhooks notify another service when work changes in Helix. They are useful for CI orchestration, security automation, reporting, and any integration that needs to react to spec-task or artifact events.
This is different from an agent app webhook trigger: a trigger sends a request to Helix to run an agent, while an outbound webhook sends an event from Helix to your service.
Helix implements the Standard Webhooks specification. Deliveries are signed and retried, and each endpoint can subscribe to an entire organisation or one project.
Before you start
You need:
- An organisation owner account and its API key
- The organisation ID or slug
- A public HTTPS receiver that can preserve the raw request body
Only organisation owners can manage endpoints. An organisation can have up to 25 active endpoints.
Set these values for the examples below:
export HELIX_URL="https://app.helix.ml"
export HELIX_API_KEY="your-api-key"
export HELIX_ORG="your-org-id-or-slug"For a self-hosted deployment, set HELIX_URL to the public URL of your Helix control plane.
Create an endpoint
Create an endpoint with POST /api/v1/organizations/{org}/webhook-endpoints:
curl --fail-with-body \
-X POST "$HELIX_URL/api/v1/organizations/$HELIX_ORG/webhook-endpoints" \
-H "Authorization: Bearer $HELIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://automation.example.com/webhooks/helix",
"description": "Production task automation",
"project_id": "project_...",
"events": ["spec_task.status_changed", "artifact.published"]
}'project_id is optional. Omit it to receive matching events from every project in the organisation. Omit events to subscribe to all supported events, or pass ["*"].
The response contains the new endpoint and its signing secret:
{
"endpoint": {
"id": "whep_...",
"organization_id": "org_...",
"project_id": "project_...",
"url": "https://automation.example.com/webhooks/helix",
"events": ["artifact.published", "spec_task.status_changed"],
"status": "active",
"secret_preview": "9xY="
},
"secret": "whsec_..."
}Store secret in your receiver's secret manager. Helix returns the full value only when you create an endpoint or rotate its secret. Later API responses contain only secret_preview.
Events
| Event | Sent when | data fields |
|---|---|---|
spec_task.created | A spec task becomes available in a project | spec_task_id, project_id, organization_id, status, status_updated_at |
spec_task.status_changed | A spec task moves to another status | The fields above, plus previous_status |
artifact.published | Helix creates an artifact or publishes a new version | artifact_id, project_id, organization_id, active_version_id, kind, source_spec_task_id |
The request body is a small notification rather than a complete resource snapshot:
{
"id": "whevt_...",
"api_version": "v1",
"type": "spec_task.status_changed",
"organization_id": "org_...",
"project_id": "project_...",
"data": {
"spec_task_id": "task_...",
"project_id": "project_...",
"organization_id": "org_...",
"status": "done",
"previous_status": "pull_request",
"status_updated_at": "2026-09-22T12:00:00Z"
},
"timestamp": "2026-09-22T12:00:00Z"
}Use the IDs in data to fetch current state from the Helix API. An event can be delayed or replayed, so do not treat the body as the latest version of the task or artifact.
Verify requests
Every delivery includes these headers:
| Header | Meaning |
|---|---|
webhook-id | Delivery ID. It stays the same across automatic retries. |
webhook-timestamp | Unix timestamp used to sign the request. |
webhook-signature | One or more Standard Webhooks signatures. |
webhook-type | Event type, such as artifact.published. |
Verify webhook-id, webhook-timestamp, and the exact raw request body with your Standard Webhooks library and the whsec_... secret. Do this before parsing or acting on the JSON. The Standard Webhooks libraries handle signature parsing and timestamp checks.
After verification:
- Deduplicate on
webhook-id. - Queue any longer work.
- Return a
2xxresponse quickly. - Fetch the referenced task or artifact from Helix before acting on it.
Your receiver must tolerate duplicate deliveries. Automatic retries reuse the same webhook-id; a manual replay also reuses the delivery.
Delivery and retries
Helix records the event in the same database transaction as the state change, then delivers it asynchronously. This prevents a committed change from losing its notification if an API process restarts.
- Any
2xxresponse marks the delivery successful. - Helix does not follow redirects.
410 Gonedisables the endpoint and stops new deliveries to it.- Other failures retry with exponential backoff and jitter. The default is eight attempts over several days.
- A valid
Retry-Afterresponse header can postpone the next attempt.
Design your receiver for at-least-once delivery: verify the signature, deduplicate, acknowledge, and process asynchronously.
Inspect and replay deliveries
List the most recent 50 deliveries for an endpoint:
curl --fail-with-body \
"$HELIX_URL/api/v1/organizations/$HELIX_ORG/webhook-endpoints/whep_.../deliveries" \
-H "Authorization: Bearer $HELIX_API_KEY"Each result includes the event type, delivery status, attempt count, last HTTP status, last error category, and timestamps. Status is one of pending, processing, retrying, delivered, failed, or disabled.
Replay a delivery after fixing the receiver:
curl --fail-with-body \
-X POST "$HELIX_URL/api/v1/organizations/$HELIX_ORG/webhook-endpoints/whep_.../deliveries/whd_.../replay" \
-H "Authorization: Bearer $HELIX_API_KEY"The API returns 202 Accepted. A delivery cannot be replayed while it is being processed.
Update or disable an endpoint
PUT replaces the endpoint configuration. Include the URL and the complete event selection you want to keep:
curl --fail-with-body \
-X PUT "$HELIX_URL/api/v1/organizations/$HELIX_ORG/webhook-endpoints/whep_..." \
-H "Authorization: Bearer $HELIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://automation.example.com/webhooks/helix",
"description": "Production task automation",
"events": ["*"],
"enabled": true
}'Disable an endpoint with DELETE:
curl --fail-with-body \
-X DELETE "$HELIX_URL/api/v1/organizations/$HELIX_ORG/webhook-endpoints/whep_..." \
-H "Authorization: Bearer $HELIX_API_KEY"This disables the endpoint rather than removing its delivery history. Set enabled to true in a later PUT request to enable it again.
Rotate a signing secret
curl --fail-with-body \
-X POST "$HELIX_URL/api/v1/organizations/$HELIX_ORG/webhook-endpoints/whep_.../rotate-secret" \
-H "Authorization: Bearer $HELIX_API_KEY"Store the returned secret immediately. For the next 24 hours, Helix signs deliveries with both the new and previous secrets. Configure the receiver to accept the new secret during that overlap, then remove the old one.
Self-hosted configuration
Outbound webhooks work without extra configuration. Operators can tune the worker with the WEBHOOK_* environment variables.
Production endpoints must use HTTPS and resolve to public IP addresses. For local development only, WEBHOOK_ALLOW_PRIVATE_ENDPOINTS=true permits HTTP and private or loopback destinations. Do not enable it on an internet-facing deployment.