A notification contact is somewhere All's Well sends alerts. A monitor with contacts attached alerts only those; a monitor with none alerts every contact on the team. Every team starts with one contact, its owner's email address. To pick specific contacts, create them and pass their IDs in a monitor's `notification_contact_ids`.

## Config for each type
`config` holds the keys the contact's type needs. Other keys are dropped. A Viewer's token sees webhook URLs as their scheme and host followed by `/********`, and a Telegram `bot_token` as `********`.

| Type | `config` | Example |
|---|---|---|
| `email` | `email`: the address to alert | `{"email": "ops@example.com"}` |
| `slack` | `webhook_url`: an incoming webhook URL | `{"webhook_url": "https://hooks.slack.com/services/T0000000/B0000000/example"}` |
| `discord` | `webhook_url`: a channel webhook URL | `{"webhook_url": "https://discord.com/api/webhooks/1234567890/example"}` |
| `webhook` | `url`: receives a JSON POST for each alert | `{"url": "https://hooks.example.com/alls-well"}` |
| `telegram` | `bot_token` and `chat_id` (a numeric ID or an @channel) | `{"bot_token": "123456789:example-bot-token", "chat_id": "-1001234567890"}` |

A `webhook` contact receives a POST like this for each alert. `event` is the subject type and what happened: `monitor.down`, `monitor.recovery`, and the same for servers and sites.

```json
{"event": "monitor.down", "type": "monitor", "id": 48213, "name": "Storefront", "target": "https://shop.example.com", "timestamp": "2026-10-07T09:10:00+00:00"}
```

A test alert, sent from the app's dashboard or Alert contacts page, is about no subject: `event` and `type` are `test`, and `id` and `target` are null. A receiver that switches on `type` can ignore it.

```json
{"event": "test", "type": "test", "id": null, "name": "Test alert", "target": null, "timestamp": "2026-10-12T16:00:00+00:00"}
```

## The contact object
| Field | Type | Description |
|---|---|---|
| `id` | integer | The contact's ID. |
| `name` | string | The contact's name. |
| `type` | string | What to check. One of `email`, `slack`, `discord`, `telegram`, `webhook`. |
| `config` | ContactConfig | The keys the contact's type needs. |
| `notify_on_down` | boolean | Whether to alert when a monitor goes down. |
| `notify_on_recovery` | boolean | Whether to alert when a monitor recovers. |

## List notification contacts
`GET /api/v1/notification-contacts`

Lists the team's alert contacts in name order. Not paginated.

| | |
|---|---|
| Authentication | Bearer token. Any member of the team |
| Rate limit | 60 requests per minute per user, shared by all of that user's tokens |

### Example request

```bash
curl https://alls-well.io/api/v1/notification-contacts \
  -H "Authorization: Bearer $ALLS_WELL_TOKEN" \
  -H "Accept: application/json"
```

### Responses

