Skip to main content
ElevenLabs Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Custom Channel

Custom Channel connects an external messaging system to an ElevenLabs agent. Send user messages to an ElevenLabs webhook, then receive agent replies on your own HTTPS endpoint.

Capability Support
Zero retention mode (ZRM) Not supported — unavailable for ZRM workspaces and ZRM agents
Attachments in messages Not supported — messages are text only

Open your agent, select Channels, choose Custom Channel, and click Add trigger.

Select an existing connection or create one, then enter the Reply Webhook URL.

Click Add, then copy the Inbound Webhook URL, Inbound Secret, and Outbound Signing Secret.

Send user messages to the inbound webhook URL with the inbound secret in X-Webhook-Secret. Use the outbound signing secret to verify each reply.

Send a POST request to the generated webhook URL:

text
POST /v1/convai/api-integrations/custom_channel/triggers/{trigger_connection_id}/async_message
X-Webhook-Secret: <inbound-secret>
Content-Type: application/json
JSON
{
  "data": {
    "type": "user_message",
    "text": "Where is my order?",
    "user_identifier": "customer_8427"
  },
  "user_message_id": "msg_01k1e6z3f4t8n9c2",
  "dynamic_variables": {
    "order_id": "order_72491"
  }
}
Field Required Description
data.type Yes Must be user_message.
data.text Yes Non-empty user message.
data.user_identifier No Identifier for the external user.
user_message_id Yes Non-empty idempotency key supplied by your system.
conversation_id No Include the returned ID to continue a conversation. Omit it to start a new conversation.
dynamic_variables No Dynamic variables supplied to the agent for this turn.

ElevenLabs returns 202 Accepted before processing the turn:

JSON
{
  "conversation_id": "conv_01k1e72d4x8p6v3m",
  "status": "queued"
}

To continue the conversation, send another request with that conversation_id and a new user_message_id.

ElevenLabs sends a POST request to the reply webhook URL after each turn:

JSON
{
  "version": "1",
  "conversation_id": "conv_01k1e72d4x8p6v3m",
  "user_message_ids": ["msg_01k1e6z3f4t8n9c2"],
  "status": "completed",
  "data": [
    {
      "type": "agent_response",
      "event": {
        "agent_response": "Your order is scheduled to arrive tomorrow.",
        "response_id": "9f2c1a7e-4b3d-4e8a-9c1f-2d6b8e0a5f31",
        "event_id": 4
      }
    },
    {
      "type": "agent_tool_response",
      "event": {
        "tool_name": "end_call",
        "tool_call_id": "toolu_01k1e70r4b8y",
        "tool_type": "system",
        "event_id": 4,
        "is_called": true,
        "is_error": false,
        "is_blocked": false,
        "status": "success"
      }
    }
  ],
  "error": null
}

If processing fails, status is failed, data is [], and error contains a description.

data lists events in turn order. Every item has a type and an event:

  • agent_response contains one agent utterance. response_id uniquely identifies the utterance, while event_id associates it with a turn. Join the agent_response values if your channel renders one text bubble per turn.
  • agent_tool_response reports a tool outcome and shares the turn's event_id. Its status is success, error, blocked, or skipped. A response with tool_type: "system", tool_name: "end_call", and status: "success" means the agent ended the conversation.

Multiple inbound messages can be coalesced into one turn. user_message_ids lists the user message IDs this reply is answering.

Each reply includes an ElevenLabs-Signature header:

text
t=1753876800,v0=<hex-digest>

The digest is an HMAC-SHA256 signature over {timestamp}.{raw_request_body} using the outbound signing secret. Verify the raw body before parsing JSON and reject stale timestamps.

Python
import hashlib
import hmac
import time


def verify_signature(raw_body: bytes, header: str, secret: str) -> None:
    values = dict(part.split("=", 1) for part in header.split(","))
    timestamp = values["t"]
    if abs(time.time() - int(timestamp)) > 30 * 60:
        raise ValueError("Stale webhook signature")

    expected = hmac.new(
        secret.encode(),
        timestamp.encode() + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expected, values["v0"]):
        raise ValueError("Invalid webhook signature")
TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySignature(rawBody: Buffer, header: string, secret: string): void {
  const values = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
  const timestamp = values.t;
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 30 * 60) {
    throw new Error("Stale webhook signature");
  }

  const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
  const received = Buffer.from(values.v0 ?? "", "hex");
  if (received.length !== expected.length || !timingSafeEqual(expected, received)) {
    throw new Error("Invalid webhook signature");
  }
}

ElevenLabs makes three in-process delivery attempts at approximately 0, 0.5, and 2 seconds. A 2xx response marks delivery successful.

Request bodies are limited to 256 KiB.

Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu