# Webhooks - X-Radius API

> Base URL: https://x-radius.com/api/v1
> Auth: Authorization: Bearer xrt_...  (a manager API token)
> Envelope: {"data": ...}; lists add {"meta":{page,page_size,total,has_next}}
> Errors: {"error":{"code","message","request_id"}} - branch on code, never on message
> Timestamps: yyyy-MM-dd HH:mm:ss, UTC
> Money: a bare JSON number in major units, with an ISO-4217 currency code beside it
> Idempotency: redeem and activate endpoints take a client-supplied request_id (UUID)
>
> This page: https://x-radius.com/docs/api/webhooks
> Every group: https://x-radius.com/llms.txt

9 endpoints in 1 resource groups. 8 carry a hand-written reference entry with examples; the remaining 1 are generated from the running router and carry method, path, authentication, permission and rate-limit bucket, but no request or response example.

### Read the delivery log

`GET /api/v1/admin/webhooks/deliveries`

One page of delivery attempts, newest first, without the payload bodies. This is where you diagnose an integration that is not receiving events.

- Authentication: manager session (JWT) or API token
- Permission: `prm_notifications_manage` (Manage notification channels & templates)
- Risk: write

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `filter[endpoint_id]` | query | integer | no | Narrow to one endpoint. Also accepted as a bare endpoint_id parameter. |
| `filter[state]` | query | enum | no | queued, sending, sent, failed or dead. An unknown value is a 400. |
| `sort` | query | string | no | queued_at, state, attempts or id. Defaults to queued_at descending. |

#### Response — 200 OK

```json
{
  "data": [
    {
      "id": 77120,
      "endpoint_id": 4,
      "event_type": "user.created",
      "state": "failed",
      "attempts": 3,
      "max_attempts": 6,
      "response_code": 502,
      "response_ms": 1204,
      "last_error": "upstream returned 502",
      "url": "https://hooks.acme.example/xradius",
      "queued_at": "2026-09-20 09:00:00",
      "sent_at": "2026-09-20 09:04:11",
      "next_attempt_at": "2026-09-20 09:20:00"
    }
  ],
  "meta": { "page": 1, "page_size": 50, "total": 4102, "has_next": true }
}
```

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_VALIDATION` | 400 | an unknown state filter, or a non-numeric endpoint_id |

#### Note

failed and dead are different. failed means it will be retried, and next_attempt_at says when; dead means the attempt budget is spent and nothing further will happen without a replay. An integration that went quiet has rows in dead, not failed.


### Replay a delivery

`POST /api/v1/admin/webhooks/deliveries/{id}/replay`

Re-queues a past delivery, which is how a dead one is recovered after the receiving side is fixed.

- Authentication: manager session (JWT) or API token
- Permission: `prm_notifications_manage` (Manage notification channels & templates)
- Risk: write
- Rate limit bucket: `t_egress`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Delivery id. |

#### Note

Your receiver will see the same event twice if the original eventually landed. Webhook receivers have to be idempotent on the event id; this endpoint is one of the reasons why.


### List webhook endpoints

`GET /api/v1/admin/webhooks/endpoints`

Every configured endpoint for the tenant, with its subscription set. Un-paginated: the whole set is page 1 and has_next is always false.

- Authentication: manager session (JWT) or API token
- Permission: `prm_notifications_manage` (Manage notification channels & templates)
- Risk: write

#### Response — 200 OK

```json
{
  "data": [
    {
      "id": 4,
      "name": "billing-sync",
      "url": "https://hooks.acme.example/xradius",
      "events": ["user.created", "user.disconnected"],
      "all_events": false,
      "enabled": true,
      "created_at": "2026-06-11 14:20:00",
      "updated_at": "2026-09-02 08:31:12"
    }
  ],
  "meta": { "page": 1, "page_size": 1, "total": 1, "has_next": false }
}
```


### Create a webhook endpoint

`POST /api/v1/admin/webhooks/endpoints`

Registers a URL and the events it should receive. Set all_events to subscribe to everything, present and future, instead of naming keys.

- Authentication: manager session (JWT) or API token
- Permission: `prm_notifications_manage` (Manage notification channels & templates)
- Risk: write
- Rate limit bucket: `t_mutate`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | body | string | yes | A label for the endpoint. |
| `url` | body | string | yes | Where deliveries are POSTed. Validated on save. |
| `events` | body | array | no | Event keys from the catalogue. Ignored when all_events is true. |
| `all_events` | body | boolean | no | Subscribe to every event, including ones added in later releases. |
| `enabled` | body | boolean | no | Defaults to true on create. |

#### Request

```json
{
  "name": "billing-sync",
  "url": "https://hooks.acme.example/xradius",
  "events": ["user.created", "user.disconnected"],
  "all_events": false,
  "enabled": true
}
```

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_VALIDATION` | 400 | an unknown event key, or a URL that does not validate |
| `ERR_CONFLICT` | 409 | the licensed endpoint ceiling is reached |

