A notification contact is somewhere AgileMonitor sends alerts. A monitor alerts only the contacts attached to it: create the contact, then pass its ID 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/agilemonitor"}` |
| `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"}
```

## 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://monitor.agilepixel.io/api/v1/notification-contacts \
  -H "Authorization: Bearer $AGILEMONITOR_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://monitor.agilepixel.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://monitor.agilepixel.io/docs/api/errors) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://monitor.agilepixel.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://monitor.agilepixel.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`.

| | |
|---|---|
| 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. |

### Example request

```bash
curl -X POST https://monitor.agilepixel.io/api/v1/notification-contacts \
  -H "Authorization: Bearer $AGILEMONITOR_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://monitor.agilepixel.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://monitor.agilepixel.io/docs/api/errors) |
| `422` | The request body or query is invalid. `errors` lists the messages for each field. | [Validation problem](https://monitor.agilepixel.io/docs/api/problems/validation-failed) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://monitor.agilepixel.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://monitor.agilepixel.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://monitor.agilepixel.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://monitor.agilepixel.io/api/v1/notification-contacts/9312 \
  -H "Authorization: Bearer $AGILEMONITOR_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://monitor.agilepixel.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://monitor.agilepixel.io/docs/api/errors) |
| `404` | Nothing with that ID belongs to the team, or the path doesn't exist. | [Problem](https://monitor.agilepixel.io/docs/api/errors) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://monitor.agilepixel.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://monitor.agilepixel.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://monitor.agilepixel.io/api/v1/notification-contacts/9312 \
  -H "Authorization: Bearer $AGILEMONITOR_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://monitor.agilepixel.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://monitor.agilepixel.io/docs/api/errors) |
| `404` | Nothing with that ID belongs to the team, or the path doesn't exist. | [Problem](https://monitor.agilepixel.io/docs/api/errors) |
| `422` | The request body or query is invalid. `errors` lists the messages for each field. | [Validation problem](https://monitor.agilepixel.io/docs/api/problems/validation-failed) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://monitor.agilepixel.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://monitor.agilepixel.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://monitor.agilepixel.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 send no alerts.

| | |
|---|---|
| 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://monitor.agilepixel.io/api/v1/notification-contacts/9312 \
  -H "Authorization: Bearer $AGILEMONITOR_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://monitor.agilepixel.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://monitor.agilepixel.io/docs/api/errors) |
| `404` | Nothing with that ID belongs to the team, or the path doesn't exist. | [Problem](https://monitor.agilepixel.io/docs/api/errors) |
| `429` | The rate limit is used up. Wait for `Retry-After` seconds. | [Problem](https://monitor.agilepixel.io/docs/api/errors) |
| `500` | Something went wrong on our side. Try again; if it persists, contact support. | [Problem](https://monitor.agilepixel.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}
```
