Channels

Connect a channel

Connect a channel. The required fields depend on channel; no credential is ever returned. Discord (channel: discord) — supply application_id and bot_token, plus the modes selecting which Gateway flows to serve (direct messages, and/or @mention-opens-a-thread). The token is checked with Discord first: 400 invalid_bot_token when Discord rejects it, 400 application_mismatch when it belongs to another application. Returns 501 when the deployment has no CHANNEL_TOKEN_KEY configured. WhatsApp (default) — one of two credential paths, both filling the same write-only secret:

  • BYOT (credential_source: byot, default) — supply phone_number_id and access_token from your own Meta app.

  • Embedded signup (credential_source: embedded_signup) — supply code and waba_id from the Meta popup; naturali exchanges the code for the token, subscribes its app to the WABA and registers the number for sending when it is not yet, so the customer never hands over a token. Returns 501 on deployments where Meta App credentials are not configured.

The project's plan limits how many channels it may hold. At the limit this responds 403 with plan_limit_reached, whose details carry the plan and the limit, before any credential is acquired. Only active channels count, so disabling one frees allowance without deleting it.

post/v1/projects/{project_id}/channels

Request

  • Base URL: https://api.naturali.ai
  • URL: https://api.naturali.ai/v1/projects/{project_id}/channels
  • 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)

Request body

idempotency_keystring

Deduplication key, unique within your account, that makes a retry of an ambiguous failure safe. The first request under a key performs the write and answers 201; any later request carrying the same key answers 200 with that same result, so a timeout you cannot interpret can simply be retried.

The key is claimed for as long as the record exists and never silently expires. Reusing one with a different request body is 409 idempotency_key_reused — a key names one request, so changing the body and keeping the key is a bug rather than a retry. A retry that arrives while the original is still in flight is 409 idempotency_request_in_progress.

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

channel'whatsapp' | 'discord'
phone_number_idstring

Required for whatsapp.

credential_source'byot' | 'embedded_signup'
access_tokenstring

Write-only. Required for byot. The WhatsApp access token from your own Meta app. Accepted on write, never returned.

codestring

Required for embedded_signup. The short-lived authorization code from the Meta popup; naturali exchanges it for the access token server-side. Never stored or returned.

pinstring

Optional (embedded_signup only). The two-step verification PIN used if naturali registers the number, which it does only when the number is not yet registered for sending. Needed only when the number already has one; otherwise naturali generates a PIN and does not keep it. Never stored or returned.

waba_idstring

Required for embedded_signup; optional for byot.

application_idstring

Required for discord. The Discord application id.

bot_tokenstring

Write-only. Required for discord. The bot token, sealed in naturali's encrypted store so the gateway worker can authenticate its WebSocket. Accepted on write, never returned.

Example request

{
  "idempotency_key": "signup-2026-09-18-acct-42",
  "default": {
    "action": "agent",
    "agent_id": "agent_V1StGXR8Z5jdHi6B",
    "text": "Subscribe at https://example.com/pricing",
    "language": "en-US"
  },
  "max_context_messages": 20,
  "channel": "whatsapp",
  "phone_number_id": "109999999999999",
  "credential_source": "byot",
  "access_token": "EAAG...ZDZD",
  "code": "AQD...abc",
  "pin": "123456",
  "waba_id": "104444444444444",
  "application_id": "1290000000000000000",
  "bot_token": "MT2...bot-token",
  "modes": {
    "dms": true
  }
}

Response

Channel created.

idstring required

Public channel ID (chan_ prefix).

project_idstring required
channel'whatsapp' | 'discord' required
status'active' | 'disabled' required
credential_source'byot' | 'embedded_signup' required

How the credential was obtained.

phone_number_idstring nullable required

WhatsApp phone number id; inbound webhooks route on this. Null for non-WhatsApp channels.

waba_idstring nullable required

WhatsApp Business Account id, when known.

display_phone_numberstring 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_namestring nullable

The business name Meta shows for the number, when known; null on the same terms as display_phone_number.

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

max_context_messagesinteger 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_credentialboolean required

Whether an access token is on file (its value is never returned).

created_atstring date-time required
updated_atstring date-time required

Example response

{
  "id": "chan_V1StGXR8Z5jdHi6B",
  "project_id": "proj_V1StGXR8Z5jdHi6B",
  "channel": "whatsapp",
  "status": "active",
  "credential_source": "byot",
  "phone_number_id": "109999999999999",
  "waba_id": "104444444444444",
  "display_phone_number": "+55 11 90000-0001",
  "verified_name": "Acme Suporte",
  "application_id": "1290000000000000000",
  "modes": {
    "dms": true
  },
  "default": {
    "action": "agent",
    "agent_id": "agent_V1StGXR8Z5jdHi6B",
    "text": "Subscribe at https://example.com/pricing",
    "language": "en-US",
    "config": {
      "media": {
        "audio": "transcribe"
      },
      "discord": {
        "allowed_role_ids": [
          "1290000000000000042"
        ]
      }
    }
  },
  "max_context_messages": 20,
  "has_credential": true,
  "gateway_error": {
    "close_code": 4004
  },
  "created_at": "2026-07-23T00:00:00.000Z",
  "updated_at": "2026-07-23T00:00:00.000Z"
}

Changes

No recorded changes to this operation across all 1 revision of this API.