Channel Connection API
This guide shows you how to connect messaging channels to an account using the API. It is written for a developer building an integration or wrapper, so it focuses on the exact requests, the order to make them in, and the responses you get back.
There is one pattern you need to understand up front, because it applies to almost every channel here.
The connect-then-poll pattern
Most channels cannot be connected with a single API call. Connecting WhatsApp, Instagram, or Messenger means the account holder has to log in to their own provider account and approve access. There is no headless (fully automated) path for that approval - a real person has to open a URL in a browser, or scan a QR code with their phone.
So the flow is always:
- Start the connection with a
POST. The response gives you either a URL to open, or a QR code to display. - Hand that off to the end user - open the URL in their browser, or render the QR code on screen for them to scan.
- Poll the status endpoint with
GETon a short interval (every few seconds) until the status reaches a connected state.
Your integration’s job is to drive that loop: show the URL or QR, then poll until done. Plan your UI around the poll - a spinner with a “waiting for you to finish in your browser” message works well.
Note: Before you start, make sure API access is enabled on the plan and you have an API key. See API Access for how to generate one. All requests below use the base URL https://api.youraiconnector.com/v1 and you must authenticate every request. See Authentication for the four accepted forms - the examples here use the X-API-Key header, with one cURL example per page showing the simpler ?apiKey= query form.
Instagram + Messenger (Meta)
Instagram and Messenger are connected together in one flow, because they both run on a Facebook Page. The account holder authorizes through Facebook, you fetch the list of Pages they manage, and you pick which Page to connect.
Step 1 - Start the Instagram + Messenger connection
POST /channels/meta/connect
This returns a consent URL. No credentials are sent in this request - the connection is authorized entirely in the browser.
cURL
curl -X POST "https://api.youraiconnector.com/v1/channels/meta/connect?apiKey=YOUR_API_KEY"
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/channels/meta/connect", {
method: "POST",
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
// Open data.oauth_url in the end user's browser.
Python
import requests
res = requests.post(
"https://api.youraiconnector.com/v1/channels/meta/connect",
headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
# Open data["oauth_url"] in the end user's browser.
Response
{
"success": true,
"oauth_url": "https://www.facebook.com/v21.0/dialog/oauth?client_id=...&state=...",
"state_token": "opaque-one-time-token",
"connect_url": "https://api.youraiconnector.com/v1/channels/meta/connect/page?token=eyJhbGciOi...",
"connect_url_expires_at": 1717000000000,
"expires_at": "2026-06-10T12:30:00.000Z"
}
Open oauth_url in the end user’s browser so they can log in to Facebook and approve access. The connection attempt expires at expires_at (about 30 minutes) - if it lapses, start over. Treat state_token as a short-lived secret and do not log it.
Easiest option for Instagram + Messenger: hand over connect_url
The response also includes a ready-made connect_url: a hosted page that runs the whole flow for the account holder. They open it, log in to Facebook, and when they have more than one Page it shows the list and lets them pick which one to connect - then it reports success on its own. Give this link to the account holder instead of opening oauth_url yourself, building a Page picker, and polling. The link works for about 30 minutes (connect_url_expires_at); if it lapses, start a new connection. The manual steps below are for integrations that want to drive the flow and render the Page picker themselves.
Step 2 - Poll the status until pages load
GET /channels/meta/status
After the user finishes the Facebook login, poll this endpoint every few seconds. The status field walks through these steps:
status |
Meaning |
|---|---|
pending |
Consent not completed yet. Keep waiting. |
token_received |
Authorized, but the list of Pages is still loading. |
pages_loaded |
Pages are available - move to step 3. |
connected |
A Page has been selected and the channel is live. |
cURL
curl "https://api.youraiconnector.com/v1/channels/meta/status" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/channels/meta/status", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
// Poll until data.status === "pages_loaded".
Python
res = requests.get(
"https://api.youraiconnector.com/v1/channels/meta/status",
headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
# Poll until data["status"] == "pages_loaded".
Response (once pages have loaded)
{
"success": true,
"status": "pages_loaded",
"pages": [
{
"id": "1234567890",
"name": "My Business Page",
"category": "Local business",
"instagram_business_account": {
"id": "17890000000000000",
"username": "mybusiness"
}
}
],
"selected_page": null
}
Step 3 - List the pages (optional)
If you’d rather fetch the Page list on its own (for example, to render a picker), use:
GET /channels/meta/pages
curl "https://api.youraiconnector.com/v1/channels/meta/pages" \
-H "X-API-Key: YOUR_API_KEY"
It returns the same pages array as the status endpoint. (The status endpoint already includes the pages, so this call is just a convenience.)
Step 4 - Select the page to connect
POST /channels/meta/select-page
Send the page_id of the Page the user chose. The Instagram account linked to that Page is connected automatically; you only need the instagram object if you want to override which Instagram account to use.
cURL
curl -X POST "https://api.youraiconnector.com/v1/channels/meta/select-page" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "page_id": "1234567890" }'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/channels/meta/select-page", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ page_id: "1234567890" }),
});
const data = await res.json();
Python
res = requests.post(
"https://api.youraiconnector.com/v1/channels/meta/select-page",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"page_id": "1234567890"},
)
data = res.json()
Response
{
"success": true,
"page_id": "1234567890",
"instagram_business_account_id": "17890000000000000"
}
The channel is now connected. A follow-up GET /channels/meta/status will report status: "connected".
List the connected page’s posts
GET /channels/meta/posts?platform=instagram
Returns the recent posts of the page you connected - Instagram media or Facebook posts. This is what you render a picker from when you set up an Entry Point that reacts to comments on one specific post.
| Query parameter | Required | Description |
|---|---|---|
platform |
Yes | instagram or facebook. Anything else returns a 400. |
limit |
No | How many posts to return, 1-50. Defaults to 25. |
after |
No | Cursor for the next page - pass the nextCursor value from the previous response. |
cURL
curl "https://api.youraiconnector.com/v1/channels/meta/posts?platform=instagram&limit=25" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"connected": true,
"platform": "instagram",
"posts": [
{
"id": "17900000000000000",
"caption": "New spring menu is live",
"thumbnailUrl": "https://scontent.cdninstagram.com/...",
"permalink": "https://www.instagram.com/p/Cxxxxxxxxxx/",
"createdAt": "2026-05-02T09:12:00.000Z",
"mediaType": "REELS"
}
],
"nextCursor": "QVFIUkxxxxxxxx"
}
mediaType is Instagram’s own label (REELS, FEED, STORY, or the format - IMAGE, VIDEO, CAROUSEL_ALBUM); for Facebook it is always POST. nextCursor is null on the last page.
If nothing can be listed the call still returns 200 with connected: false and an empty posts array, plus a reason telling you why:
reason |
What to do |
|---|---|
| (absent) | No page is connected yet - run the connect flow first. |
no_instagram_account |
A Facebook Page is connected but no Instagram business account is linked to it. Facebook posts still list fine. |
token_expired |
The stored page credential no longer works - reconnect the channel. |
Disconnect Instagram + Messenger
DELETE /channels/meta
curl -X DELETE "https://api.youraiconnector.com/v1/channels/meta" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "disconnected": true }
This stops inbound routing for both Instagram and Messenger. It is idempotent - calling it when nothing is connected still succeeds.
WhatsApp Business
This connects an official WhatsApp Business number. The number must already exist on the account before you call connect. Like Meta, the account holder authorizes in their browser, then you poll until the number reports ONLINE.
Step 1 - Start the WhatsApp Business connection
POST /channels/whatsapp/connect
cURL
curl -X POST "https://api.youraiconnector.com/v1/channels/whatsapp/connect?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone_number": "+14155551234" }'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/channels/whatsapp/connect", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ phone_number: "+14155551234" }),
});
const data = await res.json();
// Open data.oauth_url in the account holder's browser.
Python
res = requests.post(
"https://api.youraiconnector.com/v1/channels/whatsapp/connect",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"phone_number": "+14155551234"},
)
data = res.json()
# Open data["oauth_url"] in the account holder's browser.
| Field | Required | Description |
|---|---|---|
phone_number |
Yes | The number to connect, in E.164 format (e.g. +14155551234). |
only_waba_sharing |
No | Restrict the authorization to sharing an existing WhatsApp Business Account, skipping new sender setup. Defaults to false. |
retry |
No | Re-run authorization for a number whose previous attempt did not complete. Defaults to false. |
business_name |
No | Cosmetic override for the business name shown on the consent screen only (max 256 chars). Not stored. |
description |
No | Cosmetic override for the business description shown on the consent screen only (max 256 chars). Not stored. |
Response
{
"success": true,
"status": "pending",
"oauth_url": "https://www.facebook.com/v21.0/dialog/oauth?client_id=...&state=...",
"state_token": "opaque-one-time-token",
"expires_at": "2026-06-10T12:30:00.000Z"
}
Open oauth_url in the account holder’s browser to authorize. Once they approve, registration completes in the background.
Step 2 - Poll the status until ONLINE
GET /channels/whatsapp/connect/{phoneNumber}/status
Poll this until status is ONLINE.
cURL
curl "https://api.youraiconnector.com/v1/channels/whatsapp/connect/+14155551234/status" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const phone = encodeURIComponent("+14155551234");
const res = await fetch(
`https://api.youraiconnector.com/v1/channels/whatsapp/connect/${phone}/status`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
// Poll until data.status === "ONLINE".
Python
import urllib.parse
phone = urllib.parse.quote("+14155551234")
res = requests.get(
f"https://api.youraiconnector.com/v1/channels/whatsapp/connect/{phone}/status",
headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
# Poll until data["status"] == "ONLINE".
Response
{
"success": true,
"phone_number": "+14155551234",
"channel": "whatsapp",
"status": "ONLINE",
"status_reason": null,
"live": true
}
The status field can be:
status |
Meaning |
|---|---|
PENDING |
Authorized, approval still in progress. Keep polling. |
ONLINE |
Connected and ready to send. |
RATE_LIMITED |
Too many attempts - wait before retrying. |
REGISTRATION_FAILED |
Setup could not be completed. |
DELETED |
The registration no longer exists. |
live: true means the status was checked against the provider in real time; false means it came from the last cached state.
Disconnect a WhatsApp Business number
DELETE /channels/whatsapp/{phoneNumber}
curl -X DELETE "https://api.youraiconnector.com/v1/channels/whatsapp/+14155551234" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "phone_number": "+14155551234", "disconnected": true }
The number itself stays on the account, so you can reconnect it later.
WhatsApp Web
WhatsApp Web links a regular WhatsApp number by scanning a QR code, just like linking a device in the WhatsApp app. The flow is: start the session, fetch the QR code and show it, then poll until the status is connected.
Step 1 - Start a WhatsApp Web pairing session
POST /channels/whatsapp-web/connections
cURL
curl -X POST "https://api.youraiconnector.com/v1/channels/whatsapp-web/connections?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone_number": "+15551230000" }'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/channels/whatsapp-web/connections", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ phone_number: "+15551230000" }),
});
const data = await res.json();
Python
res = requests.post(
"https://api.youraiconnector.com/v1/channels/whatsapp-web/connections",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"phone_number": "+15551230000"},
)
data = res.json()
| Field | Required | Description |
|---|---|---|
phone_number |
Yes | The WhatsApp number to connect, in E.164 format. |
proxy_country |
No | ISO 3166-1 alpha-2 country code for the routing region. Auto-detected from the number when omitted. |
force_new |
No | Discard any existing session and start fresh pairing. Defaults to false. |
import_contacts |
No | Import the device’s existing contacts on first connection. Defaults to false. |
pause_ai_for_imported_contacts |
No | When importing contacts, keep automated replies paused for them. Defaults to true. |
import_existing_chats |
No | Import existing chat history (requires import_contacts: true). Defaults to false. |
Response
{
"success": true,
"phone_number": "+15551230000",
"session_id": "session-id",
"status": "qr_pending",
"connect_url": "https://api.youraiconnector.com/v1/channels/whatsapp-web/connect?token=eyJhbGciOi...",
"connect_url_expires_at": 1717000000000,
"poll_qr_path": "/v1/channels/whatsapp-web/connections/%2B15551230000/qr",
"poll_status_path": "/v1/channels/whatsapp-web/connections/%2B15551230000/status"
}
Easiest option for WhatsApp Web: hand over connect_url
The response includes a ready-made connect_url: a hosted page that shows the QR code, refreshes it automatically as it rotates, and switches to a success message the moment the number is linked. Just give this link to the account holder (open it in a browser, send it to them, or show it as a QR/button) and have them scan it with WhatsApp - you don’t need to fetch the QR or poll anything yourself. The link works for about 30 minutes (connect_url_expires_at); if it lapses before they finish, start a new connection to get a fresh one.
This is the recommended path when a person can open a link. The manual steps below (fetch the QR yourself, poll the status) are for integrations that want to render the QR inside their own interface instead.
The response also hands you the exact poll_qr_path and poll_status_path to use, so you don’t have to build them yourself.
Step 2 - Fetch the QR code and show it
GET /channels/whatsapp-web/connections/{phoneNumber}/qr
cURL
curl "https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/+15551230000/qr" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const phone = encodeURIComponent("+15551230000");
const res = await fetch(
`https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/${phone}/qr`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
// Render data.qr_data_url as an <img src> for the user to scan.
Python
import urllib.parse
phone = urllib.parse.quote("+15551230000")
res = requests.get(
f"https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/{phone}/qr",
headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
# Render data["qr_data_url"] for the user to scan.
Response
{
"success": true,
"phone_number": "+15551230000",
"status": "qr_pending",
"qr_code": "2@raw-qr-payload-string...",
"qr_data_url": "data:image/png;base64,iVBORw0KGgo...",
"expires_at": "2026-06-10T12:05:00.000Z"
}
Render the QR for the user to scan with their phone (WhatsApp > Linked Devices > Link a Device):
qr_data_urlis a ready-to-use image - drop it straight into an<img src>.qr_codeis the raw payload if you’d rather generate the image yourself.
The QR is short-lived. If you call this right after starting the session you may get a 404 with “QR code not available yet” - just wait a moment and retry. If you get a 410 (“QR code expired”), start the connection over to get a fresh code.
Step 3 - Poll the status until connected
GET /channels/whatsapp-web/connections/{phoneNumber}/status
cURL
curl "https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/+15551230000/status" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const phone = encodeURIComponent("+15551230000");
const res = await fetch(
`https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/${phone}/status`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
// Poll until data.status === "connected" (or "open").
Python
import urllib.parse
phone = urllib.parse.quote("+15551230000")
res = requests.get(
f"https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/{phone}/status",
headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
# Poll until data["status"] == "connected" (or "open").
Response
{
"success": true,
"phone_number": "+15551230000",
"status": "connected",
"has_qr": false,
"qr_expires_at": null,
"last_activity": null,
"message_count": null,
"proxy": null,
"live": true
}
status |
Meaning |
|---|---|
not_initialized |
No session yet (terminal failure). |
qr_pending |
Waiting for the QR to be scanned. |
connecting |
Scanned, finishing setup. |
connected / open |
Linked and live - this is success. |
disconnected |
Session ended (terminal failure). |
Disconnect a WhatsApp Web session
DELETE /channels/whatsapp-web/connections/{phoneNumber}
curl -X DELETE "https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/+15551230000" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "phone_number": "+15551230000", "status": "removed" }
This unlinks the device and removes the connection. It always cleans up local state, so it is idempotent even if the underlying session was already gone.
Telegram
Availability: Telegram connects like any other channel and is open to every account — you do not need it switched on for you. The Telegram endpoints below can still return
403if Telegram is not included in the account’s plan, in which case the error reads"This channel is not included in your current plan. Upgrade to unlock it.".
Telegram connects a personal account by phone number plus a one-time login code (and a two-factor password, if the account has one set). The flow is: start the session, submit the code, optionally submit the password, then confirm via status.
Step 1 - Start a Telegram connection session
POST /channels/telegram/connect
cURL
curl -X POST "https://api.youraiconnector.com/v1/channels/telegram/connect?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone_number": "+14155550100" }'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/channels/telegram/connect", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ phone_number: "+14155550100" }),
});
const data = await res.json();
Python
res = requests.post(
"https://api.youraiconnector.com/v1/channels/telegram/connect",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"phone_number": "+14155550100"},
)
data = res.json()
| Field | Required | Description |
|---|---|---|
phone_number |
Yes | The account phone number to connect, in E.164 format. |
mode |
No | code (default) sends a one-time login code to the account; qr returns a login token and QR URL to display. |
proxy_country |
No | ISO 3166-1 alpha-2 country code for the outbound network route. |
force_new |
No | When true, discards any existing session and starts fresh. |
Response
{
"success": true,
"phone_number": "+14155550100",
"status": "code_required",
"session_id": "session-id",
"connect_url": "https://api.youraiconnector.com/v1/channels/telegram/connect/page?token=eyJhbGciOi...",
"connect_url_expires_at": 1717000000000
}
In code mode the account receives a login code in Telegram and status is code_required. (In qr mode the response also includes login_token and qr_url to display for scanning, and status is qr_required.)
Easiest option for Telegram: hand over connect_url
The response includes a ready-made connect_url: a hosted page that finishes the connection on its own. In code mode the account holder enters the login code - and a two-step verification password if their account has one. In qr mode the page shows a QR that refreshes itself for them to scan from the Telegram app. Either way it reports success on its own, so you can just give this link to the account holder instead of building your own UI and polling. The link works for about 30 minutes (connect_url_expires_at); if it lapses, start a new connection to get a fresh one.
The manual steps below (collect the code yourself, submit it, poll the status; or render qr_url and poll) are for integrations that want to render the UI themselves.
Step 2 - Submit the login code
POST /channels/telegram/connect/{phoneNumber}/verify-code
cURL
curl -X POST "https://api.youraiconnector.com/v1/channels/telegram/connect/+14155550100/verify-code" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "12345" }'
JavaScript
const phone = encodeURIComponent("+14155550100");
const res = await fetch(
`https://api.youraiconnector.com/v1/channels/telegram/connect/${phone}/verify-code`,
{
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ code: "12345" }),
}
);
const data = await res.json();
Python
import urllib.parse
phone = urllib.parse.quote("+14155550100")
res = requests.post(
f"https://api.youraiconnector.com/v1/channels/telegram/connect/{phone}/verify-code",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"code": "12345"},
)
data = res.json()
Response
{
"success": true,
"phone_number": "+14155550100",
"status": "connected",
"telegram_user_id": "100000001",
"username": "myhandle"
}
If status is connected, you’re done. If the account has two-factor enabled, status will be password_required instead - go to step 3.
Step 3 - Submit the two-factor password (only if needed)
POST /channels/telegram/connect/{phoneNumber}/verify-password
Only call this when step 2 returned password_required.
cURL
curl -X POST "https://api.youraiconnector.com/v1/channels/telegram/connect/+14155550100/verify-password" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "password": "the-2fa-password" }'
JavaScript
const phone = encodeURIComponent("+14155550100");
const res = await fetch(
`https://api.youraiconnector.com/v1/channels/telegram/connect/${phone}/verify-password`,
{
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ password: "the-2fa-password" }),
}
);
const data = await res.json();
Python
import urllib.parse
phone = urllib.parse.quote("+14155550100")
res = requests.post(
f"https://api.youraiconnector.com/v1/channels/telegram/connect/{phone}/verify-password",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"password": "the-2fa-password"},
)
data = res.json()
Response
{
"success": true,
"phone_number": "+14155550100",
"status": "connected",
"telegram_user_id": "100000001",
"username": "myhandle"
}
Check Telegram status
GET /channels/telegram/connect/{phoneNumber}/status
curl "https://api.youraiconnector.com/v1/channels/telegram/connect/+14155550100/status" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"phone_number": "+14155550100",
"status": "connected",
"telegram_user_id": "100000001",
"live": true
}
status can be connected, code_required, password_required, initializing, disconnected, not_initialized, or error.
Disconnect Telegram
DELETE /channels/telegram/{phoneNumber}
curl -X DELETE "https://api.youraiconnector.com/v1/channels/telegram/+14155550100" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "phone_number": "+14155550100", "status": "removed" }
Idempotent - repeated calls succeed.
Instagram (personal account)
Limited-availability beta, enabled per account. This connects a personal Instagram account by logging in with its username and password (not the official Business API). If the account is not enabled for the beta, the connect call returns a permission error.
Because this needs the account holder’s own Instagram login, the simplest path is to hand them the hosted connect_url and let them enter their credentials there - your integration never handles the password.
Step 1 - Start an Instagram (personal) connection
POST /channels/instagram-private/connect
Send the Instagram username and password.
Response
{
"success": true,
"status": "connected",
"connect_url": "https://api.youraiconnector.com/v1/channels/instagram-private/connect/page?token=eyJhbGciOi...",
"connect_url_expires_at": 1717000000000
}
If the account has two-factor authentication or Instagram presents a checkpoint, status comes back as two_factor_required or challenge_required - submit the code to /connect/{id}/verify-2fa or /connect/{id}/verify-challenge below, then poll /connect/{id}/status until connected. {id} is the normalized Instagram username returned as account_id/username in the response above - use it on every step below.
Step 2 - Submit the two-factor code (if asked for)
POST /channels/instagram-private/connect/{id}/verify-2fa
Only call this when step 1 (or step 3) returned two_factor_required.
curl -X POST "https://api.youraiconnector.com/v1/channels/instagram-private/connect/yourbrand/verify-2fa" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "123456" }'
Response
{
"success": true,
"account_id": "yourbrand",
"status": "connected",
"ig_user_id": "17890000000000000",
"username": "yourbrand"
}
status can come back connected (done), two_factor_required (wrong code, try again), or challenge_required (Instagram also wants a checkpoint code - go to step 3).
Step 3 - Submit the checkpoint confirmation code (if asked for)
POST /channels/instagram-private/connect/{id}/verify-challenge
Only call this when a previous step returned challenge_required. Same request and response shape as step 2 above.
curl -X POST "https://api.youraiconnector.com/v1/channels/instagram-private/connect/yourbrand/verify-challenge" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "123456" }'
Check Instagram (personal) status
GET /channels/instagram-private/connect/{id}/status
Poll this until status is connected, or until it reports a terminal failure.
curl "https://api.youraiconnector.com/v1/channels/instagram-private/connect/yourbrand/status" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"account_id": "yourbrand",
"status": "connected",
"ig_user_id": "17890000000000000",
"username": "yourbrand",
"live": true
}
status can be connected, two_factor_required, challenge_required, initializing, disconnected, not_initialized, or error. live: true means this was read live from the connection worker rather than a cached value.
Easiest option for Instagram (personal): hand over connect_url
The response includes a connect_url: a hosted page where the account holder enters their Instagram username and password (and a 2FA or checkpoint code if Instagram asks for one), and which reports success on its own. The credentials go straight to Instagram and are not stored. Give this link to the account holder instead of collecting their password in your own UI. The link works for about 30 minutes (connect_url_expires_at).
Disconnect Instagram (personal)
DELETE /channels/instagram-private/{id}
Idempotent - repeated calls succeed.
Sync followers
POST /channels/instagram-private/{id}/sync-followers
Manually triggers a follower sync for a connected account - the same job that runs automatically in the background, exposed here for an on-demand “Refresh followers” action. It fetches the account’s current follower list, records anyone new, and (when a Live campaign has follower outreach turned on) sends new followers an opening DM, up to a daily cap.
curl -X POST "https://api.youraiconnector.com/v1/channels/instagram-private/yourbrand/sync-followers" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"accountId": "yourbrand",
"totalFollowers": 1204,
"newFollowers": 6,
"dmsSent": 6,
"isBaselineSeed": false
}
These five fields are the one place on this page that come back
camelCaseinstead ofsnake_case- that’s how this endpoint is wired today, not a typo.isBaselineSeed: truemeans this was the very first sync after connecting, which only records the starting follower list and never sends outreach DMs (sodmsSentis always0on that run).
The very first call for an account can take a while (walking the full follower list); later calls are faster since only new followers get diffed. 404 means the account isn’t connected; 412 means the connection hasn’t finished initializing yet - wait and retry.
LINE
LINE is the simplest channel to connect because there is no browser redirect or polling. The customer creates a Messaging API channel in the LINE Developers console, copies two values, and you submit them in a single call. You then give them back a webhook URL to paste into the console.
Step 1 - Connect with the channel credentials
POST /channels/line
cURL
curl -X POST "https://api.youraiconnector.com/v1/channels/line?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel_access_token": "LONG_LIVED_CHANNEL_ACCESS_TOKEN",
"channel_secret": "CHANNEL_SECRET"
}'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/channels/line", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
channel_access_token: "LONG_LIVED_CHANNEL_ACCESS_TOKEN",
channel_secret: "CHANNEL_SECRET",
}),
});
const data = await res.json();
Python
res = requests.post(
"https://api.youraiconnector.com/v1/channels/line",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"channel_access_token": "LONG_LIVED_CHANNEL_ACCESS_TOKEN",
"channel_secret": "CHANNEL_SECRET",
},
)
data = res.json()
| Field | Required | Description |
|---|---|---|
channel_access_token |
Yes | The Official Account’s long-lived Messaging API channel access token. Used to send and receive messages. |
channel_secret |
Yes | The Messaging API channel secret, used to verify inbound event signatures. |
channel_id |
No | The numeric channel id. Informational only. |
Response
{
"success": true,
"status": "connected",
"bot_user_id": "Uxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"basic_id": "@mybusiness",
"display_name": "My Business",
"picture_url": "https://...",
"chat_mode": "bot",
"chat_mode_ok": true,
"webhook_url": "https://api.youraiconnector.com/line/webhook/..."
}
Two fields matter for what you do next:
webhook_url- the customer must paste this into the Webhook URL field of their LINE channel in the LINE Developers console (and enable “Use webhook”). Until they do, no inbound messages arrive. Show this to them prominently.chat_mode_ok- whenfalse, the Official Account is in “chat” mode and will not receive or send messages until it is switched to “bot” mode in the LINE Official Account Manager. Gate your onboarding on this flag and tell the customer to switch the mode.
The
channel_access_tokenandchannel_secretare never returned by any endpoint. Store them on your side if you need them again; otherwise re-paste from the LINE console.
The bot_user_id returned here is the connection identifier you use in the status, verify, and disconnect calls below.
Step 2 - Re-verify after webhook setup
POST /channels/line/{botUserId}/verify-webhook
After the customer finishes configuring the webhook URL and switches to bot mode, call this to re-validate the stored token and refresh the cached chat mode.
curl -X POST "https://api.youraiconnector.com/v1/channels/line/Uxxxxxxxx.../verify-webhook" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"token_valid": true,
"chat_mode": "bot",
"chat_mode_ok": true,
"webhook_url": "https://api.youraiconnector.com/line/webhook/..."
}
If token_valid is false, the stored access token no longer authenticates - have the customer reissue it in the console and call POST /channels/line again with the new token.
Check LINE status
GET /channels/line/{botUserId}/status
curl "https://api.youraiconnector.com/v1/channels/line/Uxxxxxxxx.../status" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"bot_user_id": "Uxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"channel": "line",
"status": "connected",
"basic_id": "@mybusiness",
"display_name": "My Business",
"picture_url": "https://...",
"chat_mode": "bot",
"is_active": true,
"live": false
}
LINE has no live status feed, so live is always false here - the values reflect the state captured at connect (or last verify) time.
Disconnect LINE
DELETE /channels/line/{botUserId}
curl -X DELETE "https://api.youraiconnector.com/v1/channels/line/Uxxxxxxxx..." \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "status": "removed", "bot_user_id": "Uxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
Viber
Viber connects the same way LINE does - paste the bot’s auth token from the Viber Admin Panel in one call - with one difference worth knowing: connecting also REGISTERS our webhook on your bot right then, so there’s no separate console step afterward. That also means a connect attempt can fail if our ingress can’t answer Viber’s synchronous webhook check, not only if the token itself is wrong.
Step 1 - Connect with the bot’s auth token
POST /channels/viber
| Field | Required | Description |
|---|---|---|
auth_token |
Yes | The bot’s auth token, from the Viber Admin Panel (My Bot Settings). |
curl -X POST "https://api.youraiconnector.com/v1/channels/viber?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "auth_token": "444d5555e6666f7777a8888b9999c000" }'
Response
{
"success": true,
"status": "connected",
"bot_id": "botIdFromViber",
"bot_name": "My Business Bot",
"bot_avatar": "https://...",
"bot_uri": "mybusinessbot",
"subscribers_count": 0,
"webhook_url": "https://api.youraiconnector.com/v1/incoming-viber-message/...",
"event_types": ["delivered", "seen", "failed", "subscribed", "unsubscribed", "conversation_started"]
}
The auth token is never echoed back by any endpoint - store it on your side if you’ll need to re-paste it. bot_id is the connection identifier used by the status, verify, and disconnect calls below.
Check Viber status
GET /channels/viber/{botId}/status
Reports the stored connection state. Add ?live=true to also re-check the bot against Viber and refresh the cached webhook registration - useful before assuming a silent bot is actually broken.
curl "https://api.youraiconnector.com/v1/channels/viber/botIdFromViber/status?live=true" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"bot_id": "botIdFromViber",
"channel": "viber",
"status": "connected",
"bot_name": "My Business Bot",
"bot_avatar": "https://...",
"bot_uri": "mybusinessbot",
"webhook_url": "https://api.youraiconnector.com/v1/incoming-viber-message/...",
"registered_webhook": "https://api.youraiconnector.com/v1/incoming-viber-message/...",
"webhook_ok": true,
"subscribers_count": 128,
"is_active": true,
"live": true
}
webhook_ok: false means the bot’s webhook no longer points at us - inbound messages are dead. This usually means another tool connected the same bot afterward (Viber’s webhook registration is last-write-wins). Fix it with the re-verify call below, no need to ask the customer to re-paste their token. live is false when the response is the last cached state rather than a fresh check against Viber.
Re-register the webhook
POST /channels/viber/{botId}/verify-webhook
The repair action for webhook_ok: false - re-registers our webhook on the bot using the already-stored auth token.
curl -X POST "https://api.youraiconnector.com/v1/channels/viber/botIdFromViber/verify-webhook" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "token_valid": true, "webhook_ok": true, "webhook_url": "https://api.youraiconnector.com/v1/incoming-viber-message/...", "event_types": ["delivered", "seen", "failed", "subscribed", "unsubscribed", "conversation_started"] }
token_valid: false means the stored token no longer works - reconnect with POST /channels/viber and a fresh token.
Disconnect Viber
DELETE /channels/viber/{botId}
Unregisters our webhook on Viber’s side (best-effort) and removes the connection.
curl -X DELETE "https://api.youraiconnector.com/v1/channels/viber/botIdFromViber" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "status": "removed", "bot_id": "botIdFromViber", "webhook_removed": true }
TikTok
Availability: Limited-availability beta, enabled per account. Connecting TikTok returns a permission error until the account is enabled for it.
TikTok Business Messaging is a full OAuth channel like Meta, but simpler on the polling side: there’s no dedicated status-polling step to build against, because the connected account shows up on its own once TikTok redirects back and the connection is written. The status endpoint below exists for confirming state on demand (support tooling, health checks), not as something you need to loop on during connect.
Step 1 - Start the TikTok connection
POST /channels/tiktok/connect
Takes no credentials - the account holder authorizes entirely in their browser.
curl -X POST "https://api.youraiconnector.com/v1/channels/tiktok/connect?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"status": "pending_authorization",
"oauth_url": "https://www.tiktok.com/v2/auth/authorize?client_key=...&state=...",
"state_token": "opaque-one-time-token",
"expires_at": "2026-06-10T12:30:00.000Z"
}
Open oauth_url in the account holder’s browser so they can log in to TikTok and approve access. The state expires at expires_at (about 30 minutes) - if it lapses, start over. There is no connect_url hosted-page shortcut for TikTok; opening oauth_url yourself is the only path.
Check TikTok status
GET /channels/tiktok/{openId}/status
openId is the TikTok Business Account’s open_id, known once the OAuth callback has run.
curl "https://api.youraiconnector.com/v1/channels/tiktok/openIdFromTikTok/status" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"open_id": "openIdFromTikTok",
"channel": "tiktok",
"status": "connected",
"business_id": "openIdFromTikTok",
"username": "mybusiness",
"display_name": "My Business",
"avatar_url": "https://...",
"status_reason": null,
"is_active": true,
"live": false
}
TikTok has no cheap live health check, so live is always false here - the fields reflect what connect (or the last token refresh) wrote. status: "reauth_required" with status_reason set means the account needs to go through connect again; TikTok tokens are refreshed automatically on a yearly rotation, and this is what shows up if that rotation ever fails.
Disconnect TikTok
DELETE /channels/tiktok/{openId}
curl -X DELETE "https://api.youraiconnector.com/v1/channels/tiktok/openIdFromTikTok" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "status": "removed", "open_id": "openIdFromTikTok" }
GoHighLevel
GoHighLevel (GHL) is a CRM integration, not a messaging channel - connecting it does not use up a channel slot on the plan, because it rides the account’s existing channels rather than adding a new one. It’s also the one integration on this page that can hold more than one connection at once: each GHL sub-account (“location”) the customer installs the app on gets its own entry.
Step 1 - Start the GHL connection
POST /channels/ghl/connect
| Field | Required | Description |
|---|---|---|
brand |
No | Which GHL marketplace listing to authorize through. Defaults to the standard listing - only relevant if your deployment has more than one marketplace app configured. |
curl -X POST "https://api.youraiconnector.com/v1/channels/ghl/connect?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"status": "pending_authorization",
"oauth_url": "https://marketplace.gohighlevel.com/oauth/chooselocation?client_id=...&state=...",
"state_token": "opaque-one-time-token",
"brand": "dmchamp",
"expires_at": "2026-06-10T12:30:00.000Z"
}
Open oauth_url in the account holder’s browser so they can pick a GHL location and approve access. The state expires at expires_at (about 30 minutes).
List GHL connections
GET /channels/ghl/status
Unlike other channels this isn’t a single connection’s status - it lists every location the account has connected.
curl "https://api.youraiconnector.com/v1/channels/ghl/status" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"connections": [
{
"location_id": "abc123location",
"company_id": "xyz789company",
"brand": "dmchamp",
"status": "connected",
"status_reason": null,
"scopes": ["conversations.readonly", "conversations.write", "conversations/message.write"],
"connected_at": "2026-06-01T10:00:00.000Z",
"conversation_provider_id": "provider-id-in-ghl",
"trigger_subscriptions": [
{ "id": "sub_1", "key": "InboundMessage", "workflow_id": "wf_123" }
]
}
]
}
Disconnect a GHL location
DELETE /channels/ghl/{locationId}
Deletes the connection here, which stops every sync and trigger for that location. This does not uninstall the app on the GHL side - the customer removes it from their GHL marketplace installs if they want that too.
curl -X DELETE "https://api.youraiconnector.com/v1/channels/ghl/abc123location" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "status": "disconnected", "location_id": "abc123location" }
Phone numbers (buy and release)
Instead of connecting an existing number, you can buy a new WhatsApp-capable number directly. Search for available numbers, buy one, then poll until it finishes provisioning.
Note: Numbers bought here are WhatsApp-capable. WhatsApp sender registration runs in the background after purchase, so you poll the status until it reaches ONLINE before sending. Credits are deducted on purchase and are not refunded when you release the number.
Step 1 - Search available numbers
GET /phone-numbers/available?country_code=ISO2
cURL
curl "https://api.youraiconnector.com/v1/phone-numbers/available?country_code=US&apiKey=YOUR_API_KEY"
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/phone-numbers/available?country_code=US",
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
Python
res = requests.get(
"https://api.youraiconnector.com/v1/phone-numbers/available",
params={"country_code": "US"},
headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
| Query parameter | Required | Description |
|---|---|---|
country_code |
Yes | ISO 3166-1 alpha-2 country code to search in (e.g. US, GB, NL). |
type |
No | Preferred number class, local or mobile. Both classes may still be returned. |
Response
{
"success": true,
"phone_numbers": [
{
"phone_number": "+14155551234",
"purchase_credits": 50,
"monthly_credits": 50,
"cost_usd": 1.15
}
]
}
Each result shows the one-time purchase_credits and the recurring monthly_credits. A platform-supplied number costs at least 50 credits a month, rising with the carrier’s own monthly price, charged at purchase and on every renewal. Quote the purchase_credits / monthly_credits the search returns; never derive a price yourself. The first search on a new account provisions some underlying resources, so it may be a little slower than later searches.
Step 2 - Buy a number
POST /phone-numbers
Use a phone_number from the search results.
cURL
curl -X POST "https://api.youraiconnector.com/v1/phone-numbers" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+14155551234",
"country_code": "US",
"display_name": "Support line"
}'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/phone-numbers", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
phone_number: "+14155551234",
country_code: "US",
display_name: "Support line",
}),
});
const data = await res.json();
Python
res = requests.post(
"https://api.youraiconnector.com/v1/phone-numbers",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"phone_number": "+14155551234",
"country_code": "US",
"display_name": "Support line",
},
)
data = res.json()
| Field | Required | Description |
|---|---|---|
phone_number |
Yes | A number returned by the available-numbers search, in E.164 format. |
country_code |
Yes | ISO 3166-1 alpha-2 country code (e.g. US). |
display_name |
No | A friendly label. Defaults to the phone number. |
category |
No | Optional category label. |
Response
{
"success": true,
"phone_number": "+14155551234",
"channel": "whatsapp",
"whatsapp_status": "PURCHASED",
"outgoing_status": "PURCHASED",
"status": "PURCHASED",
"purchase_credits": 50,
"monthly_credits": 50
}
The number starts in the PURCHASED state. WhatsApp registration then proceeds in the background: PURCHASED -> PENDING -> ONLINE.
If purchase fails because a business address is missing or another required detail isn’t set, you’ll get a
400with a descriptiveerror. Set up the missing detail and try again.
Step 3 - Poll until ONLINE
GET /phone-numbers/{phoneNumber}/status
This is the shared phone-number status endpoint - it works for bought WhatsApp numbers as well as your other connected numbers.
cURL
curl "https://api.youraiconnector.com/v1/phone-numbers/+14155551234/status" \
-H "X-API-Key: YOUR_API_KEY"
JavaScript
const phone = encodeURIComponent("+14155551234");
const res = await fetch(
`https://api.youraiconnector.com/v1/phone-numbers/${phone}/status`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
// Poll until data.status === "ONLINE".
Python
import urllib.parse
phone = urllib.parse.quote("+14155551234")
res = requests.get(
f"https://api.youraiconnector.com/v1/phone-numbers/{phone}/status",
headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
# Poll until data["status"] == "ONLINE".
Response
{
"success": true,
"phone_number": "+14155551234",
"channel": "whatsapp",
"status": "ONLINE",
"status_reason": null,
"live": true
}
Step 4 - Release a number
DELETE /phone-numbers/{phoneNumber}
curl -X DELETE "https://api.youraiconnector.com/v1/phone-numbers/+14155551234" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "phone_number": "+14155551234", "released": true }
What this does depends on whose number it is.
For a number rented through the platform, it is a true release: the WhatsApp sender is de-registered, the number is handed back to the carrier and removed from the account, a 7-day cooldown is applied during which the number cannot be repurchased by anyone, and no credits are refunded.
For a number the account brought itself (its own Twilio account, its own Meta app or WhatsApp Business Account, or an Android SMS gateway), the same call only removes it from the account. Nothing is released at the upstream provider and no cooldown is written, so the number can be reconnected immediately. Its WhatsApp sender registration, if it had one, may or may not survive: the teardown tries to delete the sender using the account’s platform-managed Twilio credentials. On an account still on the managed setup those credentials are valid and the sender is deleted, so reconnecting means registering it again. On an account that has switched to its own Twilio, the delete cannot authenticate, and the sender is left registered in that account — reconnecting is then just re-attaching the existing sender.
Add a number you already own (BYO)
POST /phone-numbers/byo
Skips the search-and-buy flow above entirely. Use this when the account brings its own number (their own Twilio, their own Meta WhatsApp Business Account, or an Android SMS gateway) instead of renting one through the platform. This only records the number - no credits are charged, and nothing is provisioned with a provider here. The number stays inactive until the account holder completes WhatsApp OAuth to register a Sender on it (the same flow the dashboard’s “Bring your own number” button starts).
| Field | Required | Description |
|---|---|---|
phone_number |
Yes | The number to add, in E.164 format (e.g. +14155551234). |
country_code |
Yes | ISO 3166-1 alpha-2 country code (e.g. US). |
display_name |
No | A friendly label. Defaults to the phone number. |
category |
No | Optional category label. |
curl -X POST "https://api.youraiconnector.com/v1/phone-numbers/byo?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+14155551234",
"country_code": "US",
"display_name": "Support line"
}'
Response (201 Created):
{
"success": true,
"phone_number": "+14155551234",
"channel": "whatsapp",
"type": "BYO",
"whatsapp_status": "ADDED",
"outgoing_status": "ADDED",
"is_active": false
}
A phone_number that isn’t a real E.164 number (or that looks like Meta’s WhatsApp test number, which can never message real customers) returns 400. Adding a number that already exists on the account - even spelled slightly differently, like Mexico’s +52 vs +521 forms - returns 409 rather than creating a duplicate row.
Set a number as primary
POST /phone-numbers/{phoneNumber}/set-primary
Flips one number to is_active: true and every other number on the account to is_active: false, atomically - the account never ends up with two active numbers, or none, mid-request. is_active can’t be set through the general update endpoint on purpose; this dedicated call is the only way to change which number is primary.
curl -X POST "https://api.youraiconnector.com/v1/phone-numbers/+14155551234/set-primary" \
-H "X-API-Key: YOUR_API_KEY"
Response
{
"success": true,
"phone_number": {
"id": "+14155551234",
"phone_number": "+14155551234",
"display_name": "Support line",
"channel": "whatsapp",
"is_active": true,
"whatsapp_status": "ONLINE"
}
}
phone_number here is the full number object (the same shape GET /phone-numbers returns), not just the string. A phoneNumber not on the account returns 404.
Remove a number’s record (without releasing it)
DELETE /phone-numbers/{phoneNumber}/record
A plain delete of the number’s record on this account - no provider-side release or de-registration, and no 7-day cooldown like the release step above applies. Use this to clear out BYO, WhatsApp Web, Telegram, or LINE records, or a stale entry, without going through the managed release flow. Unlike a release, deleting a number that isn’t on the account is a 404, not a silent success.
curl -X DELETE "https://api.youraiconnector.com/v1/phone-numbers/+14155551234/record" \
-H "X-API-Key: YOUR_API_KEY"
Response
{ "success": true, "phone_number": "+14155551234", "deleted": true }
Route a channel to a campaign
Connecting a channel gets messages into the account. It does not decide which AI Agent answers them.
Routing is handled by Entry Points on an AI Agent, not by campaigns. Each channel has one channel-default Entry Point naming the Agent that answers new, unknown contacts on that channel:
| What you want to do | Call |
|---|---|
| Point a channel at the Agent that should answer it | PUT /entry-points/channel-defaults with body { "channel": "instagram", "agent_id": "AGENT_ID" } |
| Check whether the Entry Points ladder is live for the account | GET /entry-points/routing-status, which returns { "success": true, "cutover_enabled": true } once Entry Points decide that account’s routing |
| Leave a channel with no Agent answering it | DELETE /entry-points/channel-defaults?channel=instagram |
Until a channel has an Entry Point, a first message from someone you have never spoken to is still stored, but nothing picks it up and no assistant replies. This is the step most integrations miss: connecting Instagram and creating an Agent is not enough on its own — you also have to point the channel at the Agent. The full set of calls — including one Agent per WhatsApp number, keyword and comment rules — is in the Entry Points API.
POST /channels/campaign still writes the legacy per-channel campaign routing map, documented below, but that map is no longer consulted for inbound routing on any account; it is retained for rollback only. Do not build against it.
Route one or more channels (legacy campaign routing map)
POST /channels/campaign
Request fields
| Field | Required | Description |
|---|---|---|
campaign_id |
Yes | The campaign that should answer new contacts on these channels. Must belong to the account. |
channels |
Yes | A non-empty array of channels to route. Allowed: whatsapp, whatsapp_web, telegram, instagram, messenger, chat_widget, custom_channel, sms, email. |
The routing slot and the campaign’s enabled_channels list are updated together in one atomic operation, so they can never drift apart. A channel already routed to a different campaign is simply re-pointed at this one.
cURL
curl -X POST "https://api.youraiconnector.com/v1/channels/campaign?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"channels": ["instagram", "messenger"]
}'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/channels/campaign", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
campaign_id: "NBCXrhqGPSFsd6MV7pRo",
channels: ["instagram", "messenger"],
}),
});
const data = await res.json();
Python
import requests
res = requests.post(
"https://api.youraiconnector.com/v1/channels/campaign",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"channels": ["instagram", "messenger"],
},
)
data = res.json()
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"channels": ["instagram", "messenger"]
}
What has to be true for routing to actually fire
On an account that still reads the legacy campaign routing map, routing succeeds as an API call but three things on the campaign decide whether a real inbound message gets answered. Check all three when a routed channel stays silent.
| Requirement | What happens otherwise |
|---|---|
type is Incoming from Unknown Contacts or Combined |
The request is rejected with 400. Outgoing and Keywords campaigns cannot hold a routing slot. |
status is Live |
The routing is stored but never picks anything up. A Draft campaign is the most common cause of “I routed it and nothing happens”. |
ai_mode is true |
The contact is created and the message stored, but the assistant never replies. |
Keyword matching now lives on Entry Points — create an Entry Point of type keyword on the AI Agent that should answer.
One campaign per channel
Each channel holds exactly one legacy routing slot. Routing a second campaign to the same channel silently re-points the slot and returns 200 — there is no conflict error. The previous campaign keeps handling the contacts it already has; it just stops receiving new ones.
Clear a channel’s routing
DELETE /channels/campaign/{channel}
Removes the routing for a single channel, whatever campaign it currently points at, and takes the channel back off that campaign’s enabled_channels. New unknown contacts on the channel are no longer picked up by any campaign. Contacts already in the campaign carry on as before.
curl -X DELETE "https://api.youraiconnector.com/v1/channels/campaign/instagram?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"channel": "instagram",
"cleared": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo"
}
It is idempotent: clearing a channel that was never routed also returns 200, with cleared: false and campaign_id: null. This endpoint requires the incoming campaigns feature on the plan; without it you get a 403.
Use your own Meta app (Instagram + Messenger)
By default the Instagram + Messenger connection runs through the platform’s Meta app, so that app’s name is what the account holder sees on the Facebook consent screen. If you want the consent screen to show your brand instead, you can register your own Meta app and route the whole flow through it. Once configured, it applies to your account — nothing changes in the connect calls above except the branding.
This only covers Instagram + Messenger. WhatsApp, WhatsApp Web, Telegram and LINE connections are unaffected by a custom Meta app.
What your app needs first
This is the part that takes time, and it happens entirely on the Meta side:
- An app of type Business, with the Messenger and Instagram products added.
- Advanced Access (via Meta App Review) for:
pages_show_list,pages_messaging,pages_manage_metadata,pages_read_engagement,instagram_basic,instagram_manage_messages. Without Advanced Access, only people who hold a role on your app can complete the connection — your clients’ connects will fail. App Review typically takes a few weeks and requires Business Verification. - A Facebook Login for Business configuration created inside your app, granting the same permissions. Its numeric configuration ID is per-app, so you must create your own.
If your app is missing any of the required permissions, the connection fails at connect time with a clear error naming what’s missing (visible in the /status poll as byo_app_missing_permissions) — rather than appearing to work and failing on the first message.
Step 1 - Save your app
PUT /account-config/meta-app
| Field | Required | Description |
|---|---|---|
app_id |
Yes | Your Meta App ID (Settings → Basic). |
app_secret |
Yes | Your Meta App Secret. Verified against Meta before it is stored, then encrypted. Never returned by any endpoint. |
config_id |
Yes | The numeric ID of the Facebook Login for Business configuration inside your app. |
All three are required for the Facebook Login flow. If you only run the Instagram Login token-push lane described further down, you can leave them out entirely.
curl -X PUT "https://api.youraiconnector.com/v1/account-config/meta-app?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"app_id": "1234567890123456",
"app_secret": "your-app-secret",
"config_id": "9876543210987654"
}'
Response
{
"success": true,
"app_id": "1234567890123456",
"config_id": "9876543210987654",
"verify_token": "1f4c…a9",
"webhook_urls": {
"instagram": "https://api.youraiconnector.com/v1/incoming-instagram-message/byo/YOUR_ACCOUNT_ID",
"messenger": "https://api.youraiconnector.com/v1/incoming-messenger-message/byo/YOUR_ACCOUNT_ID"
}
}
Step 2 - Configure your app to talk to us
In your Meta app’s dashboard:
- Webhooks - for both the Instagram and Messenger products, set the Callback URL to the matching
webhook_urlsvalue from the response, and the Verify token toverify_token. Subscribe to themessages,messaging_postbacksandcommentsfields. - Valid OAuth Redirect URIs - add
https://api.youraiconnector.com/v1/auth-meta-callback-handlerso the consent flow can return.
GET /account-config/meta-app returns the same setup material any time; DELETE /account-config/meta-app removes the app (future connects revert to the platform app — also remove the webhook subscription inside your app).
Step 3 - Connect as usual
Nothing else changes. POST /channels/meta/connect (and the hosted connect_url page) automatically uses your app for your account; the response’s uses_byo_meta_app: true confirms which app the consent screen will show. Message sending, page selection, and disconnects work identically.
Bring your own Instagram Login app (token push)
The section above covers the Facebook Login flow, where the account connects through a Facebook Page. Meta also offers Instagram API with Instagram Login (Business Login for Instagram): the account holder authenticates on Instagram itself, no Facebook account or Page involved.
If your platform already runs its own Meta app with that product, you don’t need any OAuth flow on our side at all. Your clients authorize your app, and you push us the finished credential per account:
- You save your Instagram app’s credentials once (so we can verify your webhooks).
- Per account, you push the Instagram professional account ID + the long-lived Instagram user token your app obtained.
- You point your app’s Instagram messaging webhook at us. Events for accounts you never pushed are acknowledged and ignored.
- You own the token lifecycle: refresh tokens in your own system and push each refreshed token with the same call. We never refresh a pushed token.
What your app needs first
- The Instagram product (“API setup with Instagram login”) added to your Meta app. That product has its own App ID and App Secret pair, separate from the Facebook App ID/Secret — find them in the product’s setup panel.
- Advanced Access (via Meta App Review) for
instagram_business_basicandinstagram_business_manage_messages(addinstagram_business_manage_commentsif you use comment automations). Without it, only people with a role on your app can authorize it.
Step 1 - Save your Instagram app credentials
Same endpoint as above — send the Instagram pair to PUT /account-config/meta-app. The Facebook fields are not needed for this lane: send the pair on its own if Instagram Login is all you run, or together with the Facebook fields if you run both. A save always describes the whole setting, so whichever set you leave out is removed.
| Field | Required | Description |
|---|---|---|
instagram_app_id |
Together | The Instagram product’s own numeric App ID (not the Facebook App ID). |
instagram_app_secret |
Together | The Instagram product’s own App Secret. Encrypted at rest, never returned. |
curl -X PUT "https://api.youraiconnector.com/v1/account-config/meta-app?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"instagram_app_id": "1122334455667788",
"instagram_app_secret": "your-instagram-app-secret"
}'
Response — carries the Instagram-Login webhook URL (the instagram and messenger URLs only appear when the Facebook fields are stored too):
{
"success": true,
"instagram_app_id": "1122334455667788",
"verify_token": "1f4c…a9",
"webhook_urls": {
"instagram_login": "https://api.youraiconnector.com/v1/incoming-instagram-login-message/byo/YOUR_ACCOUNT_ID"
}
}
In your app’s Webhooks panel for the Instagram product, set the Callback URL to webhook_urls.instagram_login, the Verify token to verify_token, and subscribe to the messages and comments fields.
Step 2 - Push a token per account
PUT /channels/instagram-login/token
Works with sub_account_id like every other route, so an agency key can provision its whole fleet.
| Field | Required | Description |
|---|---|---|
ig_user_id |
Yes | The Instagram professional account ID — the user_id field from GET https://graph.instagram.com/v21.0/me?fields=user_id,username. This is the same ID Instagram webhooks carry as entry.id. ⚠️ It is not the id field from /me — that one is app-scoped and differs per Meta app. Pushing the app-scoped ID returns a 400 naming the mistake. |
access_token |
Yes | The long-lived Instagram user token your app obtained for that account. Validated live against Instagram before it is stored: the token must work and must belong to ig_user_id. |
expires_at |
No | ISO-8601 expiry of the token. Alternatively send expires_in (seconds). Defaults to 60 days. |
username |
No | The account’s @handle; we read it from Instagram anyway. |
curl -X PUT "https://api.youraiconnector.com/v1/channels/instagram-login/token?apiKey=YOUR_AGENCY_KEY&sub_account_id=CLIENT_ID" \
-H "Content-Type: application/json" \
-d '{
"ig_user_id": "17841400000000000",
"access_token": "IGAAR…",
"expires_at": "2026-11-01T00:00:00Z"
}'
Response
{
"success": true,
"ig_user_id": "17841400000000000",
"username": "acme.studio",
"expires_at": "2026-11-01T00:00:00.000Z",
"webhook_url": "https://api.youraiconnector.com/v1/incoming-instagram-login-message/byo/YOUR_ACCOUNT_ID"
}
As part of the push we subscribe your app to that account’s webhooks (subscribed_apps with the pushed token), so messages start flowing without any extra call on your side.
Refreshing - push the refreshed token to the same endpoint with the same ig_user_id; it updates the stored token and expiry in place.
Conflicts - one Instagram account is never live on two connections. If the account is already connected elsewhere, or on this very account through the Facebook Page flow, the push returns a 409 telling you which connection to disconnect first. A Facebook-flow connection is never replaced automatically, because it may also be serving Messenger.
Step 3 - Disconnect when a client leaves
DELETE /channels/instagram-login/token (same auth and sub_account_id) unsubscribes the webhooks best-effort and removes the stored credential. It always succeeds, even when the token has already died — and once the credential is gone, that account’s webhook events are ignored.
Tips for building a reliable wrapper
- Poll gently. Every few seconds is plenty. Stop once you reach a terminal state (
connected/ONLINE, or a failure status), and put a sensible overall timeout on the loop (the browser/QR steps expire, see eachexpires_at). - URL-encode phone numbers in the path. The leading
+should be sent as%2B. The endpoints recover bare digits too, but encoding is the safe default. - Never expect secrets back. Access tokens, channel secrets, and page tokens are accepted or stored but are never returned in any response.
- Handle the auth gate. A
403means API access isn’t on the plan, or that the channel you are connecting isn’t included in the account’s plan. See API Access. - Mind the rate limit. Authenticated requests are capped at 300 per minute; a
429means back off and retry. See Authentication.
Next steps
- Authentication - the four accepted auth forms and error format.
- API Access - generating and managing your API key.