---
title: "Create a notification channel"
method: POST
path: "/api/v1/channels"
tags: ["Channels"]
---

# Create a notification channel

`POST /api/v1/channels`

## Request

- Base URL: `https://app.lastping.dev`
- URL: `https://app.lastping.dev/api/v1/channels`
- Auth: API key in header `X-Api-Key`

Creates a new notification channel for the authenticated project.
Secrets (`secret` for webhooks, `bot_token` for Telegram, `webhook_url` for
Discord/Slack/MS Teams/Google Chat, `token`+`user_key` for Pushover) are stored
server-side but are **never returned** in any API response.
Email channels are created unverified; a verification email is sent to the address
and the channel will not alert until verified.

`config` must name only the fields its kind accepts, each exactly once, every URL must
use `https`, and the branded kinds are pinned to their vendor's hosts — see
`ChannelCreate` for the per-kind rules and the `webhook` escape hatch.

## Request body

- ChannelCreate: Body for POST /api/v1/channels. `kind` must be one of: `webhook`, `telegram`, `discord`, `slack`, `ntfy`, `pushover`, `msteams`, `googlechat`, or `email`. The `config` object carries kind-specific fields: | kind | required config fields | optional config fields | write-only (never returned) | |------|------------------------|------------------------|----------------------------| | `webhook` | `url`, `secret` | — | `secret` | | `telegram` | `bot_token`, `chat_id` | — | `bot_token` | | `discord` | `webhook_url` | — | `webhook_url` | | `slack` | `webhook_url` | — | `webhook_url` | | `ntfy` | `topic_url` | `token` | `token` | | `pushover` | `token`, `user_key` | — | `token`, `user_key` | | `msteams` | `webhook_url` | — | `webhook_url` | | `googlechat` | `webhook_url` | — | `webhook_url` | | `email` | `address` | — | — | `config` must name only the fields listed for its kind, each exactly once — an unrecognised or repeated key is refused with `code` `DESTINATION_CONFIG_INVALID` rather than ignored. **Write-only fields** (`secret`, `bot_token`, `webhook_url` for discord/slack/msteams/ googlechat, `token`/`user_key` for pushover and the optional ntfy `token`) are stored server-side but are **never returned** in any API response. **Branded kinds are host-pinned:** `slack` must point at `hooks.slack.com`, `discord` at `discord.com` or `discordapp.com`, `msteams` at `webhook.office.com`, `outlook.office.com`, `logic.azure.com`, `logic.azure.us` or `environment.api.powerplatform.com`, `googlechat` at `chat.googleapis.com`. Subdomains of those hosts are accepted; the port and path are not constrained. Use `webhook` for any other endpoint — it accepts any host. Every destination URL must use `https`. A refused URL comes back as a 400 with `code` `DESTINATION_HOST_NOT_ALLOWED`. A pin narrows a branded destination to the vendor's own platform. It does **not** prove the endpoint belongs to whoever created the destination: every pinned domain is multi-tenant and self-service. `webhook` and `ntfy` are deliberately unpinned — `webhook` is the generic escape hatch, and self-hosted ntfy is a supported configuration. Both still require `https`. A project holds at most **25** destinations; the 26th create is refused with a 403 and `code` `DESTINATION_CAP_REACHED`. **Email channels** are created **unverified** — a verification email is sent to `address`; the channel will not send alerts until the recipient clicks the verification link. Other channel kinds are verified immediately upon creation.
  - `kind` 'webhook' | 'telegram' | 'discord' | 'slack' | 'ntfy' | 'pushover' | 'msteams' | 'googlechat' | 'email', required: Channel type.
  - `name` string, required: Human-readable label for the channel.
  - `config` object, required: Kind-specific configuration object. **webhook** — `{"url": "https://…", "secret": "…"}`. `secret` is WRITE-ONLY and never returned. **telegram** — `{"bot_token": "…", "chat_id": "…"}`. `bot_token` is WRITE-ONLY and never returned. **discord** — `{"webhook_url": "https://discord.com/api/webhooks/…"}`. `webhook_url` is WRITE-ONLY and never returned. **slack** — `{"webhook_url": "https://hooks.slack.com/services/…"}`. `webhook_url` is WRITE-ONLY and never returned. **ntfy** — `{"topic_url": "https://ntfy.sh/my-topic"}`. `topic_url` is not a credential and IS returned as `target`. **pushover** — `{"token": "…", "user_key": "…"}`. Both `token` and `user_key` are WRITE-ONLY and never returned. **msteams** — `{"webhook_url": "https://…webhook.office.com/…"}`. `webhook_url` is WRITE-ONLY and never returned. **googlechat** — `{"webhook_url": "https://chat.googleapis.com/v1/spaces/…"}`. `webhook_url` is WRITE-ONLY and never returned. **email** — `{"address": "alerts@example.com"}`. Channel is created unverified; a verification email is sent to the address. Only the fields listed above for the kind are accepted, each exactly once; an unrecognised or repeated key is a 400 with `code` `DESTINATION_CONFIG_INVALID`. Every URL must use `https`, and the branded kinds are pinned to their vendor's hosts (`slack` → `hooks.slack.com`, `discord` → `discord.com`/`discordapp.com`, `msteams` → `webhook.office.com`/`outlook.office.com`/`logic.azure.com`/ `logic.azure.us`/`environment.api.powerplatform.com`, `googlechat` → `chat.googleapis.com`). Anything else belongs on kind `webhook`, which accepts any host.

