
# 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:

1. **Start the connection** with a `POST`. The response gives you either a URL to open, or a QR code to display.
2. **Hand that off to the end user** - open the URL in their browser, or render the QR code on screen for them to scan.
3. **Poll the status endpoint** with `GET` on 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
**Note:** Before you start, make sure API access is enabled on the plan and you have an API key. See [API Access](../integrations/api-access.md) 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](authentication.md) 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**

```bash
curl -X POST "https://api.youraiconnector.com/v1/channels/meta/connect?apiKey=YOUR_API_KEY"
```

**JavaScript**

```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**

```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**

```json
{
  "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**

```bash
curl "https://api.youraiconnector.com/v1/channels/meta/status" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```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**

```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)**

```json
{
  "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
```

```bash
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**

```bash
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**

```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**

```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**

```json
{
  "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**

```bash
curl "https://api.youraiconnector.com/v1/channels/meta/posts?platform=instagram&limit=25" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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
```

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/channels/meta" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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**

```bash
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**

```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**

```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**

```json
{
  "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**

```bash
curl "https://api.youraiconnector.com/v1/channels/whatsapp/connect/+14155551234/status" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```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**

```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**

```json
{
  "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}
```

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/channels/whatsapp/+14155551234" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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**

```bash
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**

```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**

```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**

```json
{
  "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**

```bash
curl "https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/+15551230000/qr" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```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**

```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**

```json
{
  "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_url` is a ready-to-use image - drop it straight into an `<img src>`.
- `qr_code` is 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**

```bash
curl "https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/+15551230000/status" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```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**

```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**

```json
{
  "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}
```

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/channels/whatsapp-web/connections/+15551230000" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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 `403` if 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**

```bash
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**

```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**

```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**

```json
{
  "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**

```bash
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**

```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**

```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**

```json
{
  "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**

```bash
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**

```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**

```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**

```json
{
  "success": true,
  "phone_number": "+14155550100",
  "status": "connected",
  "telegram_user_id": "100000001",
  "username": "myhandle"
}
```

### Check Telegram status

```
GET /channels/telegram/connect/{phoneNumber}/status
```

```bash
curl "https://api.youraiconnector.com/v1/channels/telegram/connect/+14155550100/status" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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}
```

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/channels/telegram/+14155550100" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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**

```json
{
  "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`.

```bash
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**

```json
{
  "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.

```bash
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.

```bash
curl "https://api.youraiconnector.com/v1/channels/instagram-private/connect/yourbrand/status" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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.

```bash
curl -X POST "https://api.youraiconnector.com/v1/channels/instagram-private/yourbrand/sync-followers" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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 `camelCase` instead of `snake_case` - that's how this endpoint is wired today, not a typo. `isBaselineSeed: true` means this was the very first sync after connecting, which only records the starting follower list and never sends outreach DMs (so `dmsSent` is always `0` on 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**

```bash
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**

```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**

```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**

```json
{
  "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`** - when `false`, 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_token` and `channel_secret` are 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.

```bash
curl -X POST "https://api.youraiconnector.com/v1/channels/line/Uxxxxxxxx.../verify-webhook" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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
```

```bash
curl "https://api.youraiconnector.com/v1/channels/line/Uxxxxxxxx.../status" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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}
```

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/channels/line/Uxxxxxxxx..." \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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). |

```bash
curl -X POST "https://api.youraiconnector.com/v1/channels/viber?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "auth_token": "444d5555e6666f7777a8888b9999c000" }'
```

**Response**

```json
{
  "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.

```bash
curl "https://api.youraiconnector.com/v1/channels/viber/botIdFromViber/status?live=true" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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.

```bash
curl -X POST "https://api.youraiconnector.com/v1/channels/viber/botIdFromViber/verify-webhook" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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.

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/channels/viber/botIdFromViber" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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.

```bash
curl -X POST "https://api.youraiconnector.com/v1/channels/tiktok/connect?apiKey=YOUR_API_KEY"
```

**Response**

```json
{
  "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.

```bash
curl "https://api.youraiconnector.com/v1/channels/tiktok/openIdFromTikTok/status" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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}
```

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/channels/tiktok/openIdFromTikTok" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/channels/ghl/connect?apiKey=YOUR_API_KEY"
```

**Response**

```json
{
  "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.

```bash
curl "https://api.youraiconnector.com/v1/channels/ghl/status" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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.

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/channels/ghl/abc123location" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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
**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**

```bash
curl "https://api.youraiconnector.com/v1/phone-numbers/available?country_code=US&apiKey=YOUR_API_KEY"
```

**JavaScript**

```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**

```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**

```json
{
  "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**

```bash
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**

```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**

```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**

```json
{
  "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 `400` with a descriptive `error`. 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**

```bash
curl "https://api.youraiconnector.com/v1/phone-numbers/+14155551234/status" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```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**

```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**

```json
{
  "success": true,
  "phone_number": "+14155551234",
  "channel": "whatsapp",
  "status": "ONLINE",
  "status_reason": null,
  "live": true
}
```

### Step 4 - Release a number

```
DELETE /phone-numbers/{phoneNumber}
```

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/phone-numbers/+14155551234" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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. |

```bash
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`):

```json
{
  "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.

```bash
curl -X POST "https://api.youraiconnector.com/v1/phone-numbers/+14155551234/set-primary" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{
  "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.

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/phone-numbers/+14155551234/record" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Response**

```json
{ "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](entry-points.md).

`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**

```bash
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**

```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**

```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**

```json
{
  "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.

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/channels/campaign/instagram?apiKey=YOUR_API_KEY"
```

**Response**

```json
{
  "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:

1. **An app** of type Business, with the Messenger and Instagram products added.
2. **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.
3. **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.

```bash
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**

```json
{
  "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:

1. **Webhooks** - for both the Instagram and Messenger products, set the Callback URL to the matching `webhook_urls` value from the response, and the Verify token to `verify_token`. Subscribe to the `messages`, `messaging_postbacks` and `comments` fields.
2. **Valid OAuth Redirect URIs** - add `https://api.youraiconnector.com/v1/auth-meta-callback-handler` so 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:

1. You save your Instagram app's credentials once (so we can verify your webhooks).
2. Per account, you push the Instagram professional account ID + the long-lived Instagram user token your app obtained.
3. You point your app's Instagram messaging webhook at us. Events for accounts you never pushed are acknowledged and ignored.
4. 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_basic` and `instagram_business_manage_messages` (add `instagram_business_manage_comments` if 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. |

```bash
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):

```json
{
  "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. |

```bash
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**

```json
{
  "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 each `expires_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 `403` means 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](../integrations/api-access.md).
- **Mind the rate limit.** Authenticated requests are capped at 300 per minute; a `429` means back off and retry. See [Authentication](authentication.md).

## Next steps

- [Authentication](authentication.md) - the four accepted auth forms and error format.
- [API Access](../integrations/api-access.md) - generating and managing your API key.
