Custom Channel

Connect an external text channel to an ElevenLabs agent with webhooks

Overview

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.

Custom Channel is in alpha.

Custom Channel is unavailable for agents or workspaces using zero-retention mode. Inbound requests for such an agent or workspace are rejected with 403 Forbidden.

Setup

1

Open Custom Channel

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

2

Configure the trigger

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

3

Copy the credentials

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

4

Configure your service

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 message

Send a POST request to the generated webhook URL:

POST /v1/convai/api-integrations/custom_channel/triggers/{trigger_connection_id}/async_message
X-Webhook-Secret: <inbound-secret>
Content-Type: application/json
1{
2 "data": {
3 "type": "user_message",
4 "text": "Where is my order?",
5 "user_identifier": "customer_8427"
6 },
7 "user_message_id": "msg_01k1e6z3f4t8n9c2",
8 "dynamic_variables": {
9 "order_id": "order_72491"
10 }
11}
FieldRequiredDescription
data.typeYesMust be user_message.
data.textYesNon-empty user message.
data.user_identifierNoIdentifier for the external user.
user_message_idYesNon-empty idempotency key supplied by your system.
conversation_idNoInclude the returned ID to continue a conversation. Omit it to start a new conversation.
dynamic_variablesNoDynamic variables supplied to the agent for this turn.

ElevenLabs returns 202 Accepted before processing the turn:

1{
2 "conversation_id": "conv_01k1e72d4x8p6v3m",
3 "status": "queued"
4}

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

Replaying the same scoped user_message_id within 24 hours does not start another turn. Initial messages are scoped by trigger and user_identifier; continuation messages are additionally scoped by conversation_id.

Receive replies

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

1{
2 "version": "1",
3 "conversation_id": "conv_01k1e72d4x8p6v3m",
4 "user_message_ids": ["msg_01k1e6z3f4t8n9c2"],
5 "status": "completed",
6 "data": [
7 {
8 "type": "agent_response",
9 "event": {
10 "agent_response": "Your order is scheduled to arrive tomorrow.",
11 "response_id": "9f2c1a7e-4b3d-4e8a-9c1f-2d6b8e0a5f31",
12 "event_id": 4
13 }
14 },
15 {
16 "type": "agent_tool_response",
17 "event": {
18 "tool_name": "end_call",
19 "tool_call_id": "toolu_01k1e70r4b8y",
20 "tool_type": "system",
21 "event_id": 4,
22 "is_called": true,
23 "is_error": false,
24 "is_blocked": false,
25 "status": "success"
26 }
27 }
28 ],
29 "error": null
30}

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.

Verify reply signatures

Each reply includes an ElevenLabs-Signature header:

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.

1import hashlib
2import hmac
3import time
4
5
6def verify_signature(raw_body: bytes, header: str, secret: str) -> None:
7 values = dict(part.split("=", 1) for part in header.split(","))
8 timestamp = values["t"]
9 if abs(time.time() - int(timestamp)) > 30 * 60:
10 raise ValueError("Stale webhook signature")
11
12 expected = hmac.new(
13 secret.encode(),
14 timestamp.encode() + b"." + raw_body,
15 hashlib.sha256,
16 ).hexdigest()
17 if not hmac.compare_digest(expected, values["v0"]):
18 raise ValueError("Invalid webhook signature")

Delivery behavior

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

The reply URL must use HTTPS. Local development also permits loopback HTTP URLs such as http://127.0.0.1:8765/webhook.

Request bodies are limited to 256 KiB.