## Response `201`

Channel created.

- Channel: A notification channel returned by the API. Secrets (`secret`, `bot_token`, `webhook_url` for discord/slack/msteams/googlechat, the optional ntfy `token`, and `token`/`user_key` for pushover) are NEVER included — only a non-secret `target` hint is provided. A destination of a branded kind is host-pinned: `slack` points at `hooks.slack.com`, `discord` at `discord.com` or `discordapp.com`, `msteams` at `webhook.office.com`, `outlook.office.com`, `logic.azure.com`, `logic.azure.us` or `environment.api.powerplatform.com`, and `googlechat` at `chat.googleapis.com` — so a destination labelled `slack` reaches Slack and not some arbitrary collector. The pin narrows the target to the vendor's platform; it does **not** prove the endpoint belongs to whoever created the destination, because every pinned domain is multi-tenant and self-service. `webhook` and `ntfy` are unpinned by design. Destinations created before the pin shipped are grandfathered and may still carry an off-host URL; they keep delivering until their `config` is next written.
  - `id` string, uuid: Unique channel identifier.
  - `kind` 'webhook' | 'telegram' | 'discord' | 'slack' | 'ntfy' | 'pushover' | 'msteams' | 'googlechat' | 'email': Channel type.
  - `name` string: Human-readable label.
  - `target` string: Non-secret hint about the channel destination. - **webhook**: the webhook URL. - **telegram**: `"chat <chat_id>"`. - **discord**: the fixed string `"discord webhook"` (the actual URL is write-only). - **slack**: the fixed string `"slack webhook"` (the actual URL is write-only). - **ntfy**: the topic URL (not a credential, so it is returned in full). - **pushover**: the fixed string `"pushover"` (token and user_key are write-only). - **msteams**: the fixed string `"teams webhook"` (the actual URL is write-only). - **googlechat**: the fixed string `"google chat webhook"` (the actual URL is write-only). - **email**: the email address.
  - `verified` boolean: Whether the channel has been verified. Always `true` for webhook, telegram, discord, slack, ntfy, pushover, msteams, and googlechat channels (verified on creation). For `email` channels, `false` until the recipient clicks the verification link; unverified email channels do not send alerts.
  - `disabled` boolean: Whether the channel has been disabled (e.g. after repeated delivery failures).
  - `disable_reason` string: Human-readable reason the channel was disabled (omitted when not disabled).
  - `created_at` string, date-time: RFC 3339 UTC timestamp when the channel was created.
  - `used_by_monitors` integer: How many distinct monitors route any event to this destination. Counted across every event type, so a monitor routing both `down` and `recovery` here counts once. 0 means nothing would ever be sent to it. Omitted when the server could not read the count. That happens on one path only: `PATCH /api/v1/channels/{id}` commits the update before it reads the count, so a read failure there leaves the destination saved and this field absent rather than reporting a 0 the destination has not earned. Re-read it from `GET /api/v1/channels`.

## Other responses

- `400`: Invalid request body, unknown kind, or missing required config fields. Also: a `config` naming a field the kind does not accept, or naming one twice (`code` `DESTINATION_CONFIG_INVALID`), and a target URL that is not `https` or is off its kind's pinned hosts (`code` `DESTINATION_HOST_NOT_ALLOWED`).
- `401`: Missing or invalid API key.
- `403`: Project is at its destination limit (25). `code` is `DESTINATION_CAP_REACHED`. The request is well-formed and would succeed on a project with room, which is why this is a 403 rather than a 400. Delete a destination you no longer route to, or ask support to raise the limit.
- `429`: Too many requests. The per-principal fixed-window rate limit has been exceeded. Retry after the number of seconds indicated by the `Retry-After` response header. The problem body's `code` is `RATE_LIMITED`. It names this limiter specifically, so a client can tell it apart from the application-level throttles that also answer 429 with their own codes and their own, much longer, `Retry-After` values (`TEST_PING_RATE_LIMITED`, `TOO_MANY_ATTEMPTS`). Back off globally on `RATE_LIMITED`; back off on the one operation for the others. The principal is the API key for a key-authenticated request and the USER for a browser session, so every tab one person has open shares one budget.

---

[API](https://skmtc.dev/lastping/apis/lastping-management-api.md) · [All operations](https://skmtc.dev/lastping/apis/lastping-management-api/llms.txt) · [OpenAPI document](https://skmtc.dev/lastping/apis/lastping-management-api/revisions/bbc6eda91c46?raw)
