An external bot is a chatbot that runs on your servers and answers one or more channel lines instead of a Natiq AI agent. Natiq sends your bot every customer message; your bot replies, or hands the conversation to a person, with three REST calls.
How it fits together
| 1. Inbound | 2. Reply | 3. Handover | |
|---|---|---|---|
| Call | message.created | POST /external-bot/publish | POST /external-bot/transfer |
| Direction | Natiq to your webhook | Your bot to Natiq | Your bot to Natiq |
| Auth | HTTP Basic (optional) | Bearer token and Key | Bearer token and Key |
Your webhook only acknowledges. Do not put the reply in its response; send it with publish. A conversation keeps one conversationId, which you send back unchanged. It ends when an agent resolves it, and the customer's next message starts a new conversation with a new id.
Set it up
Open Connect and select the External bots tab, then Add external bot.
Enter a Name and your Webhook URL. If your endpoint needs a username and password, turn on Basic authentication and fill them in. Select Send test to post a sample event.
Select Create bot. Natiq creates the credentials with the bot and shows the Bearer token and Key once. Copy them to your secret store now.
On the channel that should use the bot, set Who answers to External bot and pick it.
The webhook host must be approved
For safety, Natiq only calls webhook URLs on domains approved for your deployment, each with its subdomains, and never raw IP addresses. Any other URL is refused with WEBHOOK_URL_NOT_ALLOWED. Ask support to add your domain.
Credentials on the bot let you create a new pair (the old pair stops working at once) or revoke it. Both calls use the same pair, and each pair belongs to one bot, which decides which lines and conversations the bot may act on.
Call 1: Natiq posts message.created
POST <your webhook URL>, JSON in UTF-8, with Authorization: Basic … when you configured credentials.
{
"id": "4b2b9f06c1d84e0f9a3b7e21c5d0a9f1",
"type": "message.created",
"eventTime": 1759400000000,
"conversationId": "5",
"channelType": "WHATSAPP_BUSINESS",
"customer": { "id": "970598844052", "name": "Customer Name" },
"line": { "id": "6", "name": "Support", "channelId": "1313858691801404", "phoneNumber": "970593555555" },
"message": {
"id": "1042",
"type": "text",
"channelMessageId": "wamid.HBgM…",
"text": "0",
"createdTime": 1759399999800
}
}| Field | Notes |
|---|---|
id | Event id. It stays the same on retries; use it to drop duplicates. |
test | Present and true on a test event from Send test. Answer 2xx and do not reply. |
conversationId | Send it back on publish and transfer. |
channelType | WHATSAPP_BUSINESS, FACEBOOK, INSTAGRAM, TELEGRAM, WEBCHAT or EMAIL; treat any other value as possible. |
customer.id | The customer's id on the channel (the phone number on WhatsApp). |
line | The Natiq line written to. channelId and phoneNumber may be null. |
message.type | text, postback, location, image, audio, video, document, sticker, contacts or other. |
message.text | The text; for a tapped choice, its title; for files, a caption or [image]. File contents are not sent. |
message.reply | Only for postback: { "id", "title" }, where id is the id you gave the choice. |
message.location | Only for location: latitude, longitude, name, address. |
Ignore fields you do not use; more may be added.
Respond with any 2xx within 3 seconds. Without one, Natiq retries the same event (same id), up to 3 attempts in total, within about 10 seconds of the customer's message (waiting 1 second, then 2). A 4xx other than 408 and 429 is not retried. If it never gets through, Natiq sends the customer a short fallback message once and hands the conversation to an agent with reason BOT_UNAVAILABLE. Once you answer 2xx the conversation is yours; Natiq sets no reply deadline.
Circuit breaker. If half of your bot's last 20 calls fail or take longer than 2 seconds (judged once there have been at least 10), Natiq stops calling it for 30 seconds and hands those customers straight to agents with the BOT_UNAVAILABLE fallback, then tries 3 test calls before resuming. A failure is no response or a 5xx; 4xx and 429 answers count as up. Supervisors are alerted when this happens.
A customer who types 1 instead of tapping a choice arrives as plain text. Accept the number, the title and reply.id for every choice.
Call 2: publish a reply
POST https://api.cx.na6iq.com/api/v1/external-bot/publish (Saudi region: api.sa.na6iq.com)
curl -X POST https://api.cx.na6iq.com/api/v1/external-bot/publish \
-H "Authorization: Bearer $NATIQ_BOT_TOKEN" \
-H "Key: $NATIQ_BOT_KEY" \
-H 'Content-Type: application/json' \
-H 'Request-Id: 4b2b9f06c1d84e0f9a3b7e21c5d0a9f1-1' \
-d '{
"conversationId": "5",
"type": "selection",
"text": "Choose a service",
"choices": [
{ "id": "BALANCE", "title": "My balance" },
{ "id": "AGENT", "title": "Talk to a person" }
]
}'await fetch('https://api.cx.na6iq.com/api/v1/external-bot/publish', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_BOT_TOKEN}`,
Key: process.env.NATIQ_BOT_KEY,
'Content-Type': 'application/json',
'Request-Id': `${event.id}-1`,
},
body: JSON.stringify({ conversationId: event.conversationId, type: 'text', text: 'Hello!' }),
})requests.post(
"https://api.cx.na6iq.com/api/v1/external-bot/publish",
headers={
"Authorization": f"Bearer {token}",
"Key": key,
"Request-Id": f"{event['id']}-1",
},
json={"conversationId": event["conversationId"], "type": "text", "text": "Hello!"},
timeout=30,
)| Field | Required | Notes |
|---|---|---|
conversationId | yes | From the event. |
type | yes | text, or selection for a menu. selection needs choices; text must not have them. |
text | yes | Required. WhatsApp caps a message at 4096 characters. With choices, the prompt above them. |
choices | no | Up to 13 of { id, title, description? }. id is required, unique and at most 200 characters; title at most 20; description at most 72. |
links | no | Up to 3 of { title, url }, with an http or https url. |
style | no | auto (default), buttons or list: a hint where a channel offers both. |
Request-Id makes a retry safe: the same value sends nothing twice and returns the original result with "duplicate": true. Use one value per reply.
Response 202 Accepted: { "messageId": 21, "conversationId": 5, "duplicate": false }. That means Natiq queued the message, not that the customer has it. WhatsApp only accepts free-form replies within 24 hours of the customer's last message; a reply sent outside that window is refused with 409 TEMPLATE_REQUIRED.
Natiq draws choices with what each channel offers (reply buttons or a list on WhatsApp, quick replies on Messenger and Instagram, inline keys on Telegram, chips on Web Chat) and falls back to numbered text where a channel cannot draw them.
Call 3: hand over to a person
curl -X POST https://api.cx.na6iq.com/api/v1/external-bot/transfer \
-H "Authorization: Bearer $NATIQ_BOT_TOKEN" -H "Key: $NATIQ_BOT_KEY" \
-H 'Content-Type: application/json' \
-d '{"conversationId":"5","reason":"CUSTOMER_ASKED_FOR_AGENT","variables":{"serviceNumber":"0599000000"}}'reason (default TRANSFER) and variables (text pairs; use serviceNumber for the customer's service number) appear on the ticket for the agent. inboxTags is optional, an array of strings: the tags of the inbox to hand the conversation to. Omitted or empty, the conversation goes to the line's default inbox. Response: { "ticketId": 2, "conversationId": 5, "alreadyTransferred": false }. The call is safe to repeat. After it, your bot gets no more events for the conversation.
Other calls
| Call | Use |
|---|---|
GET /api/v1/external-bot/conversations/{conversationId} | Where a conversation stands: state is NEW (your bot has it), WAITING, ASSIGNED or CLOSED. Only NEW accepts a publish. |
GET /api/v1/external-bot/lines | The lines routed to your bot. |
POST /api/v1/external-bot/outbound | Start a conversation with an approved WhatsApp template (lineId, to, template with name, language, parameters). Request-Id is required. We have not listed its error codes; check responses. |
Errors
All calls use the standard error envelope.
| Status | Code | What to do |
|---|---|---|
400 | INVALID_REQUEST | Fix the field named in error.message. Nothing was sent. |
401 | EXTERNAL_BOT_CREDENTIALS_MISSING or _INVALID | Send both headers; check the pair is current. |
404 | CONVERSATION_NOT_FOUND | Unknown id, or not your bot's conversation. |
409 | CONVERSATION_CLOSED | Stop. The next message starts a new conversation. |
409 | NOT_BOT_OWNED | A person has the conversation. Stop. |
409 | TEMPLATE_REQUIRED | WhatsApp 24-hour window has passed. |
429 | RATE_LIMITED | More than 200 publish/transfer calls per second for this bot (10 per second for outbound; your organisation may have a different limit). Wait for Retry-After. |
The management endpoints (creating bots, credentials, tests) are in the external bots API reference.