#### Note

Answers 200, not 201, despite creating a record. Do not gate your client on the status code here.


### Manage notification channels & templates

`POST /api/v1/admin/webhooks/endpoints/bulk-delete`

- Authentication: manager session (JWT) or API token
- Permission: `prm_notifications_manage` (Manage notification channels & templates)
- Risk: write
- Rate limit bucket: `t_mutate`

_This endpoint has no hand-written reference entry yet. The method, path, authentication, permission and rate limit above are generated from the running router and are accurate; there is no request or response example._


### Delete a webhook endpoint

`DELETE /api/v1/admin/webhooks/endpoints/{id}`

Removes the endpoint. Queued deliveries for it stop.

- Authentication: manager session (JWT) or API token
- Permission: `prm_notifications_manage` (Manage notification channels & templates)
- Risk: write
- Rate limit bucket: `t_mutate`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Endpoint id. |


### Update a webhook endpoint

`PATCH /api/v1/admin/webhooks/endpoints/{id}`

Replaces name, url, events and all_events outright, and patches enabled only when it is present. Send the full object, because a missing events array clears the subscription rather than leaving it alone.

- Authentication: manager session (JWT) or API token
- Permission: `prm_notifications_manage` (Manage notification channels & templates)
- Risk: write
- Rate limit bucket: `t_mutate`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Endpoint id. |

#### Note

This is a PATCH that behaves as a replace for four of its five fields. An update that sends only url wipes the events list.


### Send a test delivery

`POST /api/v1/admin/webhooks/endpoints/{id}/test`

Queues one synthetic delivery to the endpoint and returns its id so you can follow it in the log.

- Authentication: manager session (JWT) or API token
- Permission: `prm_notifications_manage` (Manage notification channels & templates)
- Risk: write
- Rate limit bucket: `t_egress`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Endpoint id. |

#### Response — 200 OK

```json
{
  "data": { "delivery_id": 77120 }
}
```

#### Note

The delivery is QUEUED, not sent. A 200 here says the job was created, not that your endpoint answered. Poll the delivery log for the outcome. It is also on the per-tenant egress budget, which is 5 a minute on a standard licence.


### List subscribable event keys

`GET /api/v1/admin/webhooks/event-catalog`

Every event an endpoint may subscribe to, with its category. The create and update endpoints validate the events array against exactly this set, so read it before writing one. Static for the life of the release, so fetch it once and cache it.

- Authentication: manager session (JWT) or API token
- Permission: `prm_notifications_manage` (Manage notification channels & templates)
- Risk: write

#### Response — 200 OK

```json
{
  "data": [
    { "key": "user.created", "category": "lifecycle" },
    { "key": "user.disconnected", "category": "lifecycle" },
    { "key": "usage.threshold", "category": "usage" }
  ],
  "meta": { "page": 1, "page_size": 3, "total": 3, "has_next": false }
}
```


