Webhook Config
The Webhook resource configures where Adjudon delivers events. The
delivery shape, signature scheme, retry schedule, and per-event
payloads are documented at Webhooks Overview,
Signature Verification, and
Webhook Events Catalog. This page is the CRUD
surface. Tested against API version v1. All endpoints are gated by
the webhooks feature; mutating endpoints additionally require the
admin or owner role.
The Webhook object
| Field | Type | Required | Description |
|---|---|---|---|
_id | string | yes | MongoDB ObjectId |
organizationId | string | yes | Owning org; chains never mix across tenants |
workspaceId | string | no | Workspace scope; null makes the webhook org-wide |
url | string | yes | Delivery URL; must be https://; SSRF-blocked addresses rejected |
events | string[] | yes | One or more event names; * for wildcard. See Events Catalog |
type | enum | no | custom (default), slack, teams |
description | string | no | Operator-readable label, max 100 characters |
isActive | boolean | no | Default true; an inactive webhook does not receive deliveries |
deliveryCount | number | no | Total delivery attempts (read-only) |
deliverySuccessCount | number | no | Successful 2xx deliveries (read-only) |
lastTriggered | string (ISO 8601) | no | Most recent delivery timestamp (read-only) |
createdAt / updatedAt | string (ISO 8601) | no | Standard timestamps |
The secret field exists on the schema but is never returned by
any endpoint after creation. The raw value is shown once on the
POST response — store it in your secret manager at that
moment or rotate (delete-and-recreate) to mint a new one.
Endpoints
GET /api/v1/webhooks
POST /api/v1/webhooks
PATCH /api/v1/webhooks/:id
DELETE /api/v1/webhooks/:id
POST /api/v1/webhooks/:id/test
GET is available to any authenticated org member; the four
mutating endpoints require the admin or owner role.
List webhooks
GET /api/v1/webhooks
| Query parameter | Default | Description |
|---|---|---|
limit | (none) | Cap server-side to 200; 1-200 range |
search | — | Case-insensitive substring on url and description (max 100 chars) |
Sort order: most-recently-created first. The x-workspace-id header
scopes the response to one workspace; absent it returns every
org-wide and workspace-tagged webhook.
curl https://api.adjudon.com/api/v1/webhooks \
-H "Authorization: Bearer $ADJUDON_API_KEY"
Response (200 OK):
{
"success": true,
"data": [/* Webhook[] (without `secret`) */],
"count": 7
}
Errors: 401, 403 (UPGRADE_REQUIRED for plan-gate), 500
INTERNAL_ERROR.
Create a webhook
POST /api/v1/webhooks
| Body field | Required | Description |
|---|---|---|
url | yes | https:// URL; http://, localhost, RFC 1918, ::1, and 169.254.169.254 are all rejected |
events | yes | Non-empty array; values must match the subscription enum or * |
description | no | ≤100 characters |
type | no | custom, slack, or teams; defaults to custom |
isActive | no | Defaults to true |
curl -X POST https://api.adjudon.com/api/v1/webhooks \
-H "Authorization: Bearer $ADJUDON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/adjudon",
"events": ["trace.created", "alert.triggered"],
"description": "Production alerting",
"type": "custom"
}'
import os, requests
r = requests.post(
"https://api.adjudon.com/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['ADJUDON_API_KEY']}"},
json={
"url": "https://hooks.example.com/adjudon",
"events": ["trace.created", "alert.triggered"],
},
)
secret = r.json()["data"]["signingSecret"] # save this — shown once
Response (201 Created):
{
"success": true,
"data": {
"_id": "65b1f2c4d3e0a1b2c3d4e5f6",
"organizationId":"65b1...",
"url": "https://hooks.example.com/adjudon",
"events": ["trace.created", "alert.triggered"],
"isActive": true,
"type": "custom",
"createdAt": "2026-05-06T10:14:22.317Z",
"signingSecret": "1f8a2c7b9d4e0a1b2c3d4e5f6789...."
},
"message": "Webhook created. Store the signing secret — it will not be shown again."
}
The signingSecret is the raw HMAC secret. Store it now. It
will not appear in any subsequent GET response. If lost, delete
the webhook and create a new one.
Errors: 400 VALIDATION_ERROR (missing url or empty events),
400 DUPLICATE (same URL + events combination), 400 SSRF_BLOCKED
(via the URL validator at create time), 401, 403, 500.
Update a webhook
PATCH /api/v1/webhooks/:id
Partial update. Any of url, events, description, type,
isActive may be supplied; omitted fields stay unchanged. The
secret is not updateable on this path — rotate via
delete-and-recreate.
curl -X PATCH https://api.adjudon.com/api/v1/webhooks/65b1f2c4 \
-H "Authorization: Bearer $ADJUDON_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "isActive": false }'
Errors: 400 VALIDATION_ERROR, 401 UNAUTHORIZED (different
org), 404 NOT_FOUND, 500.
Test a webhook
POST /api/v1/webhooks/:id/test
Fires a synthetic event to the configured URL. The body shape is
the same as a real delivery, with the event name webhook.test:
{
"event": "webhook.test",
"timestamp": "2026-05-06T10:14:22.317Z",
"data": { "message": "This is a test webhook from ADJUDON" }
}
curl -X POST https://api.adjudon.com/api/v1/webhooks/65b1f2c4/test \
-H "Authorization: Bearer $ADJUDON_API_KEY"
If the configured endpoint is unreachable or returns non-2xx, the
test endpoint returns HTTP 200 with code: WEBHOOK_TEST_FAILED
and the response detail. The non-failure status code is intentional:
it surfaces the failure shape without triggering retry-cascade
behaviour at the caller level.
Errors: 200 WEBHOOK_TEST_FAILED (delivery failure surfaced
softly), 400 SSRF_BLOCKED (URL has since become unreachable on
SSRF grounds), 401 UNAUTHORIZED, 404 NOT_FOUND, 500.
Delete a webhook
DELETE /api/v1/webhooks/:id
Hard-deletes the webhook record and stops further deliveries. The
AuditLog entry on webhook.delete remains by Cardinal Rule 5
(operations chain is append-only).
curl -X DELETE https://api.adjudon.com/api/v1/webhooks/65b1f2c4 \
-H "Authorization: Bearer $ADJUDON_API_KEY"
Errors: 401, 404 NOT_FOUND, 500.
Common gotchas
- Secret shown once. The
signingSecretfield appears only on thePOSTcreate response. There is no recovery endpoint; lose it and you rotate via delete-and-recreate. https://only. Plain HTTP is rejected at validation. Private addresses (localhost,127.x,10.x,192.168.x,172.16-31.x,0.0.0.0,::1, AWS metadata169.254.169.254) are blocked at create AND at delivery time (DNS-rebinding guard).- DUPLICATE on the create. A
400 DUPLICATEmeans the same(url, events[])combination already exists. UsePATCHto re-enable anisActive: falsewebhook instead of recreating. - Test endpoint returns
200on delivery failure. Branch on the responsecode(WEBHOOK_TEST_FAILED), not on the HTTP status, when distinguishing test success from a downstream delivery failure.
See also
- Webhooks Overview — delivery shape, retry schedule, SSRF guarantees
- Signature Verification — verify the
x-adjudon-signatureheader - Webhook Events Catalog — per-event payload schemas
- Error Codes — the full error taxonomy
- Authentication — admin/owner role requirements + secret-storage best practices