Webhooks
A webhook in Catentio is an inbound thing: a gate in a pipeline that blocks until an external system calls back. The pipeline waits; your system tells it to go.
That direction matters, so state it plainly:
Outbound webhooks now exist — see Webhooks (outbound). This page is the inbound gate feature, which is a different thing: a pipeline step that blocks until an external system calls back.
The /v1/webhooks namespace has exactly one endpoint: GET /v1/webhooks, which lists webhook gates.
What a webhook gate is
A pipeline workflow declares a step as a gate with gate_kind: webhook:
{
"step_slug": "await_render_farm",
"kind": "gate",
"gate_kind": "webhook"
}
When the runner reaches that step, its subtask goes blocked and nothing downstream of it runs. It stays blocked — indefinitely — until something outside Catentio resolves it. That's the whole feature: a pipeline pause that an external system controls.
webhook is one of four gate kinds (human, webhook, schedule, pipeline_dep). Human gates are the fully shipped surface with portal Approve/Reject buttons; webhook gates are resolved programmatically, as described below.
List webhook gates
GET /v1/webhooks
Returns every subtask with kind = gate and gate_kind = webhook — both the ones currently waiting and the recently resolved history. Ordering puts waiting gates first, then most-recent activity.
Query params:
| Param | Type | Notes |
|---|---|---|
status |
string | Filter to one subtask status. A gate that is currently waiting is blocked. Full set: pending, running, blocked, review, succeeded, failed, skipped, stale. |
limit |
int (1–1000) | Default 200. |
Response:
{
"data": [
{
"subtask_id": "9f3c1b0e-5d2a-4c77-9a1e-6b0f2d8c4a11",
"project_id": "spring-lookbook-a1b2c3d4",
"project_title": "Spring lookbook",
"workflow_slug": "content",
"phase_slug": "render",
"step_slug": "await_render_farm",
"task_id": "1c7e8a44-2f90-4b3d-8e15-7a6c9d0b2e33",
"status": "blocked",
"gate_config": {},
"consumes_artifacts": [],
"consumes_subtasks": [],
"created_at": "2026-05-12T10:42:00.000Z",
"started_at": "2026-05-12T10:42:00.512Z",
"completed_at": null
}
],
"meta": {
"total": 1,
"by_status": [{ "status": "blocked", "count": 1 }]
}
}
| Field | Type | Notes |
|---|---|---|
subtask_id |
string (uuid) | The gate subtask. This is what you pass when resolving it. |
project_id |
string | Project id — a title slug plus a hex suffix (no prj_ prefix). |
project_title |
string | Project title. |
workflow_slug |
string | The workflow the project was built from. |
phase_slug |
string | The phase (task) the gate sits in. |
step_slug |
string | The gate step's slug, as declared in the workflow. |
task_id |
string (uuid) | The parent task (phase). |
status |
string | Subtask status — blocked while waiting. |
gate_config |
object | The gate's config from the workflow. {} when unset. |
consumes_artifacts |
array | Artifact slugs the gate consumes. |
consumes_subtasks |
array | Sibling step slugs the gate consumes. |
created_at |
timestamp | ISO 8601. |
started_at |
timestamp | null | When the runner reached the gate. |
completed_at |
timestamp | null | When it resolved. null while waiting. |
meta.by_status is a count of all webhook gates by status, not just the ones in data.
const { data } = await catentio.webhooks.list();
gates = catentio.webhooks.list()
curl 'https://catent.io/api/v1/cp/v1/webhooks?status=blocked' \
-H "Cookie: catentio_session=<session cookie set at login>"
This is the only endpoint in the namespace. There is no POST /v1/webhooks and no DELETE /v1/webhooks/{id} — see A note on the SDK surface.
Resolving a gate
A webhook gate is resolved through the runtime tool pipeline_resolve_webhook_gate, which writes the gate's output and wakes the project back to active so the runner picks it up.
It is a SENSITIVE, agent-invoked tool, not a public REST route. There is no unauthenticated POST /hooks/... URL that an arbitrary third party can hit; the callback path runs through an agent or an internal handler that invokes the tool (an HTTP route you own, a Discord relay, a GitHub webhook handler, and so on).
Params:
| Param | Type | Notes |
|---|---|---|
project_id |
string | Required. |
subtask_id |
string | Required. The gate subtask id from GET /v1/webhooks. |
decision |
string | approved or rejected. Defaults to approved — webhook callers almost always approve; rejected is for the rare case where the external system declines. |
decided_by |
string | Free-form source identifier, max 64 chars. Defaults to webhook. E.g. github-webhook, stripe-webhook. Recorded in the event log and the human_decision.v1 payload. |
payload |
object | null | Optional free-form payload from the external system. Kept on the gate's output under payload. |
comment |
string | null | Optional. |
The tool rejects the call if the subtask doesn't exist, isn't a gate, or isn't gate_kind = webhook.
Portal
Dashboard → Webhooks (/dashboard/webhooks) renders this same list — what's currently blocked on an external callback, plus the recently resolved history. It's a read-only inspection view.
Looking for outbound webhooks?
They exist, and they are a different feature on a different surface: Webhooks (outbound), on the public API. Register a URL, and Catentio POSTs to it when a run or project finishes — HMAC-signed, retried with backoff.
This page is the inbound gate feature: a pipeline step that blocks until an external system calls back to release it. The two are easy to confuse and point in opposite directions.
The runtime also writes internal project_events rows (topics like project_started, project_completed — underscored, not dotted). Those are database rows for audit and observability, read with GET /v1/events; they are a separate, larger set from the three deliverable webhook events.
A note on the SDK surface
The SDKs expose webhooks.create() and webhooks.delete() alongside webhooks.list(). Only list() works. create() and delete() target POST /v1/webhooks and DELETE /v1/webhooks/{id}, which the control plane does not implement — calling them fails. They are a leftover of the outbound surface described above and should not be used.
Errors
| Status | When |
|---|---|
| 401 | Missing or invalid credential. |
| 502 | CP couldn't reach the runtime to fetch gate rows. |
Next
- Gates & approvals — all four gate kinds, and the human-approval flow.
- Events — the queryable event archive (the pull channel; there is no push channel).
- Projects — the pipelines these gates live in.