Skip to content

Webhook API

Webhook endpoints are served at /hooks/ (outside /api/) and do not require user authentication. Per-trigger authentication is handled via an optional secret field.

POST /hooks/{name}
GET /hooks/{name}

Fires a webhook trigger by name, creating a new job for the associated task.

Parameter Description
name Webhook trigger name (as defined in trigger YAML)

If the webhook trigger has a secret configured, the caller must provide it via:

  • Query parameter: ?secret=whsec_abc123
  • Authorization header: Authorization: Bearer whsec_abc123

Webhooks without a secret are public and accept any request.

For a webhook with force_refresh: true, the secret is checked twice: first against the definition the server has loaded, before the workspace is reloaded (an unauthenticated caller never triggers a git fetch), then against the refreshed definition. The job is created from the refreshed definition, so a refresh that changed the webhook’s task or ref takes effect at once. A secret newly rotated in git is accepted only after the server has loaded it.

The body is included in the job input. JSON bodies (with Content-Type: application/json) are parsed; other content types are passed as a raw string.

{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"trigger": "github-push",
"task": "ci-pipeline"
}

The job is created in the workspace that owns the trigger’s task — another workspace for task: ws.task — at the trigger’s ref, if it has one (see Git Refs).

In mode: sync, the response also carries status and output once the job finishes (200), or status: "running" without output on timeout (202). output is masked like job detail: secret values appear as ••••••.

The webhook handler builds a structured input object:

{
"body": { "ref": "refs/heads/main", "commits": [] },
"headers": { "content-type": "application/json", "x-github-event": "push" },
"method": "POST",
"query": { "env": "production" },
"environment": "staging"
}
Field Type Description
body object/string/null Parsed JSON, raw string, or null (GET)
headers object All request headers with lowercase keys
method string "GET" or "POST"
query object Query parameters (excluding secret)

Trigger YAML input defaults merge at the top level. Reserved keys (body, headers, method, query) are not overwritten by defaults.

Status Description
400 The trigger’s target cannot be resolved: unknown task or workspace, a ref that does not exist or is invalid, a task missing at the ref, YAML at the ref that does not load
401 Missing or invalid secret
404 Webhook not found or disabled — including a webhook removed by its own force_refresh
500 The workspace is unavailable after force_refresh, a transient failure resolving the target (for example, the git server is unreachable), or another job-creation error
503 Sync mode only: the job finished, but the secrets needed to mask its output cannot be loaded right now. The body is {"job_id": "...", "error": "redaction set unavailable, retry"}; poll the job-status endpoint later
Terminal window
# POST with JSON body and secret
curl -X POST http://localhost:8080/hooks/github-push?secret=whsec_abc123 \
-H "Content-Type: application/json" \
-d '{"ref": "refs/heads/main", "commits": []}'
# POST with secret via header
curl -X POST http://localhost:8080/hooks/github-push \
-H "Authorization: Bearer whsec_abc123" \
-H "Content-Type: application/json" \
-d '{"ref": "refs/heads/main"}'
# GET (public webhook, no secret)
curl http://localhost:8080/hooks/health-check
# GET with query parameters
curl "http://localhost:8080/hooks/health-check?env=production&region=eu"

If multiple workspaces define a webhook with the same name, the first match wins at dispatch time.

GET /hooks/{name}/jobs/{job_id}

Check the status of a job that was created by this webhook trigger. Uses the same authentication as the trigger itself.

Parameter Description
name Webhook trigger name
job_id Job ID returned from the trigger response

Same as the trigger endpoint — provide the secret via query parameter or Authorization: Bearer header if the webhook has a secret configured.

Parameter Default Description
secret — Webhook secret (if configured)
wait false Set to true to block until the job reaches a terminal state
timeout 30 Maximum wait time in seconds when wait=true (max: 300)
{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"trigger": "github-push",
"task": "ci-pipeline",
"status": "completed",
"output": { "result": "ok" },
"created_at": "2026-03-12T10:30:00+00:00",
"completed_at": "2026-03-12T10:30:05+00:00"
}
Field Type Description
job_id string The job identifier
trigger string Webhook trigger name
task string Target task name
status string pending, running, completed, failed, or cancelled
output object/null Job output (only populated for completed jobs), with secret values masked as ••••••
created_at string ISO 8601 timestamp of job creation
completed_at string/null ISO 8601 timestamp of job completion (null if still running)

HTTP status codes:

  • 200 OK — Job found (or wait completed with terminal status)
  • 202 Accepted — Wait timed out, job still running
  • 400 Bad Request — Invalid job ID format
  • 401 Unauthorized — Missing or invalid secret
  • 404 Not Found — Webhook not found, or job was not created by this webhook
  • 503 Service Unavailable — The secrets needed to mask the job’s output cannot be loaded right now (a pinned commit the job is connected to is unreachable). The body is {"job_id": "...", "error": "redaction set unavailable, retry"}; retry later

Every response that carries job data (including the 503) has Cache-Control: no-store.

output is masked with the same secret set as GET /api/jobs/{id}. When a pinned commit the job is connected to can never be loaded again (for example, it was force-pushed away), the answer is 200 with every string in output masked.

This endpoint only returns jobs with source_type = "webhook" and a matching source_id for the trigger. It cannot be used to query arbitrary jobs.

It is authenticated by the webhook’s own secret, not by user ACL: anyone holding the secret can read the status and (masked) output of the jobs that webhook created, whatever task folder they run in.

Terminal window
# Check status (secret via query param)
curl "http://localhost:8080/hooks/github-push/jobs/a1b2c3d4?secret=whsec_abc123"
# Check status (secret via header)
curl http://localhost:8080/hooks/github-push/jobs/a1b2c3d4 \
-H "Authorization: Bearer whsec_abc123"
# Wait for completion with 60s timeout
curl "http://localhost:8080/hooks/github-push/jobs/a1b2c3d4?secret=whsec_abc123&wait=true&timeout=60"
# Public webhook (no secret needed)
curl http://localhost:8080/hooks/health-check/jobs/a1b2c3d4