Webhooks API
Manage webhook subscriptions — the signed events Console sends to your URL when something happens in your agents — and inspect their deliveries. Concepts, the event catalogue, signing and retries are in the Webhooks guide; this page is the route reference.
Two surfaces expose the same routes with the same rules; they differ only in how they authenticate:
| Surface | Base path | Auth |
|---|---|---|
| Client API | /api/client/v1/webhooks | Authorization: Bearer cpeer_… |
| Dashboard | /api/webhooks | Browser session |
Everything below is shown for the client API.
Conventions
- Endpoint and delivery objects are camelCase (
projectId,disabledReason,failingSince,eventType,nextAttemptAt), like the other resource-management endpoints. (The agent endpoints you call from a handler — runs, sessions, events — aresnake_case.) - Errors are
{ "error": "<message>", "code": "<code>" }. - A token acts in its own project only, and manages that project's endpoints when its owner is a project admin or a tenant owner / admin. It never manages a tenant-wide endpoint or another project's, whoever owns it. Anyone else gets
403 forbidden; an endpoint outside your reach is404 not_found. - A tenant can have at most 50 endpoints.
Endpoint map
| Endpoint | |
|---|---|
GET /webhooks/events | The event catalogue |
GET /webhooks, POST /webhooks | List; create (returns secret once) |
GET /webhooks/{id}, PATCH /webhooks/{id}, DELETE /webhooks/{id} | Read; edit url, description, events, status; delete |
POST /webhooks/{id}/rotate-secret | A new secret (returned once) |
POST /webhooks/{id}/test | Send a webhook.test event now and return the delivery |
GET /webhooks/{id}/deliveries | Delivery attempts, newest first |
POST /webhooks/deliveries/{deliveryId}/redeliver | Send a delivery again |
Objects
Endpoint
{
"id": "6720b4c3d2e1f0a9b8c7d6e5",
"url": "https://example.com/hooks/cognipeer",
"description": "ops pager",
"events": ["action.required", "agent_run.*"],
"projectId": "6a1b2c3d4e5f60718293a4b5",
"status": "active",
"disabledReason": null,
"failingSince": null,
"createdBy": "6a1b2c3d4e5f60718293a4b6",
"createdAt": "2026-10-11T09:00:00.000Z",
"updatedAt": "2026-10-11T09:00:00.000Z"
}| Field | |
|---|---|
url | An http(s) URL that resolves to a public address. |
events | Exact types and/or prefix.* patterns; * for everything. |
projectId | The project the endpoint is limited to; null = every project of the tenant. |
status | active or disabled. |
disabledReason | manual, redirect, blocked_address or sustained_failure; null while active. |
failingSince | ISO timestamp the endpoint started failing continuously; cleared by any 2xx. |
The secret is never part of this object — it appears only in the responses to create and rotate-secret, as secret (whsec_…).
Delivery
{
"id": "6721c5d4e3f2a1b0c9d8e7f6",
"endpointId": "6720b4c3d2e1f0a9b8c7d6e5",
"eventId": "evt_0123456789abcdef01234567",
"eventType": "session.idled",
"status": "delivered",
"attempts": 1,
"nextAttemptAt": null,
"lastStatusCode": 200,
"lastError": null,
"lastDurationMs": 84,
"createdAt": "2026-10-11T09:14:03.200Z",
"deliveredAt": "2026-10-11T09:14:03.290Z",
"payload": {
"id": "evt_0123456789abcdef01234567",
"type": "session.idled",
"createdAt": "2026-10-11T09:14:03.120Z",
"projectId": "6a1b2c3d4e5f60718293a4b5",
"data": { "conversationId": "6f1c0a2b9d3e4f5a6b7c8d9e", "agentKey": "support-bot", "stopReason": "end_turn" }
}
}status is pending (owed — a first attempt or a scheduled retry; nextAttemptAt says when), delivered (a 2xx), or failed (it will not be tried again). lastError is Endpoint answered HTTP 500, Timed out after 10000 ms, Gave up after 8 attempts, and so on.
Event catalogue
GET /api/client/v1/webhooks/events{ "events": [ { "type": "session.idled", "group": "session" }, { "type": "action.required", "group": "action" }, "…" ] }Groups: session, action, budget, feedback, agent_run, schedule. The types are session.idled, session.work_done, action.required, budget.reached, feedback.recorded, agent_run.succeeded, agent_run.failed, agent_run.canceled, agent_run.requires_action, schedule.fired, schedule.failed. (webhook.test is only ever sent by the test route.) Each event's data is documented in the guide.
Create
POST /api/client/v1/webhooks
Content-Type: application/json| Body field | Type | |
|---|---|---|
url | string, required | At most 2048 characters; http or https; must resolve to a public address (checked now, and again on every delivery). |
events | string[], required | Non-empty. Each a known type, a prefix.* that matches at least one known type, or *. Duplicates are removed. |
description | string | At most 500 characters. |
projectId | string | null | Omitted = the token's project — the only value a client token may use. (The dashboard also takes null or "*" — every project — for tenant owners/admins, or another project's id.) |
curl -X POST "$CONSOLE_URL/api/client/v1/webhooks" \
-H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/cognipeer", "events": ["action.required", "agent_run.*"], "description": "ops pager" }'const hook = await client.webhooks.create({
url: 'https://example.com/hooks/cognipeer',
events: ['action.required', 'agent_run.*'],
description: 'ops pager',
});
console.log(hook.id, hook.secret); // the secret is shown only now201: { "endpoint": { …endpoint, "secret": "whsec_…" } }. Errors: 400 (url is required, url is not a valid URL, url must use http or https, url must resolve to a public address, events must be a non-empty array of event types, Unknown event type "x", description must be at most 500 characters), 403 forbidden, 409 limit_reached.
List, read, update, delete
GET /api/client/v1/webhooks → { "endpoints": [ … ] }
GET /api/client/v1/webhooks/{id} → { "endpoint": { … } }
PATCH /api/client/v1/webhooks/{id} → { "endpoint": { … } }
DELETE /api/client/v1/webhooks/{id} → { "deleted": true }PATCH accepts any of url, description (an empty string clears it), events and status ("active" or "disabled"); omitted fields are untouched. Switching to active re-checks the address and clears disabledReason and failingSince; switching to disabled is recorded as manual and marks the endpoint's still-queued deliveries failed. DELETE also deletes the delivery history.
# Re-enable after fixing the receiver
curl -X PATCH "$CONSOLE_URL/api/client/v1/webhooks/$WEBHOOK_ID" \
-H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
-d '{ "status": "active" }'SDK: client.webhooks.list() / get(id) / update(id, body) / delete(id). Errors: 400 invalid_request, 403 forbidden, 404 not_found.
Rotate the secret
POST /api/client/v1/webhooks/{id}/rotate-secret200: { "endpoint": { …endpoint, "secret": "whsec_…" } }. The old secret stops working at once; the new one is shown only here. SDK: client.webhooks.rotateSecret(id).
Send a test event
POST /api/client/v1/webhooks/{id}/testSends one webhook.test event now, waits for the receiver and returns the delivery with what it answered. One attempt, no retries; the answer does not count towards the endpoint's health; it works on a disabled endpoint too.
200: { "delivery": { …, "status": "delivered" | "failed", "lastStatusCode": 200, "lastError": null } } — a failing receiver is a failed delivery in a 200 answer, not an error. SDK: client.webhooks.test(id).
Deliveries
GET /api/client/v1/webhooks/{id}/deliveries?status=failed&limit=50&skip=0| Query | |
|---|---|
status | pending, delivered or failed. |
limit | Default 50, at most 200. |
skip | Offset, for paging. |
200: { "deliveries": [ … ], "hasMore": true }, newest first. SDK: client.webhooks.deliveries(id, { status, limit, skip }).
Redeliver
POST /api/client/v1/webhooks/deliveries/{deliveryId}/redeliverPuts the delivery back in the queue — the same event id, to the endpoint's current URL and secret (for example after fixing the receiver). 202: { "delivery": { …, "status": "pending" } }; it is sent asynchronously. Errors: 404 not_found, 409 endpoint_disabled (enable the endpoint first), 409 delivery_pending (it is already waiting to be sent). SDK: client.webhooks.redeliver(deliveryId).
Error codes
| Status | Code | |
|---|---|---|
400 | invalid_request | A field was rejected; the message says which. |
403 | forbidden | The caller may not manage webhooks (or a webhook for every project / another project). |
404 | not_found | No such endpoint or delivery within the caller's reach. |
409 | limit_reached | The tenant already has 50 endpoints. |
409 | endpoint_disabled | Redeliver on a disabled endpoint. |
409 | delivery_pending | Redeliver on a delivery that is still waiting. |
500 | internal_error | The test delivery could not be recorded. |
Consoles with the Authorization engine
On a Console that includes the Authorization (IAM) engine, webhooks is an administration service: webhooks.read allows GET calls (with the endpoint URL's userinfo and query string removed) and every call that changes something needs webhooks.admin; a denial is 403 and 503 authorization_unavailable means the engine could not decide (retry; it is never an allow).
SDK mapping
| Endpoint | @cognipeer/console-sdk (3.1.0+) |
|---|---|
GET /webhooks/events | client.webhooks.eventTypes() |
POST /webhooks | client.webhooks.create() |
GET /webhooks, GET /webhooks/{id} | client.webhooks.list(), client.webhooks.get() |
PATCH, DELETE /webhooks/{id} | client.webhooks.update(), client.webhooks.delete() |
POST …/rotate-secret, …/test | client.webhooks.rotateSecret(), client.webhooks.test() |
GET …/deliveries, POST …/redeliver | client.webhooks.deliveries(), client.webhooks.redeliver() |
| Signature check | verifyWebhook() from @cognipeer/console-sdk/webhooks |

