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.
Trigger Webhook
Section titled “Trigger Webhook”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) |
Authentication
Section titled “Authentication”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.
Request body (POST)
Section titled “Request body (POST)”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.
Response
Section titled “Response”{ "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
••••••.
Input structure
Section titled “Input structure”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.
Error responses
Section titled “Error responses”| 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 |
Examples
Section titled “Examples”# POST with JSON body and secretcurl -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 headercurl -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 parameterscurl "http://localhost:8080/hooks/health-check?env=production®ion=eu"Webhook name uniqueness
Section titled “Webhook name uniqueness”If multiple workspaces define a webhook with the same name, the first match wins at dispatch time.
Check Job Status
Section titled “Check Job Status”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 |
Authentication
Section titled “Authentication”Same as the trigger endpoint — provide the secret via query parameter or Authorization: Bearer header if the webhook has a secret configured.
Query parameters
Section titled “Query parameters”| 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) |
Response
Section titled “Response”{ "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 running400 Bad Request— Invalid job ID format401 Unauthorized— Missing or invalid secret404 Not Found— Webhook not found, or job was not created by this webhook503 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.
Security
Section titled “Security”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.
Examples
Section titled “Examples”# 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 timeoutcurl "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