| Status | When | Body |
|---|---|---|
| `200` | The team's contacts. | [Contacts](#contact-object), in `data` |
| `401` | There is no token, or it is invalid or revoked. | [Problem](https://alls-well.io/docs/api/errors) |
| `403` | The token is valid but may not do this: a Viewer tried to change something, the token isn't pinned to a team, or its creator has left the team. | [Problem](https://alls-well.io/docs/api/errors) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://alls-well.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://alls-well.io/docs/api/errors) |

#### 200

```json
{
  "data": [
    {
      "id": 9312,
      "name": "Ops email",
      "type": "email",
      "config": {
        "email": "ops@example.com"
      },
      "notify_on_down": true,
      "notify_on_recovery": true
    },
    {
      "id": 9313,
      "name": "Ops Slack",
      "type": "slack",
      "config": {
        "webhook_url": "https://hooks.slack.com/services/T0000000/B0000000/example"
      },
      "notify_on_down": true,
      "notify_on_recovery": false
    }
  ]
}
```

#### 401

```json
{"type": "about:blank", "title": "Unauthorized", "status": 401}
```

#### 403

```json
{"type": "about:blank", "title": "Forbidden", "status": 403}
```

#### 429

```json
{"type": "about:blank", "title": "Too Many Requests", "status": 429}
```

#### 500

```json
{"type": "about:blank", "title": "Internal Server Error", "status": 500}
```

## Create a notification contact
`POST /api/v1/notification-contacts`

Creates an alert contact. `config` holds the keys the contact's type needs; other keys are dropped. Attach the contact to monitors with `notification_contact_ids`, or send `attach_to_existing` to add it to everything that already has contacts of its own.

| | |
|---|---|
| Authentication | Bearer token. Owners and Admins; Viewers get 403 |
| Rate limit | 60 requests per minute per user, shared by all of that user's tokens |

### Request body

| Field | Type | Required | Rules |
|---|---|---|---|
| `name` | string | Yes | The contact's name. Up to 255 characters. |
| `type` | string | Yes | What to check. One of `email`, `slack`, `discord`, `telegram`, `webhook`. |
| `config` | ContactConfig | Yes | The keys the contact's type needs. |
| `notify_on_down` | boolean | No | Alert when a monitor goes down. Defaults to true. |
| `notify_on_recovery` | boolean | No | Alert when a monitor recovers. Defaults to true. |
| `attach_to_existing` | boolean | No | Also add the contact to every monitor, server and site that already has contacts of its own. Those with none are left alone, since they alert every contact on the team as it is. Defaults to false. |

### Example request

```bash
curl -X POST https://alls-well.io/api/v1/notification-contacts \
  -H "Authorization: Bearer $ALLS_WELL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Ops email", "type": "email", "config": {"email": "ops@example.com"}}'
```

### Responses

| Status | When | Body |
|---|---|---|
| `201` | The contact was created. | The [contact](#contact-object), in `data` |
| `401` | There is no token, or it is invalid or revoked. | [Problem](https://alls-well.io/docs/api/errors) |
| `403` | The token is valid but may not do this: a Viewer tried to change something, the token isn't pinned to a team, or its creator has left the team. | [Problem](https://alls-well.io/docs/api/errors) |
| `422` | The request body or query is invalid. `errors` lists the messages for each field. | [Validation problem](https://alls-well.io/docs/api/problems/validation-failed) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://alls-well.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://alls-well.io/docs/api/errors) |

#### 201

```json
{
  "data": {
    "id": 9312,
    "name": "Ops email",
    "type": "email",
    "config": {
      "email": "ops@example.com"
    },
    "notify_on_down": true,
    "notify_on_recovery": true
  }
}
```

#### 401

```json
{"type": "about:blank", "title": "Unauthorized", "status": 401}
```

#### 403

```json
{"type": "about:blank", "title": "Forbidden", "status": 403}
```

#### 422

```json
{
  "type": "https://alls-well.io/docs/api/problems/validation-failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "The url field is required unless type is in heartbeat.",
  "errors": {
    "url": ["The url field is required unless type is in heartbeat."]
  }
}
```

#### 429

```json
{"type": "about:blank", "title": "Too Many Requests", "status": 429}
```

#### 500

```json
{"type": "about:blank", "title": "Internal Server Error", "status": 500}
```

## Get a notification contact
`GET /api/v1/notification-contacts/{notificationContact}`

Returns one of the team's alert contacts. A contact that belongs to another team is a 404.

| | |
|---|---|
| Authentication | Bearer token. Any member of the team |
| Rate limit | 60 requests per minute per user, shared by all of that user's tokens |

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `notificationContact` | path | integer | Yes | The notification contact's ID. |

### Example request

```bash
curl https://alls-well.io/api/v1/notification-contacts/9312 \
  -H "Authorization: Bearer $ALLS_WELL_TOKEN" \
  -H "Accept: application/json"
```

### Responses

| Status | When | Body |
|---|---|---|
| `200` | The contact. | The [contact](#contact-object), in `data` |
| `401` | There is no token, or it is invalid or revoked. | [Problem](https://alls-well.io/docs/api/errors) |
| `403` | The token is valid but may not do this: a Viewer tried to change something, the token isn't pinned to a team, or its creator has left the team. | [Problem](https://alls-well.io/docs/api/errors) |
| `404` | Nothing with that ID belongs to the team, or the path doesn't exist. | [Problem](https://alls-well.io/docs/api/errors) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://alls-well.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://alls-well.io/docs/api/errors) |

#### 200

```json
{
  "data": {
    "id": 9312,
    "name": "Ops email",
    "type": "email",
    "config": {
      "email": "ops@example.com"
    },
    "notify_on_down": true,
    "notify_on_recovery": true
  }
}
```

#### 401

```json
{"type": "about:blank", "title": "Unauthorized", "status": 401}
```

#### 403

```json
{"type": "about:blank", "title": "Forbidden", "status": 403}
```

#### 404

```json
{"type": "about:blank", "title": "Not Found", "status": 404}
```

#### 429

```json
{"type": "about:blank", "title": "Too Many Requests", "status": 429}
```

#### 500

```json
{"type": "about:blank", "title": "Internal Server Error", "status": 500}
```

## Update a notification contact
`PATCH /api/v1/notification-contacts/{notificationContact}`

`PUT /api/v1/notification-contacts/{notificationContact}` is the same partial update. It is deprecated and stops working on 7 April 2027; its responses carry `Deprecation`, `Sunset` and `Link` headers. Use PATCH.

Changes only the fields sent. The type can't change. A `config` that is sent replaces the stored one and must hold every key the contact's type needs.

| | |
|---|---|
| Authentication | Bearer token. Owners and Admins; Viewers get 403 |
| Rate limit | 60 requests per minute per user, shared by all of that user's tokens |

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `notificationContact` | path | integer | Yes | The notification contact's ID. |

### Request body

| Field | Type | Required | Rules |
|---|---|---|---|
| `name` | string | No | The contact's name. Up to 255 characters. |
| `config` | ContactConfig | No | The keys the contact's type needs. |
| `notify_on_down` | boolean | No | Alert when a monitor goes down. |
| `notify_on_recovery` | boolean | No | Alert when a monitor recovers. |

### Example request

```bash
curl -X PATCH https://alls-well.io/api/v1/notification-contacts/9312 \
  -H "Authorization: Bearer $ALLS_WELL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"config": {"email": "alerts@example.com"}}'
```

### Responses

| Status | When | Body |
|---|---|---|
| `200` | The updated contact. | The [contact](#contact-object), in `data` |
| `401` | There is no token, or it is invalid or revoked. | [Problem](https://alls-well.io/docs/api/errors) |
| `403` | The token is valid but may not do this: a Viewer tried to change something, the token isn't pinned to a team, or its creator has left the team. | [Problem](https://alls-well.io/docs/api/errors) |
| `404` | Nothing with that ID belongs to the team, or the path doesn't exist. | [Problem](https://alls-well.io/docs/api/errors) |
| `422` | The request body or query is invalid. `errors` lists the messages for each field. | [Validation problem](https://alls-well.io/docs/api/problems/validation-failed) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://alls-well.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://alls-well.io/docs/api/errors) |

#### 200

```json
{
  "data": {
    "id": 9312,
    "name": "Ops email",
    "type": "email",
    "config": {
      "email": "alerts@example.com"
    },
    "notify_on_down": true,
    "notify_on_recovery": true
  }
}
```

#### 401

```json
{"type": "about:blank", "title": "Unauthorized", "status": 401}
```

#### 403

```json
{"type": "about:blank", "title": "Forbidden", "status": 403}
```

#### 404

```json
{"type": "about:blank", "title": "Not Found", "status": 404}
```

#### 422

```json
{
  "type": "https://alls-well.io/docs/api/problems/validation-failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "The url field is required unless type is in heartbeat.",
  "errors": {
    "url": ["The url field is required unless type is in heartbeat."]
  }
}
```

#### 429

```json
{"type": "about:blank", "title": "Too Many Requests", "status": 429}
```

#### 500

```json
{"type": "about:blank", "title": "Internal Server Error", "status": 500}
```

## Delete a notification contact
`DELETE /api/v1/notification-contacts/{notificationContact}`

Deletes the contact and detaches it from every monitor. Monitors left with no contacts alert every contact on the team.

| | |
|---|---|
| Authentication | Bearer token. Owners and Admins; Viewers get 403 |
| Rate limit | 60 requests per minute per user, shared by all of that user's tokens |

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `notificationContact` | path | integer | Yes | The notification contact's ID. |

### Example request

```bash
curl -X DELETE https://alls-well.io/api/v1/notification-contacts/9312 \
  -H "Authorization: Bearer $ALLS_WELL_TOKEN" \
  -H "Accept: application/json"
```

### Responses

| Status | When | Body |
|---|---|---|
| `204` | The contact was deleted. There is no body. | None |
| `401` | There is no token, or it is invalid or revoked. | [Problem](https://alls-well.io/docs/api/errors) |
| `403` | The token is valid but may not do this: a Viewer tried to change something, the token isn't pinned to a team, or its creator has left the team. | [Problem](https://alls-well.io/docs/api/errors) |
| `404` | Nothing with that ID belongs to the team, or the path doesn't exist. | [Problem](https://alls-well.io/docs/api/errors) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://alls-well.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://alls-well.io/docs/api/errors) |

#### 204

No body.

#### 401

```json
{"type": "about:blank", "title": "Unauthorized", "status": 401}
```

#### 403

```json
{"type": "about:blank", "title": "Forbidden", "status": 403}
```

#### 404

```json
{"type": "about:blank", "title": "Not Found", "status": 404}
```

#### 429

```json
{"type": "about:blank", "title": "Too Many Requests", "status": 429}
```

#### 500

```json
{"type": "about:blank", "title": "Internal Server Error", "status": 500}
```
