# Webhook endpoints

> Register HTTPS endpoints for event notifications, rotate signing secrets, send test pings and inspect delivery history.

Source: https://dev.ody.co/api/webhook-endpoints

A **webhook endpoint** is an HTTPS URL Ody POSTs signed events to. This page covers managing endpoints. For what gets delivered and how to verify it, see [Webhooks](https://dev.ody.co/webhooks/overview), [Event types](https://dev.ody.co/webhooks/events) and [Verifying signatures](https://dev.ody.co/webhooks/signatures).

Endpoints are managed only through this API. There's no webhook screen in the app and no MCP tool for it yet.

## The webhook endpoint object

- `id` *uuid*
- `url` *string* — HTTPS only.
- `events` *array of strings* — Subscribed [event types](https://dev.ody.co/webhooks/events), or `["*"]` for every current and future type.
- `label` *string | null* — Your own name for it.
- `status` *"enabled" | "disabled"*
- `apiVersion` *string | null* — The payload version pinned when the endpoint was created.
- `createdAt` *timestamp*
- `secret` *string* — `whsec_…` signing secret. **Returned only by create and rotate.** Store it right away.

A workspace can have up to **50** endpoints.

**`GET /v1/api/webhook-events`**

### List event types

Returns the catalog of event types you can subscribe to.

**Scope:** `webhooks:manage`

**Returns** `{ events: string[] }`.

cURL:

```bash
curl https://api.ody.co/v1/api/webhook-events \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const types = await ody.webhooks.eventTypes();
```
Python:

```python
types = client.webhooks.event_types()
```

**`GET /v1/api/webhooks`**

### List endpoints

Secrets are never included.

**Scope:** `webhooks:manage`

**Returns** `{ webhooks: WebhookEndpoint[] }`.

cURL:

```bash
curl https://api.ody.co/v1/api/webhooks \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const endpoints = await ody.webhooks.list();
```
Python:

```python
endpoints = client.webhooks.list()
```

**`POST /v1/api/webhooks`**

### Create an endpoint

**Scope:** `webhooks:manage`

**Body**

- `url` *string* **required** — Public HTTPS URL.
- `events` *array of strings* **required** — Event types, or `["*"]`.
- `label` *string*

**Returns** `201` with `{ webhook }`, **including `secret`**. Returns `400` for a non-HTTPS URL, an unknown event type, or when you'd exceed 50 endpoints.

cURL:

```bash
curl https://api.ody.co/v1/api/webhooks \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/ody/webhooks",
    "events": ["message.received", "call.completed", "call.summary.completed"],
    "label": "Production CRM sync"
  }'
```
TypeScript:

```ts
const endpoint = await ody.webhooks.create({
  url: "https://example.com/ody/webhooks",
  events: ["message.received", "call.completed", "call.summary.completed"],
  label: "Production CRM sync",
});
console.log(endpoint.secret); // whsec_… store it now
```
Python:

```python
endpoint = client.webhooks.create(
    url="https://example.com/ody/webhooks",
    events=["message.received", "call.completed", "call.summary.completed"],
    label="Production CRM sync",
)
```

Response:

```json
{
  "webhook": {
    "id": "3a8f…",
    "url": "https://example.com/ody/webhooks",
    "events": ["message.received", "call.completed", "call.summary.completed"],
    "label": "Production CRM sync",
    "status": "enabled",
    "apiVersion": "2026-06-01",
    "createdAt": "2026-10-14T18:20:00.000Z",
    "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
  }
}
```

**`GET /v1/api/webhooks/:id`**

### Retrieve an endpoint

**Scope:** `webhooks:manage`

**Returns** `{ webhook }` (without the secret).

cURL:

```bash
curl https://api.ody.co/v1/api/webhooks/3a8f… \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const endpoint = await ody.webhooks.get("3a8f…");
```
Python:

```python
endpoint = client.webhooks.get("3a8f…")
```

**`PATCH /v1/api/webhooks/:id`**

### Update an endpoint

Change the URL, subscriptions or label, or pause deliveries with `status: "disabled"`.

**Scope:** `webhooks:manage`

**Body:** any of `url`, `events`, `label` (`null` clears it), `status`.

**Returns** `{ webhook }`.

cURL:

```bash
curl -X PATCH https://api.ody.co/v1/api/webhooks/3a8f… \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"disabled"}'
```
TypeScript:

```ts
await ody.webhooks.update("3a8f…", { status: "disabled" });
```
Python:

```python
client.webhooks.update("3a8f…", status="disabled")
```

**`DELETE /v1/api/webhooks/:id`**

### Delete an endpoint

**Scope:** `webhooks:manage`

**Returns** `{ "deleted": true }`.

cURL:

```bash
curl -X DELETE https://api.ody.co/v1/api/webhooks/3a8f… \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
await ody.webhooks.delete("3a8f…");
```
Python:

```python
client.webhooks.delete("3a8f…")
```

**`POST /v1/api/webhooks/:id/rotate`**

### Rotate the signing secret

Issues a new `whsec_` secret and returns it **once**. Signatures made with the old secret stop validating **immediately**. Deploy the new secret to your verifier first, or accept both secrets briefly (see [Verifying signatures](https://dev.ody.co/webhooks/signatures#rotating-secrets)).

**Scope:** `webhooks:manage`

**Returns** `{ webhook }`, including the new `secret`.

cURL:

```bash
curl -X POST https://api.ody.co/v1/api/webhooks/3a8f…/rotate \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const { secret } = await ody.webhooks.rotate("3a8f…");
```
Python:

```python
secret = client.webhooks.rotate("3a8f…")["secret"]
```

**`POST /v1/api/webhooks/:id/test`**

### Send a test delivery

POSTs a `ping` event to the endpoint, signed exactly like a real delivery, and returns the result straight away. This is the fastest way to check your signature verification.

**Scope:** `webhooks:manage`

**Returns** `{ delivered: boolean, attempt: { at, statusCode, error?, durationMs } }`. `delivered` is `true` for any `2xx` response.

cURL:

```bash
curl -X POST https://api.ody.co/v1/api/webhooks/3a8f…/test \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const { delivered, attempt } = await ody.webhooks.test("3a8f…");
```
Python:

```python
res = client.webhooks.test("3a8f…")
```

Response:

```json
{
  "delivered": true,
  "attempt": { "at": "2026-10-14T18:21:03.114Z", "statusCode": 200, "durationMs": 143 }
}
```

**`GET /v1/api/webhooks/:id/deliveries`**

### List deliveries

Recent deliveries, newest first, with every attempt's status code or error. This is your debugging view.

**Scope:** `webhooks:manage`

**Query parameters**

- `limit` *integer* — 1–200, default 50.

**Returns** `{ deliveries: [{ id, webhookId, eventId, status, attempts, createdAt }] }`. `status` is `pending`, `success` or `failed`.

cURL:

```bash
curl "https://api.ody.co/v1/api/webhooks/3a8f…/deliveries?limit=20" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const deliveries = await ody.webhooks.deliveries("3a8f…", { limit: 20 });
```
Python:

```python
deliveries = client.webhooks.deliveries("3a8f…", limit=20)
```
