---
title: "Alert Sources API"
description: "Create, configure and test Incident Management alert sources: ingest tokens, optional HMAC or Bearer authentication, payload mapping, maintenance windows and per-source statistics."
---

An **alert source** is an inbound integration. Each webhook source has its own ingest URL (`https://uptimeify.io/api/im/ingest/<token>`) and a payload mapping that turns the vendor's JSON into an alert. The [setup guides](/api/incident-management/alert-sources) explain how to point Zabbix, Datadog, Grafana and others at that URL. This page covers the endpoints that manage the sources themselves.

## Authentication

Every endpoint needs IM access: an IM-eligible role (`admin`, `editor`, `responder`) or an organization-wide API token. Incident Management must be enabled for the organization. A customer-scoped token is rejected with `403 Forbidden` (`customerScopedTokenForbidden`).

- **Read endpoints** (list, detail, presets, payloads, stats, test-mapping) are open to every IM-eligible caller.
- **Write endpoints** (create, update, delete, token and secret rotation, disable-auth, test-alert) additionally require the role `admin` or a team-admin membership in the source's team. An API token carries the role of the user who created it, so use a token created by an organization admin.

Sources are scoped to your organization. A source of another organization answers `404 Not Found` (`imSourceNotFound`), never `403`.

## The source object

List, detail, update and disable-auth return this shape. Credentials never appear in it: the ingest token, the bearer token and the HMAC secret are only returned once, by the endpoint that mints them.

