---
title: "Update a channel"
method: PATCH
path: "/v1/projects/{project_id}/channels/{channel_id}"
tags: ["Channels"]
---

# Update a channel

`PATCH /v1/projects/{project_id}/channels/{channel_id}`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/channels/{channel_id}`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

Rotate the credential, update the kind's config (`waba_id` / `credential_source` for WhatsApp, `modes` for Discord), or flip the naturali-side status. At least one field is required. Rotating a WhatsApp token stores a new write-only secret, repoints the send tool and deletes the old secret; rotating a Discord bot token reseals it and the gateway worker reconnects with it.

## Request body

- ChannelUpdate: At least one field is required. For WhatsApp, `access_token` rotates the write-only credential; for Discord, `bot_token` rotates the sealed bot token (checked with Discord as on connect) and `modes` turns the gateway flows on or off. `default` replaces the channel's default action whole when present. The worker picks either change up on its next poll and reconnects.
  - `default` object: `agent_id` is required when `action` is `agent` or `oneshot`, `text` when it is `message`; `repeat` is only valid alongside `action: message`.
    - `action` 'agent' | 'oneshot' | 'message' | 'silence' | 'null', nullable, required: What an inbound message becomes; `null` clears an address's own action, and is refused for a channel default or a route. `agent` is a dialogue: the identity behind the message gets a session, and every later message on it continues the same one. `oneshot` runs the agent once and keeps nothing — no address, no actor, no session, no conversation — so each message is answered on its own. Choose it for work that is filed rather than discussed; choose `agent` when the answer depends on what came before. `message` delivers fixed text without running an agent, and `silence` answers nothing.
    - `agent_id` string
    - `text` string
    - `repeat` union
      - 'every' | 'once'
      - object
        - `after_seconds` integer, required
    - `language` string
    - `config` object, nullable: Conversation config bag — media handling, persona overrides, the Discord allowlist, and how a `oneshot` answers. `discord.reply` picks the delivery: `react` (the default) adds `discord.reaction` — `\u2705` unless named — to the message that triggered the run, `mention` answers beside it addressing whoever asked, `react_or_mention` reacts when the agent writes nothing and answers like `mention` when it writes a line, `thread` opens a thread and answers inside it, and `none` delivers nothing at all. A run that fails delivers nothing in every mode, so a reaction always means the work was done; there is no failure marker. A mention with nothing else in it is ignored unless the action says otherwise, for any action. `discord.use_replied_message: true` takes the text of the message it replies to, so replying to a message with only the mention files that message. `discord.empty_mention_text` answers any other bare mention with that fixed text, addressing the sender, without running the agent. `discord.forward_user_id: true` hands the sender's Discord user id to the agent's tools as the `discord_user_id` tool context, which a tool's `headers` read as `{{context:discord_user_id}}`. It is the id Discord reported for the message's author, never text the model wrote, so a tool can act for whoever sent it. Off unless set, because tool context reaches every tool the agent has. Read only for `oneshot`. A conversational `agent` in a guild *is* its thread — that is what its session is keyed on — so it always opens one.
  - `max_context_messages` integer, nullable: The most recent messages each conversation's session sends the model, taking precedence over the agent's own `max_context_messages`. An open conversation picks a change up on its next message. Null defers to the agent.
  - `access_token` string: WhatsApp. Write-only. Replaces the stored credential.
  - `waba_id` string
  - `credential_source` 'byot' | 'embedded_signup'
  - `bot_token` string: Discord. Write-only. Rotates the sealed bot token.
  - `modes` DiscordModes, nullable: Which Discord Gateway flows this channel serves; null for non-Discord channels. At least one must be enabled. * `dms` — the bot converses 1:1 in direct messages (the WhatsApp shape). Needs only the non-privileged `DIRECT_MESSAGES` intent. * `mention_threads` — an @mention in a server opens a thread, and the whole thread is one conversation. Reading the follow-up messages in the thread needs the **privileged** `MESSAGE_CONTENT` intent, which you enable on your own Discord app (Discord requires bot verification past ~100 servers), so it is off unless you ask for it.
    - `dms` boolean
    - `mention_threads` boolean
  - `status` 'active' | 'disabled'

## Response `200`

Channel updated.

- Channel
  - `id` string, required: Public channel ID (chan_ prefix).
  - `project_id` string, required
  - `channel` 'whatsapp' | 'discord', required
  - `status` 'active' | 'disabled', required
  - `credential_source` 'byot' | 'embedded_signup', required: How the credential was obtained.
  - `phone_number_id` string, nullable, required: WhatsApp phone number id; inbound webhooks route on this. Null for non-WhatsApp channels.
  - `waba_id` string, nullable, required: WhatsApp Business Account id, when known.
  - `display_phone_number` string, nullable: The number as Meta displays it, read when the number was linked by embedded signup. Null for other channels and other credential sources.
  - `verified_name` string, nullable: The business name Meta shows for the number, when known; null on the same terms as `display_phone_number`.
  - `application_id` string, nullable, required: Discord application id — the tenant's app, one channel per app. The gateway worker holds one connection per channel. Null for non-Discord channels.
  - `modes` DiscordModes, nullable, required: Which Discord Gateway flows this channel serves; null for non-Discord channels. At least one must be enabled. * `dms` — the bot converses 1:1 in direct messages (the WhatsApp shape). Needs only the non-privileged `DIRECT_MESSAGES` intent. * `mention_threads` — an @mention in a server opens a thread, and the whole thread is one conversation. Reading the follow-up messages in the thread needs the **privileged** `MESSAGE_CONTENT` intent, which you enable on your own Discord app (Discord requires bot verification past ~100 servers), so it is off unless you ask for it.
    - `dms` boolean
    - `mention_threads` boolean
  - `default` ChannelAction, required: What an inbound message resolves to — a channel's `default`, a route, or an address's own action, one shape on all three. For a Discord channel, `config.discord.allowed_role_ids` / `allowed_user_ids` restrict who may invoke the agent in a server — either list admits, empty or absent lists mean everyone, decided before the agent is invoked so an unlisted member costs nothing.
    - `action` 'agent' | 'oneshot' | 'message' | 'silence' | 'null', nullable, required: What an inbound message becomes; `null` only on an address with no action of its own, which the route table then decides. `agent` is a dialogue: the identity behind the message gets a session, and every later message on it continues the same one. `oneshot` runs the agent once and keeps nothing — no address, no actor, no session, no conversation — so each message is answered on its own. Choose it for work that is filed rather than discussed; choose `agent` when the answer depends on what came before. `message` delivers fixed text without running an agent, and `silence` answers nothing.
    - `agent_id` string, nullable, required: The agent that answers, when `action` is `agent` or `oneshot`.
    - `text` string, nullable, required: The fixed text delivered, when `action` is `message`.
    - `repeat` union, required: How often a `message` fires: `every` (default), `once`, or `{ after_seconds }`. Null unless `action` is `message`.
      - 'every' | 'once'
      - object
        - `after_seconds` integer, required
    - `language` string, nullable, required: Preferred reply language; null lets the agent decide.
    - `config` object, nullable, required: Conversation config bag — media handling, persona overrides, the Discord allowlist, and how a `oneshot` answers. `discord.reply` picks the delivery: `react` (the default) adds `discord.reaction` — `\u2705` unless named — to the message that triggered the run, `mention` answers beside it addressing whoever asked, `react_or_mention` reacts when the agent writes nothing and answers like `mention` when it writes a line, `thread` opens a thread and answers inside it, and `none` delivers nothing at all. A run that fails delivers nothing in every mode, so a reaction always means the work was done; there is no failure marker. A mention with nothing else in it is ignored unless the action says otherwise, for any action. `discord.use_replied_message: true` takes the text of the message it replies to, so replying to a message with only the mention files that message. `discord.empty_mention_text` answers any other bare mention with that fixed text, addressing the sender, without running the agent. `discord.forward_user_id: true` hands the sender's Discord user id to the agent's tools as the `discord_user_id` tool context, which a tool's `headers` read as `{{context:discord_user_id}}`. It is the id Discord reported for the message's author, never text the model wrote, so a tool can act for whoever sent it. Off unless set, because tool context reaches every tool the agent has. Read only for `oneshot`. A conversational `agent` in a guild *is* its thread — that is what its session is keyed on — so it always opens one.
  - `max_context_messages` integer, nullable: The most recent messages each conversation's session sends the model, taking precedence over the agent's own `max_context_messages`. Null defers to the agent.
  - `has_credential` boolean, required: Whether an access token is on file (its value is never returned).
  - `gateway_error` object, nullable, required: Why Discord refused this channel's gateway connection, when it did: the worker stops retrying, so the channel stays silent until a `PATCH` changes `bot_token`, `modes` or `status`, which clears it. Null when there is none, and for non-Discord channels.
    - `code` 'invalid_bot_token' | 'disallowed_intents' | 'gateway_rejected', required: `invalid_bot_token` — the token was reset or revoked. `disallowed_intents` — `mention_threads` needs the privileged Message Content intent enabled on the Discord application.
    - `close_code` integer, required: The gateway close code Discord sent.
    - `at` string, date-time, required
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required

## Other responses

- `400`: The request was malformed or failed validation.
- `401`: Missing or invalid credentials.
- `403`: The credential is scoped to a different project, or the caller's role in the project does not carry this action.
- `404`: The resource does not exist (existence is not leaked).
- `502`: The upstream runtime, or Meta, could not complete the operation.

---

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