A channel line is one connection to a messaging channel — a WhatsApp number, a website chat, a Telegram bot. Agents bind to a line, the Inbox routes its tickets, and campaigns send through it.
Secrets come back verbatim
GET /channels returns configJson exactly as stored, which for WhatsApp, Telegram and email lines includes access tokens, app secrets and API keys, to anyone with channels:read. Never log a response, never render a stored secret back into a form or a page, and treat a blank field as “keep what is stored”.
Credentials are globally unique
A phone number, verify token or widget key can belong to only one active line of its type across all organisations. Claiming one that another organisation holds answers 409 CHANNEL_CREDENTIAL_IN_USE.
There is no hard delete: Disable a channel soft-disables the line and it keeps its name, so creating a new line under the same name answers 409. Re-enable the old line with an update instead.
Endpoints
GET/channelsList channelsPOST/channelsCreate a channelPUT/channels/{channelId}Update a channelDELETE/channels/{channelId}Disable a channelPUT/channels/{channelId}/agentBind an agentPUT/channels/{channelId}/routing-modeSet who answers a channelGET/channels/routable-agentsList agents a channel can usePOST/channels/whatsapp/inspectInspect a WhatsApp tokenPOST/channels/whatsapp/connectConnect WhatsAppThe channel line object
The example is a Web Chat line, whose configJson holds only a widget key. Other types carry credentials there.
Attributes
idintegerUnique identifier of the channel line.namestringLine name. Unique per organisation and channel type — even after the line is disabled.channelTypestringThe channel:WEBCHAT,WHATSAPP,TELEGRAM,INSTAGRAM,FACEBOOKorEMAIL. Stored upper-case. Not every type can send and receive yet; only WhatsApp, Web Chat and Telegram are fully connected.statusstringA line routes messages only whileACTIVE.One of:ACTIVEDISABLEDconfigJsonstringnullableThe line’s settings and credentials as a serialised JSON object. The backend stores it verbatim and validates nothing inside it. It is returned verbatim too, secrets included — see the warning below.routingModestringWho answers the line: the bound agent (BOT), people (LIVE_AGENT), or a third party’s chatbot (EXTERNAL_BOT). A line with no value reads asBOT.One of:BOTLIVE_AGENTEXTERNAL_BOTbotIdintegernullableThe published agent bound to the line.nullleaves the line human-answered.defaultQueueIdintegernullableThe queue new conversations and tickets from this line land in. Several lines may share one.inboxIdintegernullableThe same queue asdefaultQueueId, under its older name.externalBotConnectionIdintegernullableThe external bot that answers the line. Set only inEXTERNAL_BOTmode.metaSubscriptionobjectnullableThe last Meta subscribe attempt:ok,code,message,checkedAt. Read-only; ignored if sent.
{
"id": 21,
"name": "Support — website",
"channelType": "WEBCHAT",
"status": "ACTIVE",
"configJson": "{\"widgetKey\":\"6b0f7c1e-2f4e-4d5a-9a53-0c3c1f6d8e21\"}",
"routingMode": "BOT",
"botId": 55,
"defaultQueueId": 4,
"inboxId": 4,
"externalBotConnectionId": null,
"metaSubscription": null
}
List channels
Returns your organisation’s channel lines as a plain array. Leave channelType out to see everything, disabled lines included: the backend narrows to ACTIVE lines only when channelType is present.
channels:readQuery parameters
channelTypestringUpper-case channel type, e.g.WHATSAPP. Returns only theACTIVElines of that type.
Returns
200The channel line object.
curl -X GET "https://api.cx.na6iq.com/api/v1/channels" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/channels', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.get(
"https://api.cx.na6iq.com/api/v1/channels",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
[
{
"id": 21,
"name": "Support — website",
"channelType": "WEBCHAT",
"status": "ACTIVE",
"configJson": "{\"widgetKey\":\"6b0f7c1e-2f4e-4d5a-9a53-0c3c1f6d8e21\"}",
"routingMode": "BOT",
"botId": 55,
"defaultQueueId": 4,
"inboxId": 4,
"externalBotConnectionId": null,
"metaSubscription": null
}
]
Create a channel
Creates a line. It lands in defaultQueueId when you name a queue; otherwise a new queue is created with the line and named after it, which is what makes handover to people work. For Web Chat the backend mints no widget key — generate a unique one yourself (a UUID works) and put it in configJson as widgetKey. For guided WhatsApp setup use Connect WhatsApp. A name already used by a line of the same type answers 409. A line created with no botId is answered by people (routingMode LIVE_AGENT).
channels:writeRequest body
namestringrequiredLine name.channelTypestringrequiredThe channel type, e.g.WEBCHAT.configJsonstringSettings as a serialised JSON object. Defaults to"{}".botIdintegernullableA published agent to bind to the line.defaultQueueIdintegernullableAn existing queue to land in. Omit for a new queue named after the line.
Returns
201The channel line object.
Errors
- 400
VALIDATION_FAILED`name` or `channelType` is missing. - 404
BOT_NOT_FOUNDNo agent with this `botId` in your organisation. - 404
INBOX_NOT_FOUNDNo queue with this `defaultQueueId` in your organisation. - 409
INBOX_INACTIVEThe queue is not active. - 409
INBOX_IS_TESTThe queue is a test queue. - 409
CHANNEL_LINE_EXISTSA line of the same type already has this name. - 409
CHANNEL_CREDENTIAL_IN_USEA phone number, verify token, widget key, page ID, Instagram account ID or inbound address in `configJson` (for Telegram, also the bot ID and URL token) is already claimed by another **active** line of the same type. This check is global across all organisations, and the message names the offending key.
curl -X POST "https://api.cx.na6iq.com/api/v1/channels" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Support — website",
"channelType": "WEBCHAT",
"configJson": "{\"widgetKey\":\"6b0f7c1e-2f4e-4d5a-9a53-0c3c1f6d8e21\"}",
"botId": 55
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/channels', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Support — website",
"channelType": "WEBCHAT",
"configJson": "{\"widgetKey\":\"6b0f7c1e-2f4e-4d5a-9a53-0c3c1f6d8e21\"}",
"botId": 55
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/channels",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"name": "Support — website",
"channelType": "WEBCHAT",
"configJson": "{\"widgetKey\":\"6b0f7c1e-2f4e-4d5a-9a53-0c3c1f6d8e21\"}",
"botId": 55
},
)
res.raise_for_status()
data = res.json()
{
"id": 21,
"name": "Support — website",
"channelType": "WEBCHAT",
"status": "ACTIVE",
"configJson": "{\"widgetKey\":\"6b0f7c1e-2f4e-4d5a-9a53-0c3c1f6d8e21\"}",
"routingMode": "BOT",
"botId": 55,
"defaultQueueId": 4,
"inboxId": 4,
"externalBotConnectionId": null,
"metaSubscription": null
}
Update a channel
Updates the line. Fields you omit keep their stored value, except configJson, which is replaced wholesale — to change one setting, read the line, merge into the parsed configJson, and send the whole object back, or you will erase the other keys (a Web Chat line’s widgetKey is the live credential every embedded widget uses). Send status: "ACTIVE" to bring a disabled line back. This endpoint cannot detach an agent — a null botId is ignored; use Bind an agent for that.
channels:writePath parameters
channelIdintegerrequiredThe channel line’sid.
Request body
namestringnullableLine name.nullkeeps the stored name.channelTypestringnullableThe channel type.nullkeeps the stored type.configJsonstringnullableThe complete new settings, serialised.botIdintegernullableRebinds the line to this agent; null is ignored.statusstringACTIVEre-enables a disabled line.One of:ACTIVEDISABLEDdefaultQueueIdintegernullableMove the line’s new conversations to another queue. Work already queued stays where it is.
Returns
200The channel line object.
Errors
- 404
BOT_NOT_FOUNDNo agent with this `botId` in your organisation. - 404
INBOX_NOT_FOUNDNo queue with this `defaultQueueId` in your organisation. - 409
INBOX_INACTIVEThe queue is not active. - 409
INBOX_IS_TESTThe queue is a test queue. - 409
CONSTRAINT_VIOLATIONThe name is already taken. - 409
CHANNEL_CREDENTIAL_IN_USEA phone number, verify token, widget key, page ID, Instagram account ID or inbound address in `configJson` (for Telegram, also the bot ID and URL token) is already claimed by another **active** line of the same type. This check is global across all organisations, and the message names the offending key. - 404
CHANNEL_LINE_NOT_FOUNDNo channel line with this ID in your organisation.
curl -X PUT "https://api.cx.na6iq.com/api/v1/channels/42" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Support — website (Arabic)",
"channelType": "WEBCHAT",
"status": "ACTIVE"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/channels/42', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Support — website (Arabic)",
"channelType": "WEBCHAT",
"status": "ACTIVE"
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.put(
"https://api.cx.na6iq.com/api/v1/channels/42",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"name": "Support — website (Arabic)",
"channelType": "WEBCHAT",
"status": "ACTIVE"
},
)
res.raise_for_status()
data = res.json()
{
"id": 21,
"name": "Support — website (Arabic)",
"channelType": "WEBCHAT",
"status": "ACTIVE",
"configJson": "{\"widgetKey\":\"6b0f7c1e-2f4e-4d5a-9a53-0c3c1f6d8e21\"}",
"routingMode": "BOT",
"botId": 55,
"defaultQueueId": 4,
"inboxId": 4,
"externalBotConnectionId": null,
"metaSubscription": null
}
Disable a channel
Soft-disables the line: it stops routing messages, but it is not removed and keeps its name, and its queue stays open because other lines may land there. For a Telegram line the bot’s webhook is also removed.
channels:writePath parameters
channelIdintegerrequiredThe channel line’sid.
Returns
204The line is disabled.
Errors
- 404
CHANNEL_LINE_NOT_FOUNDNo channel line with this ID in your organisation.
curl -X DELETE "https://api.cx.na6iq.com/api/v1/channels/42" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/channels/42', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
import os, requests
res = requests.delete(
"https://api.cx.na6iq.com/api/v1/channels/42",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
Bind an agent
Points the line at the published agent that answers it, or detaches it with an empty body, leaving the line human-answered. Binding sets routingMode to BOT; detaching sets it to LIVE_AGENT. This is the only way to detach: it is its own route because PUT /channels/{channelId} cannot express it. One line holds one agent, but an agent can answer many lines. Web Chat lines are bound by publishing an agent, not here.
channels:writePath parameters
channelIdintegerrequiredThe channel line’sid.
Request body
botIdintegernullableThe agent’sid. Leave it out (an empty body) to detach.
Returns
200The channel line object.
Errors
- 404
BOT_NOT_FOUNDNo agent with this ID in your organisation. - 409
BOT_NOT_PUBLISHEDThe agent exists but is not active, so it cannot run. - 409
CHANNEL_AGENT_MANAGED_BY_PUBLISHThe line is Web Chat; its agent is whatever was last published to it. - 404
CHANNEL_LINE_NOT_FOUNDNo channel line with this ID in your organisation.
curl -X PUT "https://api.cx.na6iq.com/api/v1/channels/42/agent" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"botId": 55
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/channels/42/agent', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"botId": 55
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.put(
"https://api.cx.na6iq.com/api/v1/channels/42/agent",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"botId": 55
},
)
res.raise_for_status()
data = res.json()
{
"id": 22,
"name": "Support — WhatsApp",
"channelType": "WHATSAPP",
"status": "ACTIVE",
"configJson": "{\"phoneNumberId\":\"109876543210987\"}",
"routingMode": "BOT",
"botId": 55,
"defaultQueueId": 4,
"inboxId": 4,
"externalBotConnectionId": null,
"metaSubscription": null
}
Set who answers a channel
Switches the line between its bound agent, people, and an external bot, without unbinding the agent. Use it to hand a number to people for an hour and take it back. Not available on Web Chat lines.
channels:writePath parameters
channelIdintegerrequiredThe channel line’sid.
Request body
routingModestringrequiredWho answers from now on.One of:BOTLIVE_AGENTEXTERNAL_BOTexternalBotConnectionIdintegernullableRequired withEXTERNAL_BOT: the external bot that answers.
Returns
200The channel line object.
Errors
- 400
INVALID_REQUEST`routingMode` is `EXTERNAL_BOT` but `externalBotConnectionId` is missing. - 404
EXTERNAL_BOT_NOT_FOUNDNo external bot with this `externalBotConnectionId` in your organisation. - 409
CHANNEL_AGENT_MANAGED_BY_PUBLISHThe line is Web Chat; its binding belongs to publishing. - 404
CHANNEL_LINE_NOT_FOUNDNo channel line with this ID in your organisation.
curl -X PUT "https://api.cx.na6iq.com/api/v1/channels/42/routing-mode" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"routingMode": "LIVE_AGENT"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/channels/42/routing-mode', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"routingMode": "LIVE_AGENT"
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.put(
"https://api.cx.na6iq.com/api/v1/channels/42/routing-mode",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"routingMode": "LIVE_AGENT"
},
)
res.raise_for_status()
data = res.json()
{
"id": 22,
"name": "Support — WhatsApp",
"channelType": "WHATSAPP",
"status": "ACTIVE",
"configJson": "{\"phoneNumberId\":\"109876543210987\"}",
"routingMode": "LIVE_AGENT",
"botId": 55,
"defaultQueueId": 4,
"inboxId": 4,
"externalBotConnectionId": null,
"metaSubscription": null
}
List agents a channel can use
Returns the organisation’s published (active) agents — the only ones a line can be bound to. It needs channels:read rather than builder access, so people who manage channels do not need to enter the builder.
channels:readReturns
200The channel line object.
curl -X GET "https://api.cx.na6iq.com/api/v1/channels/routable-agents" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/channels/routable-agents', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.get(
"https://api.cx.na6iq.com/api/v1/channels/routable-agents",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
[
{
"id": 55,
"name": "Support assistant",
"status": "ACTIVE"
}
]
Inspect a WhatsApp token
Step 1 of WhatsApp quick connect. Reads a Meta access token and reports the WhatsApp Business accounts and phone numbers it can reach. Stores nothing, and the token is never echoed back or logged. If needsWabaId comes back true, call again with wabaId.
channels:writeRequest body
accessTokenstringrequiredA Meta access token.wabaIdstringThe WhatsApp Business account ID. Only when a first call answeredneedsWabaId.
Returns
200The channel line object.
Errors
- 422
WHATSAPP_TOKEN_INVALIDMeta rejected the token. - 422
WHATSAPP_WABA_NOT_ACCESSIBLEThe token cannot reach that business account. - 502
META_UNAVAILABLEMeta’s API did not answer.
curl -X POST "https://api.cx.na6iq.com/api/v1/channels/whatsapp/inspect" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accessToken": "<META_ACCESS_TOKEN>"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/channels/whatsapp/inspect', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"accessToken": "<META_ACCESS_TOKEN>"
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/channels/whatsapp/inspect",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"accessToken": "<META_ACCESS_TOKEN>"
},
)
res.raise_for_status()
data = res.json()
{
"tokenValid": true,
"permanent": true,
"expiresAt": null,
"needsWabaId": false,
"wabas": [
{
"id": "104857612345678",
"name": "Al-Noor Telecom"
}
],
"numbers": [
{
"wabaId": "104857612345678",
"phoneNumberId": "109876543210987",
"displayPhoneNumber": "+962 6 555 0142",
"verifiedName": "Al-Noor Telecom",
"status": "CONNECTED",
"ready": true,
"needsRegistration": false,
"pinEnabled": false,
"inUse": false
}
],
"warnings": []
}
Connect WhatsApp
Step 2 of WhatsApp quick connect. Saves the line as ACTIVE with a generated verify token, then subscribes the app and sets the webhook. It is idempotent per phone number ID: calling it again re-subscribes instead of creating a second line. If the subscribe step fails, the line is kept and webhook.subscribed is false. pin is for a number that still needs registration and is a single attempt — ten wrong tries lock the number for 72 hours, so never retry it automatically. Without appSecret, signature checks on inbound messages stay off.
channels:writeRequest body
accessTokenstringrequiredThe Meta access token.wabaIdstringrequiredThe WhatsApp Business account ID.phoneNumberIdstringrequiredThe phone number to connect, from Inspect a WhatsApp token.namestringLine name.pinstringSix-digit PIN, only for a number thatneedsRegistration.appSecretstringYour Meta app secret, to turn on signature checks for inbound messages.
Returns
200The channel line object.
Errors
- 409
CHANNEL_CREDENTIAL_IN_USEA phone number, verify token, widget key, page ID, Instagram account ID or inbound address in `configJson` (for Telegram, also the bot ID and URL token) is already claimed by another **active** line of the same type. This check is global across all organisations, and the message names the offending key. - 422
WHATSAPP_TOKEN_INVALIDMeta rejected the token. - 422
WHATSAPP_REGISTER_FAILEDRegistering the number with the PIN failed. If it came after the line was saved, `error.details` has an entry with code `LINE_SAVED` whose message is the line ID. - 412
WHATSAPP_PUBLIC_URL_MISSINGThe platform has no public webhook URL configured. - 412
WHATSAPP_PUBLIC_URL_INVALIDThe platform’s public webhook URL is not usable as a callback. - 502
META_UNAVAILABLEMeta’s API did not answer.
curl -X POST "https://api.cx.na6iq.com/api/v1/channels/whatsapp/connect" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accessToken": "<META_ACCESS_TOKEN>",
"wabaId": "104857612345678",
"phoneNumberId": "109876543210987",
"name": "Support — WhatsApp"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/channels/whatsapp/connect', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"accessToken": "<META_ACCESS_TOKEN>",
"wabaId": "104857612345678",
"phoneNumberId": "109876543210987",
"name": "Support — WhatsApp"
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/channels/whatsapp/connect",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"accessToken": "<META_ACCESS_TOKEN>",
"wabaId": "104857612345678",
"phoneNumberId": "109876543210987",
"name": "Support — WhatsApp"
},
)
res.raise_for_status()
data = res.json()
{
"channelConfigId": 22,
"name": "Support — WhatsApp",
"displayPhoneNumber": "+962 6 555 0142",
"routingMode": "BOT",
"botId": null,
"webhook": {
"subscribed": true,
"override": "PHONE",
"error": null
},
"registered": false,
"signatureChecks": false,
"warnings": []
}