| Field | Type | Description |
|-------|------|-------------|
| `id` | number | Source ID. |
| `organizationId` | number | Owning organization. |
| `teamId` | number | Team whose escalation the source feeds. |
| `name` | string | Display name, up to 120 characters. |
| `type` | string | `zabbix`, `datadog`, `grafana`, `alertmanager`, `sentry`, `custom`, `api`, `email`, or `uptimeify_monitoring` (the built-in bridge from classic monitoring, created by the platform). |
| `authMode` | object | `{ url_token?, hmac?: { enabled }, bearer?: { enabled } }`. See [Optional request authentication](#optional-request-authentication). |
| `payloadMapping` | object | Field mapping applied to every inbound payload. |
| `autoResolve` | boolean | Whether a resolve event closes the incident automatically. Copied onto each new incident. |
| `groupingWindowMinutes` | number or null | Grouping window. `null` groups by dedup key only. |
| `maintenance` | object or null | Recurring maintenance windows, `{ windows: [...] }`. |
| `snoozeUntil` | ISO 8601 or null | Ad hoc mute until this instant. |
| `secondaryExpiresAt` | ISO 8601 or null | Until when the previous ingest token still works after a rotation. |
| `createdAt`, `updatedAt` | ISO 8601 | Timestamps. |

```json
{
  "id": 7,
  "organizationId": 1,
  "teamId": 3,
  "name": "Zabbix production",
  "type": "zabbix",
  "secondaryExpiresAt": null,
  "payloadMapping": {
    "title": "trigger_name",
    "severity": "event_severity",
    "status": "trigger_status",
    "host": "host_name",
    "dedupKey": "event_id",
    "severityMap": { "Disaster": "sev1", "High": "sev2", "Average": "sev3", "Warning": "sev4", "Information": "sev4", "Not classified": "sev4" },
    "resolveValues": ["RESOLVED", "OK"]
  },
  "autoResolve": true,
  "groupingWindowMinutes": null,
  "maintenance": null,
  "snoozeUntil": null,
  "createdAt": "2026-10-01T08:00:00.000Z",
  "updatedAt": "2026-10-01T08:00:00.000Z",
  "authMode": {}
}
```

## List sources

`GET /api/im/sources`

Returns all sources of your organization as an array of source objects, sorted by name.

```bash
curl "$BASE_URL/api/im/sources" \
  -H "Authorization: Bearer $TOKEN"
```

## Get a source

`GET /api/im/sources/:id`

Returns one source object.

## List presets

`GET /api/im/sources/presets`

Returns the built-in presets you can pass as `type` when creating a source. `docsSlug` is the setup guide under `/api/incident-management/alert-sources/`, `vendorTemplate` is a ready-made vendor config (only Zabbix ships one, a media type YAML), `samplePayload` is the test payload [test-mapping](#test-a-mapping) and [test-alert](#fire-a-test-alert) fall back to.

```json
[
  {
    "name": "zabbix",
    "docsSlug": "zabbix",
    "vendorTemplate": "zabbix_export:\n  version: '7.4'\n  ...",
    "samplePayload": { "event_id": "6633487", "event_severity": "Disaster", "trigger_name": "Zabbix agent is not available on Zabbix server", "trigger_status": "PROBLEM", "host_name": "Zabbix server" }
  },
  { "name": "datadog", "docsSlug": "datadog", "vendorTemplate": null, "samplePayload": { "alert_id": "1234567", "alert_title": "[Triggered] High CPU usage on host web-01" } }
]
```

The full list is `zabbix`, `datadog`, `grafana`, `alertmanager`, `sentry`, `custom` (sample payloads shortened above).

## Create a source

`POST /api/im/sources`

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `teamId` | number | Yes | Team in your organization. |
| `name` | string | Yes | 1 to 120 characters, trimmed. |
| `type` | string | Yes | A preset name, `api` (no ingest URL, fed through [Events Ingest](/api/incident-management/events-ingest)) or `email` (inbound mail address). |
| `payloadMapping` | object | No | Omit it to start from the preset's mapping. An explicit value, even `{}`, replaces the preset. For `email` only the nested `emailSelectors` key is kept, see the [Email guide](/api/incident-management/alert-sources/email). |
| `autoResolve` | boolean | No | Default `true`. |
| `groupingWindowMinutes` | number or null | No | Positive integer, or `null` (default). |
| `authMode` | object | No | Only `url_token` and `enabled: false` flags are accepted here. HMAC and Bearer are switched on by their own endpoints below. |

`payloadMapping` accepts two shapes. The flat form uses string selectors `title`, `severity`, `status`, `host`, `dedupKey`, plus `severityMap` (raw value to `sev1` .. `sev4`), `resolveValues` (array of strings) and `custom` (object of selectors). The attribute form is `{ "version": 2, "attributes": [...] }`, each attribute `{ key, standard?, steps?, flags? }` with steps `{ kind: "extract", type: "path" | "jsonpath" | "constant", expr }` or `{ kind: "valueMap", entries: [{ from, to }], fallback? }`. Both forms accept `defaultSeverity` (`sev1` .. `sev4`, default `sev3`), used when the payload yields no valid severity.

### Example (cURL)

```bash
curl -X POST "$BASE_URL/api/im/sources" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "teamId": 3, "name": "Zabbix production", "type": "zabbix" }'
```

### Response

`200 OK`: the source object plus `ingestToken`. **The token is shown only in this response.** Only its SHA-256 hash is stored, it cannot be read back later. Build the webhook URL as `https://uptimeify.io/api/im/ingest/<ingestToken>`. For `type: "api"` the token is `null`. For `type: "email"` the response also carries `inboundEmailAddress` (`<token>@alerts.uptimeify.io`).

```json
{
  "id": 7,
  "organizationId": 1,
  "teamId": 3,
  "name": "Zabbix production",
  "type": "zabbix",
  "secondaryExpiresAt": null,
  "payloadMapping": { "title": "trigger_name", "dedupKey": "event_id" },
  "autoResolve": true,
  "groupingWindowMinutes": null,
  "maintenance": null,
  "snoozeUntil": null,
  "createdAt": "2026-10-01T08:00:00.000Z",
  "updatedAt": "2026-10-01T08:00:00.000Z",
  "authMode": {},
  "ingestToken": "Q2hR4mVx9LkP0sTz7bNw1cYe5uJa3fGd"
}
```

## Update a source

`PATCH /api/im/sources/:id`

Send only the fields you want to change. The write bar is checked on the current team, and on the target team when you move the source.

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | 1 to 120 characters. |
| `teamId` | number | Moves the source to another team of the same organization. |
| `payloadMapping` | object | **Replaces** the whole mapping, it is not merged. `null` sets an empty mapping. |
| `authMode` | object | Merged into the stored value. `hmac.enabled: true` / `bearer.enabled: true` only work once a secret exists (see below). `bearer.token_hash` is rejected. |
| `autoResolve` | boolean | |
| `groupingWindowMinutes` | number or null | `null` clears the window. |
| `maintenance` | object or null | `{ "windows": [{ "dow": [1,2,3,4,5], "from": "22:00", "to": "23:30", "timezone": "Europe/Berlin" }] }`. `from`/`to` as 24 h `HH:MM`, `dow` ISO weekdays 1 (Monday) to 7, `timezone` an IANA zone, at most 20 windows. `null` clears it. Alerts inside a window are suppressed. |
| `snoozeUntil` | ISO 8601 or null | Mutes the source until this instant. A past value is accepted and has no effect. `null` clears it. |

The ingest token cannot be changed here, use [rotate-token](#rotate-the-ingest-token).

```bash
curl -X PATCH "$BASE_URL/api/im/sources/7" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "snoozeUntil": "2026-10-09T18:00:00Z", "groupingWindowMinutes": 15 }'
```

`200 OK`: the updated source object.

## Delete a source

`DELETE /api/im/sources/:id`

Refused with `409` while the source still has open alerts or is the primary source of an open incident (anything not `resolved` or `merged`). Resolve those first. The monitoring bridge source (`uptimeify_monitoring`) cannot be deleted.

`200 OK`: `{ "success": true }`.

The `409` (`imSourceReferencesExist`) body lists what blocks the delete (each list capped at 50):

```json
{
  "statusCode": 409,
  "statusMessage": "Source is still referenced by open alerts or incidents",
  "data": {
    "code": "imSourceReferencesExist",
    "openAlertCount": 2,
    "openIncidents": [{ "id": "42", "title": "Database connection pool exhausted" }]
  }
}
```

## Rotate the ingest token

`POST /api/im/sources/:id/rotate-token`

No request body. Mints a new ingest token. The previous token keeps working for **7 days** (until `secondaryExpiresAt`), so you can update the vendor configuration without losing alerts. The new token is **shown only in this response**. For an email source, the new inbound address is `<ingestToken>@alerts.uptimeify.io`.

```json
{
  "ingestToken": "Vn8kT1qZ4xLb7RwE0mYc2sHa9uJd6fPg",
  "secondaryExpiresAt": "2026-10-16T09:30:00.000Z"
}
```

## Optional request authentication

The token in the URL is always required. On top of it you can require a signature or a header:

- **HMAC**: the sender adds `X-Uptimeify-Timestamp` (Unix seconds, at most 5 minutes off) and `X-Uptimeify-Signature`, the hex HMAC-SHA256 of `<timestamp>.<raw body>` with the source's secret. A `sha256=` prefix is accepted.
- **Bearer**: the sender adds `Authorization: Bearer <token>`.

A request that fails either check is answered exactly like an unknown token (`404`). `authMode.url_token` is stored but has no effect.

### Set the HMAC secret

`POST /api/im/sources/:id/set-hmac-secret`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `secret` | string | No | Your own secret, at least 16 characters. Omit it to have a 256-bit secret generated. |

Enables HMAC (`authMode.hmac.enabled: true`). Calling it again replaces the secret, and the old one stops working immediately. The secret is stored encrypted and **returned only in this response**:

```json
{ "secret": "c2VjcmV0LWV4YW1wbGUtbm90LXJlYWwtMTIzNDU2Nzg5MA", "hmacEnabled": true }
```

### Rotate the bearer token

`POST /api/im/sources/:id/rotate-bearer-token`

No request body. Mints a 256-bit bearer token and enables Bearer auth. A previous bearer token stops working immediately. Only a hash is stored, the token is **returned only in this response**:

```json
{ "token": "b3V0LW9mLWJhbmQtZXhhbXBsZS10b2tlbi1ub3QtcmVhbA", "bearerEnabled": true }
```

### Disable HMAC or Bearer

`POST /api/im/sources/:id/disable-auth`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mode` | string | Yes | `hmac` or `bearer`. |

Sets `authMode.<mode>.enabled` to `false` and returns the source object. The secret stays stored, so `PATCH` with `{ "authMode": { "hmac": { "enabled": true } } }` turns it back on without a new secret. Disabling a mode that is already off succeeds.

## Test a mapping

`POST /api/im/sources/:id/test-mapping`

Runs the source's stored mapping against a payload without creating anything. Payload order: `payload` from the body, else the most recent payload the source received, else the preset's sample payload. A mapping failure is not an HTTP error: the response is `200` with `ok: false`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `payload` | object | No | The JSON payload to map. |

```bash
curl -X POST "$BASE_URL/api/im/sources/7/test-mapping" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "payload": { "event_id": "6633487", "event_severity": "Disaster", "trigger_name": "Zabbix agent is not available on Zabbix server", "trigger_status": "PROBLEM", "host_name": "Zabbix server" } }'
```

```json
{
  "ok": true,
  "alert": {
    "title": "Zabbix agent is not available on Zabbix server",
    "severity": "sev1",
    "status": "open",
    "host": "Zabbix server",
    "dedupKey": "6633487",
    "custom": {}
  },
  "attributes": [
    { "key": "title", "standard": true, "value": "Zabbix agent is not available on Zabbix server", "resolved": true, "flags": {} },
    { "key": "severity", "standard": true, "value": "sev1", "resolved": true, "flags": {} },
    { "key": "status", "standard": true, "value": "PROBLEM", "resolved": true, "flags": {} },
    { "key": "host", "standard": true, "value": "Zabbix server", "resolved": true, "flags": {} },
    { "key": "correlationId", "standard": true, "value": "6633487", "resolved": true, "flags": { "grouped": true } }
  ]
}
```

On failure: `{ "ok": false, "error": "title selector resolved to nothing on this payload" }`.

`alert.status` is `resolved` only when the mapped status value equals `resolved` (case-insensitive) after `resolveValues`, anything else is `open`. Without a `dedupKey` selector the dedup key is the SHA-1 of title plus host.

## Fire a test alert

`POST /api/im/sources/:id/test-alert`

Queues a real alert through the normal ingest pipeline. It usually opens a real incident and **pages whoever is on call** for the source's team. Payload: `payload` from the body, else the preset's sample payload (no fallback to recently received payloads). The body is limited to 256 KB and 20 nesting levels, like the ingest URL.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `payload` | any JSON | No | Payload to ingest. Required for `api` and `email` sources, which have no sample. |

`200 OK`: `{ "enqueued": true }`.

## Recent payloads

`GET /api/im/sources/:id/payloads`

Up to the last 10 raw payloads the source received, newest first. Useful to build a mapping. Kept in a short-lived cache, so an empty list is normal for a quiet source.

```json
{ "payloads": [ { "event_id": "6633487", "trigger_status": "PROBLEM" } ] }
```

## Source statistics

`GET /api/im/sources/:id/stats`

Daily counters for the last 30 days with data, oldest first. `day` is a UTC calendar date. `alerts` counts inbound alerts, `deduped` those folded into an existing open alert, `incidents` the incidents opened.

```json
{
  "days": [
    { "day": "2026-10-07", "alerts": 41, "deduped": 33, "incidents": 3 },
    { "day": "2026-10-08", "alerts": 12, "deduped": 9, "incidents": 1 }
  ]
}
```

## Common errors

- `401 Unauthorized` (`unauthorized`) when not authenticated
- `403 Forbidden` (`customerScopedTokenForbidden`) for a customer-scoped token
- `403 Forbidden` (`imAccessDenied`) when the session has no IM-eligible role
- `403 Forbidden` (`imNotEnabled`) when Incident Management is not enabled for the organization
- `403 Forbidden` (`imTeamWriteDenied`) on a write endpoint without `admin` role or team-admin membership
- `400 Bad Request` (`invalidRequestBody`) when `:id` is not a positive integer, `teamId`/`name`/`type` is missing or invalid, a mapping/maintenance/`snoozeUntil` value is malformed, `authMode.bearer.token_hash` is sent, `mode` is not `hmac`/`bearer`, or a supplied HMAC secret is shorter than 16 characters
- `404 Not Found` (`imSourceNotFound`) when the source does not exist or belongs to another organization
- `422 Unprocessable Entity` (`invalidTeamId`) when `teamId` is not a team of the organization
- `422 Unprocessable Entity` (`imAuthModeNotProvisionable`) when enabling HMAC or Bearer via create/`PATCH` before a secret exists
- `409 Conflict` (`imSourceReferencesExist`) when deleting a source that still has open alerts or incidents
- `409 Conflict` (`imSourceProtected`) when deleting the monitoring bridge source
- `400 Bad Request` (`imSourceHasNoIngestToken`) when rotating the token of an `api` source
- `400 Bad Request` (`imNoTestPayloadAvailable`) when test-mapping or test-alert has no payload to use
- `413 Payload Too Large` (`payloadTooLarge`), `422 Unprocessable Entity` (`invalidJson`, `payloadTooDeep`) on test-alert
- `503 Service Unavailable` (`unavailable`) when test-alert cannot queue the alert, safe to retry
