---
title: "Update a notification channel"
method: PATCH
path: "/api/v1/channels/{id}"
tags: ["Channels"]
---

# Update a notification channel

`PATCH /api/v1/channels/{id}`

## Request

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

Partially updates a channel's name and/or config in place, preserving its
id and all route assignments.  This lets IaC clients (e.g. a Terraform
provider) edit a destination without destroying and recreating it, which
would otherwise discard email-verification state and cascade into routes.

`kind` is immutable — attempting to change it returns 400.

Changing an email channel's `address` resets `verified` to `false` and
sends a fresh verification email; any other change preserves verification.

## Request body

- ChannelUpdate: Body for PATCH /api/v1/channels/{id}. All fields are optional; an omitted field is left unchanged. `kind` is accepted only so that an attempt to change it can be rejected explicitly (400) rather than silently ignored — changing kind would invalidate the config shape and any routes pointing at this channel. To change kind, delete the destination and create a new one. `config`, when present, replaces the channel's config, with one exception: **write-only (secret) fields are merged.** A secret field omitted from `config` keeps its stored value, so a destination can be edited without re-sending a credential that is never returned, but only while the destination still points where it did: the `webhook` `url` and the `ntfy` `topic_url` must be unchanged (host and scheme compared case-insensitively, everything else exactly). A `config` that changes either of them and leaves out a stored secret is refused with 400 (`code` `DESTINATION_SECRET_REQUIRED`), so a stored secret is never carried to a new destination; send it again with the new URL. An explicit empty string (or `null`) clears a secret, which is accepted only where the field is optional (the ntfy `token`). The secret fields are the ones listed at the end of this description. Every other field is replaced: omitting it from a supplied `config` removes it. The resulting config is validated with the same per-kind rules as POST /api/v1/channels (`webhook` requires `url`+`secret`, `slack`/`discord`/`msteams`/`googlechat` require `webhook_url`, `telegram` requires `bot_token`+`chat_id`, `ntfy` requires `topic_url`, `pushover` requires `token`+`user_key`, `email` requires `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. A `config` you supply is re-validated against the host-pin and `https` rules documented on `ChannelCreate`. A destination stored before those rules existed keeps delivering, and a request that omits `config` does not re-check it. **Email re-verification:** changing an email channel's `address` resets `verified` to `false` and sends a fresh verification email to the new address. Any other change (including a name-only PATCH) preserves the existing verification state. Write-only fields (`secret`, `bot_token`, the discord/slack/msteams/googlechat `webhook_url`, `token`/`user_key` for pushover and the optional ntfy `token`) are stored but never returned in the response.
  - `kind` string: Present only to be rejected — kind is immutable.
  - `name` string: New human-readable label for the channel.
  - `config` object: Replacement kind-specific configuration object (see description above). A secret field left out keeps its stored value while the webhook `url` / ntfy `topic_url` is unchanged; `""` clears an optional one.

## Response `200`

The updated channel (no secrets).

- 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`: id is not a valid UUID, body is malformed, kind was changed, or config failed per-kind validation.
- `401`: Missing or invalid API key.
- `404`: Channel not found in this project.
- `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)
