An external bot is a chatbot you run yourself that answers channel lines in place of a Natiq agent. Natiq posts each customer message to your webhook, your bot replies by calling Natiq with its own credentials, and when the bot hands over or stops answering the conversation goes to a person in the line’s queue. These endpoints use the same channels permissions as the lines themselves.
Passwords, tokens and keys are never returned by the list: the credentials are shown exactly once, when you create them.
Endpoints
GET/external-bot-connectionsList external botsPOST/external-bot-connectionsCreate an external botPUT/external-bot-connections/{connectionId}Update an external botDELETE/external-bot-connections/{connectionId}Delete an external botPOST/external-bot-connections/{connectionId}/credentialsCreate credentialsDELETE/external-bot-connections/{connectionId}/credentialsRevoke credentialsPOST/external-bot-connections/testTest a webhookThe external bot object
Attributes
idintegerUnique identifier of the external bot connection.namestringName your team recognises it by. Unique per organisation.webhookUrlstringThe URL Natiq posts each customer message (message.created) to.authUsernamestringnullableUsername Natiq sends with HTTP Basic authentication to your webhook.hasAuthPasswordbooleanWhether a Basic password is stored. The password itself is never returned.activebooleanWhether the bot is switched on.hasCredentialsbooleanWhether a Bearer token and key pair exists for the bot to call Natiq with.credentialsCreatedAtstringnullableWhen that pair was created.linesarray<object>The channel lines this bot answers. Read-only: you link a line to a bot with Set who answers a channel.circuitStatestringThe bot’s circuit breaker.OPENmeans it is not answering, so new messages go to agents.HALF_OPENmeans it is being tested with a few messages.One of:CLOSEDOPENHALF_OPENcircuitChangedAtstringnullableWhen the circuit last changed.nullwhile it has never opened.createdAtstringnullableWhen the connection was created.updatedAtstringnullableWhen it was last changed.
{
"id": 6,
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/webhook",
"authUsername": "natiq",
"hasAuthPassword": true,
"active": true,
"hasCredentials": true,
"credentialsCreatedAt": "2026-10-20T08:15:00Z",
"lines": [
{
"id": 22,
"name": "Support — WhatsApp",
"channelType": "WHATSAPP"
}
],
"circuitState": "CLOSED",
"circuitChangedAt": null,
"createdAt": "2026-10-20T08:12:44Z",
"updatedAt": "2026-10-20T08:15:00Z"
}
List external bots
Returns every external bot in your organisation as a plain array, with the lines each one answers.
channels:readReturns
200The external bot object.
curl -X GET "https://api.cx.na6iq.com/api/v1/external-bot-connections" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/external-bot-connections', {
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/external-bot-connections",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
[
{
"id": 6,
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/webhook",
"authUsername": "natiq",
"hasAuthPassword": true,
"active": true,
"hasCredentials": true,
"credentialsCreatedAt": "2026-10-20T08:15:00Z",
"lines": [
{
"id": 22,
"name": "Support — WhatsApp",
"channelType": "WHATSAPP"
}
],
"circuitState": "CLOSED",
"circuitChangedAt": null,
"createdAt": "2026-10-20T08:12:44Z",
"updatedAt": "2026-10-20T08:15:00Z"
}
]
Create an external bot
Registers a bot. It answers no line until you set a line’s routing mode to EXTERNAL_BOT. Create its credentials separately with Create credentials.
channels:writeRequest body
namestringrequiredDisplay name.webhookUrlstringrequiredThe bot’s webhook URL.authUsernamestringnullableBasic authentication username your webhook expects.authPasswordstringnullableBasic authentication password. On update, an empty value keeps the stored password.activebooleanSwitch the bot on or off. On update, omitting it keeps the current state.
Returns
201The external bot object.
Errors
- 400
INVALID_REQUEST`name` is blank or too long, `webhookUrl` is not an absolute http(s) URL, or `authUsername` is sent without `authPassword`. - 400
WEBHOOK_URL_NOT_ALLOWEDThe URL’s domain is not allowed. - 409
EXTERNAL_BOT_NAME_TAKENAn external bot with this name already exists.
curl -X POST "https://api.cx.na6iq.com/api/v1/external-bot-connections" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/webhook",
"authUsername": "natiq",
"authPassword": "<WEBHOOK_PASSWORD>"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/external-bot-connections', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/webhook",
"authUsername": "natiq",
"authPassword": "<WEBHOOK_PASSWORD>"
}),
})
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/external-bot-connections",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/webhook",
"authUsername": "natiq",
"authPassword": "<WEBHOOK_PASSWORD>"
},
)
res.raise_for_status()
data = res.json()
{
"id": 6,
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/webhook",
"authUsername": "natiq",
"hasAuthPassword": true,
"active": true,
"hasCredentials": false,
"credentialsCreatedAt": null,
"lines": [],
"circuitState": "CLOSED",
"circuitChangedAt": null,
"createdAt": "2026-10-20T08:12:44Z",
"updatedAt": "2026-10-20T08:15:00Z"
}
Update an external bot
Changes the bot’s name, webhook, authentication or on/off state. An empty authPassword keeps the stored one; an omitted active keeps the current state.
channels:writePath parameters
connectionIdintegerrequiredThe external bot’sid.
Request body
namestringrequiredDisplay name.webhookUrlstringrequiredThe bot’s webhook URL.authUsernamestringnullableBasic authentication username your webhook expects.authPasswordstringnullableBasic authentication password. On update, an empty value keeps the stored password.activebooleanSwitch the bot on or off. On update, omitting it keeps the current state.
Returns
200The external bot object.
Errors
- 400
INVALID_REQUEST`name` is blank or too long, `webhookUrl` is not an absolute http(s) URL, or `authUsername` is sent without `authPassword`. - 400
WEBHOOK_URL_NOT_ALLOWEDThe URL’s domain is not allowed. - 409
EXTERNAL_BOT_NAME_TAKENAnother external bot already has this name. - 404
EXTERNAL_BOT_NOT_FOUNDNo external bot with this ID in your organisation.
curl -X PUT "https://api.cx.na6iq.com/api/v1/external-bot-connections/42" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/v2/webhook",
"active": true
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/external-bot-connections/42', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/v2/webhook",
"active": true
}),
})
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/external-bot-connections/42",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/v2/webhook",
"active": True
},
)
res.raise_for_status()
data = res.json()
{
"id": 6,
"name": "Al-Noor billing bot",
"webhookUrl": "https://bot.example.com/natiq/v2/webhook",
"authUsername": "natiq",
"hasAuthPassword": true,
"active": true,
"hasCredentials": true,
"credentialsCreatedAt": "2026-10-20T08:15:00Z",
"lines": [
{
"id": 22,
"name": "Support — WhatsApp",
"channelType": "WHATSAPP"
}
],
"circuitState": "CLOSED",
"circuitChangedAt": null,
"createdAt": "2026-10-20T08:12:44Z",
"updatedAt": "2026-10-20T08:15:00Z"
}
Delete an external bot
Deletes the bot. It is refused while any channel line still uses it — switch those lines to another routing mode first.
channels:writePath parameters
connectionIdintegerrequiredThe external bot’sid.
Returns
204The bot is deleted.
Errors
- 409
EXTERNAL_BOT_IN_USEA channel line still uses this bot. - 404
EXTERNAL_BOT_NOT_FOUNDNo external bot with this ID in your organisation.
curl -X DELETE "https://api.cx.na6iq.com/api/v1/external-bot-connections/42" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/external-bot-connections/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/external-bot-connections/42",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
Create credentials
Mints the Bearer token and key your bot uses to call Natiq. This is the only time the raw values exist outside your bot — store them now. Calling it again issues a new pair and the previous one stops working at once.
channels:writePath parameters
connectionIdintegerrequiredThe external bot’sid.
Returns
201The external bot object.
Errors
- 404
EXTERNAL_BOT_NOT_FOUNDNo external bot with this ID in your organisation.
curl -X POST "https://api.cx.na6iq.com/api/v1/external-bot-connections/42/credentials" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/external-bot-connections/42/credentials', {
method: 'POST',
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.post(
"https://api.cx.na6iq.com/api/v1/external-bot-connections/42/credentials",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
{
"apiToken": "<API_TOKEN>",
"apiKey": "<API_KEY>"
}
Revoke credentials
Revokes the bot’s Bearer token and key. The bot can no longer call Natiq until you create a new pair.
channels:writePath parameters
connectionIdintegerrequiredThe external bot’sid.
Returns
204The credentials are revoked.
Errors
- 404
EXTERNAL_BOT_NOT_FOUNDNo external bot with this ID in your organisation.
curl -X DELETE "https://api.cx.na6iq.com/api/v1/external-bot-connections/42/credentials" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/external-bot-connections/42/credentials', {
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/external-bot-connections/42/credentials",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
Test a webhook
Posts a sample message.created event to a webhook exactly as you give it, saved or not, and tells you whether it arrived. Nothing is stored, and the bot’s own reply is never returned. To test a saved bot without retyping its password, pass connectionId and leave authPassword blank — the stored password is used.
channels:writeRequest body
webhookUrlstringrequiredThe URL to post to.authUsernamestringnullableBasic authentication username.authPasswordstringnullableBasic authentication password. Blank uses the stored one whenconnectionIdis set.connectionIdintegernullableA saved bot whose stored password to fall back on.
Returns
200The external bot object.
Errors
- 400
INVALID_REQUESTA username without a password, whether sent or stored. - 400
WEBHOOK_URL_NOT_ALLOWEDThe URL’s domain is not allowed. - 404
EXTERNAL_BOT_NOT_FOUNDNo external bot with this `connectionId` in your organisation.
curl -X POST "https://api.cx.na6iq.com/api/v1/external-bot-connections/test" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://bot.example.com/natiq/webhook",
"authUsername": "natiq",
"authPassword": null,
"connectionId": 6
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/external-bot-connections/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"webhookUrl": "https://bot.example.com/natiq/webhook",
"authUsername": "natiq",
"authPassword": null,
"connectionId": 6
}),
})
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/external-bot-connections/test",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"webhookUrl": "https://bot.example.com/natiq/webhook",
"authUsername": "natiq",
"authPassword": None,
"connectionId": 6
},
)
res.raise_for_status()
data = res.json()
{
"delivered": true,
"statusCode": 200,
"latencyMs": 184,
"error": null
}