# Subscribe to an event webhook

`POST /webhook.subscribe`

## Description

Create or update a webhook subscription for a given event. If a subscription
already exists for the same event and URL, its filter is updated instead of
creating a duplicate. Re-subscribing the same event and URL also clears a
pause (`disabledAt` / `failStreakStartedAt`) so deliveries resume on the
next event. See [Subscription health](/docs/webhooks/webhooks#subscription-health).

**Event names are dotted:** `chat.message`, `lobby.booked`,
`magicast.created`. Colon names (`chat:message:dm`, `lobby:booked`) are
v0-only — sending them here returns `400` / `Unrecognized event`.

Roam does not probe the destination URL when you subscribe — the
subscription is created immediately and the first delivery is a real event.

Optional `filter` limits which occurrences are delivered. Which keys are
valid depends on `event` — see that event's page and the
[Event Filters](/docs/webhooks/webhooks#event-filters) table. Omit
`filter` to receive every occurrence. An empty object (`{}`) is rejected,
as is a filter that does not apply to the event.

**DMs only:**

```json
{
  "url": "https://example.com/hooks/messages",
  "event": "chat.message",
  "filter": { "chatType": "dm" }
}
```

**Required scope:** `webhook:write`

---

**OpenAPI Spec:** [webhooks-v1.json](https://developer.ro.am/webhooks-v1.json)

## Authentication

```
Authorization: Bearer YOUR_API_KEY
```

## Request Body

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `url` | string | Yes | Destination URL for webhook deliveries (max 1024 characters). HTTPS is required outside local environments. |
| `event` | "chat.message" | "chat.reaction" | "chat.link.shared" | "lobby.booked" | "magicast.created" | "meeting.started" | "meeting.ended" | "user.status.update" | "onair.event.created" | "onair.event.updated" | "onair.event.canceled" | "onair.guest.rsvp" | "onair.guest.added" | "token.revoked" | "app.uninstalled" | Yes | Event to subscribe to. |
| `filter` | WebhookSubscriptionFilter | No | Optional event-specific filter. Which keys are valid depends on `event` (see the schema). Omit to receive every occurrence; `{}` and `null` are rejected rather than treated as "omitted". Example for D |
| `apiVersion` | string | No | Optional [API version](/docs/guides/api-versioning) (`YYYY-MM-DD`) to pin this subscription's payload shape to. When omitted, the subscription is frozen at your integration's default version. Unsuppor |

**WebhookSubscriptionFilter**:

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `chatType` | "dm" | "group" | No | For `chat.message`: restrict to direct messages (`dm`, 1:1 and multi-person) or group messages (`group`, including meeting channels). Same vocabulary as `data.chatType` on the delivered payload.  |
| `mention` | boolean | No | For `chat.message`: restrict to messages that @mention your app. Only `true` constrains anything, so `{"mention": false}` on its own is rejected like `{}`; alongside another key (`{"chatType": "dm", " |
| `names` | string[] | No | For `chat.reaction`: restrict to events where the changed reaction is one of these names (e.g. 'thumbs_up', 'heart'), matching the `name` field of `/reaction.add` and `/reaction.list`. |
| `hasVideo` | "true" | No | For `meeting.ended`: restrict to meetings that were video recorded, i.e. a video track exists. This keys on "was recorded", not "is ready to fetch" — the recording upload is a separate pipeline that h |
| `eventId` | string | No | For On-Air events (`onair.event.*`, `onair.guest.*`): restrict to the specified event. |
| `status` | "invited" | "going" | "maybe" | "notGoing" | No | For `onair.guest.rsvp`: restrict to the specified RSVP status. |

## Responses

### 200 - Subscription created or updated.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `id` | string | Yes | Unique identifier of the webhook subscription. |
| `event` | "chat.message" | "chat.reaction" | "chat.link.shared" | "lobby.booked" | "magicast.created" | "meeting.started" | "meeting.ended" | "user.status.update" | "onair.event.created" | "onair.event.updated" | "onair.event.canceled" | "onair.guest.rsvp" | "onair.guest.added" | "token.revoked" | "app.uninstalled" | Yes | Subscribed event name. |
| `url` | string | Yes | Destination URL for webhook deliveries. |
| `filter` | WebhookSubscriptionFilter | No | Event-specific filter applied to the subscription. |
| `dynamic` | boolean | Yes | `true` if the subscription was created via `/webhook.subscribe`. `false` if it was configured statically in the Roam Administration UI.  |
| `created` | string | No | When the subscription was created. |
| `lastSuccessAt` | string | No | Last terminal 2xx (RFC3339 UTC). Omitted until the destination has succeeded at least once. Kept when a pause is cleared.  |
| `failStreakStartedAt` | string | No | Start of the current consecutive-failure span (RFC3339 UTC). Cleared on 2xx and on [`/webhook.subscribe`](/docs/webhooks/webhook-subscribe) to the same event+URL. Omitted when healthy.  |
| `disabledAt` | string | No | When the fail streak reached 24 hours (RFC3339 UTC). While set the subscription is paused (one probe event per day). Omitted when active. See [Subscription health](/docs/webhooks/webhooks#subscription |

**WebhookSubscriptionFilter**:

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `chatType` | "dm" | "group" | No | For `chat.message`: restrict to direct messages (`dm`, 1:1 and multi-person) or group messages (`group`, including meeting channels). Same vocabulary as `data.chatType` on the delivered payload.  |
| `mention` | boolean | No | For `chat.message`: restrict to messages that @mention your app. Only `true` constrains anything, so `{"mention": false}` on its own is rejected like `{}`; alongside another key (`{"chatType": "dm", " |
| `names` | string[] | No | For `chat.reaction`: restrict to events where the changed reaction is one of these names (e.g. 'thumbs_up', 'heart'), matching the `name` field of `/reaction.add` and `/reaction.list`. |
| `hasVideo` | "true" | No | For `meeting.ended`: restrict to meetings that were video recorded, i.e. a video track exists. This keys on "was recorded", not "is ready to fetch" — the recording upload is a separate pipeline that h |
| `eventId` | string | No | For On-Air events (`onair.event.*`, `onair.guest.*`): restrict to the specified event. |
| `status` | "invited" | "going" | "maybe" | "notGoing" | No | For `onair.guest.rsvp`: restrict to the specified RSVP status. |


#### Example Response

```json
{
  "id": "19c6401f-6d02-4d8c-87c5-9fc45f02f4b5",
  "event": "chat.message",
  "url": "https://example.com/hooks/messages",
  "filter": {
    "chatType": "dm"
  },
  "dynamic": true
}
```

### 400 - Bad request. Common causes:
- `url` missing, malformed, or not HTTPS
- `url` is not a valid webhook destination — private, internal, or
  otherwise undeliverable hosts are rejected at subscribe time
- `event` missing or unrecognized
- Invalid filter for the event


### 401 - Presented invalid authentication credentials.

### 500 - An internal error occurred.

---

*Machine-readable API documentation.*
*Full documentation: https://developer.ro.am/docs/webhooks/webhook-subscribe*
