Campaigns API
A campaign bundles together everything the AI bot needs to talk to your contacts: its instructions, the channels it runs on, its active hours, and its follow-up behaviour. The Campaigns API lets you list, create, update, duplicate, enable, archive, and fine-tune campaigns from your own code instead of the dashboard.
All endpoints below are relative to the base URL https://api.youraiconnector.com/v1. Every request must be authenticated — see API Access and Authentication for how to get and pass your API key. API access is a paid feature; without it, requests are rejected with a 403.
Heads up: Some examples show the simple
?apiKey=YOUR_API_KEYquery form, others use theX-API-Keyheader. Both work everywhere — use whichever fits your setup.
Campaign types
When you create a campaign you must pick one of these types:
| Type | What it’s for |
|---|---|
Incoming from Unknown Contacts |
The bot replies to people who message you for the first time. |
Outgoing |
The bot starts conversations with contacts you add to the campaign. |
Keywords |
Inert - do not use. A Keywords campaign is inert: it is still accepted for backwards compatibility, but it is invisible to inbound routing on every channel and nothing reads its trigger keywords. Use an Entry Point of type Keyword on an AI Agent instead. |
Combined |
A mix of incoming and outgoing behaviour. |
Casing doesn’t matter. type, status, booking_provider, first_response_mode, bot.anthropic_model and bot.ai_speed all accept any casing — "live", "Live" and "LIVE" are the same thing — and the value is stored in its canonical form, which is what comes back when you read the campaign. The one exception is the pause pair: "Paused" and "paused" are two genuinely different states, so an ambiguous spelling like "PAUSED" is rejected with a 400 telling you to pick one.
The two pause states
| Status | Who writes it | What it means |
|---|---|---|
Paused |
The platform’s own safety checks (low engagement, repeated send errors, a limit hit) and the newer Agents and Broadcasts surfaces | The campaign is held. A scheduled sweep can lift a safety pause automatically once the reason clears. |
paused |
The dashboard’s Pause button, paired with resumed on Resume |
A person paused it by hand. Scheduled sends are torn down and rebuilt on resume. |
Both stop the campaign: inbound routing only runs while the status is exactly Live. From the API, use Paused to pause and Live to resume — the lower-case pair exists for the dashboard button and is kept working for it.
Neither of these is what happens when the AI stops replying inside one conversation. That is a per-contact switch, is_bot_active on the contact — set when a human takes over, when the contact opts out, or when the AI concludes the chat. The campaign’s own status is untouched, and every other conversation in it keeps running. See pause or resume the AI for one contact.
Creating a campaign does not decide who answers a channel. 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 it: set it with
PUT /entry-points/channel-defaults, check whether the ladder is live for the account withGET /entry-points/routing-status, clear it withDELETE /entry-points/channel-defaults.POST /channels/campaignstill writes the legacy per-channel campaign routing map, but that map is no longer consulted for inbound routing on any account; it is retained for rollback only. Do not build against it. See Route a channel to a campaign for both surfaces side by side.
List campaigns
GET /campaigns
Returns your campaigns, newest first. Archived campaigns are excluded unless you pass archived=true.
Query parameters
| Parameter | Required | Description |
|---|---|---|
limit |
No | Maximum number of campaigns to return. Default 50, maximum 100. |
cursor |
No | Pagination cursor. Pass the next_cursor value from the previous response to get the next page. |
archived |
No | Set to true to include archived campaigns. |
cURL
curl "https://api.youraiconnector.com/v1/campaigns?limit=20&apiKey=YOUR_API_KEY"
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/campaigns?limit=20", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.campaigns, data.next_cursor);
Python
import requests
res = requests.get(
"https://api.youraiconnector.com/v1/campaigns",
params={"limit": 20},
headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
print(data["campaigns"], data["next_cursor"])
Response
{
"success": true,
"campaigns": [
{
"id": "NBCXrhqGPSFsd6MV7pRo",
"name": "Inbound WhatsApp Leads",
"type": "Incoming from Unknown Contacts",
"status": "Live",
"enabled": true,
"archived": false,
"created_at": 1700000000000,
"ai_mode": true,
"language": "en",
"enabled_channels": ["whatsapp", "instagram"]
}
],
"next_cursor": "NBCXrhqGPSFsd6MV7pRo"
}
When next_cursor is null, you’ve reached the last page.
Get a campaign
GET /campaigns/{campaignId}
Returns the full campaign document, including the live bot configuration (bot), follow-up settings, enabled channels, and any keywords. Timestamps come back as epoch milliseconds.
cURL
curl "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo?apiKey=YOUR_API_KEY"
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const { campaign } = await res.json();
Python
import requests
res = requests.get(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
headers={"X-API-Key": "YOUR_API_KEY"},
)
campaign = res.json()["campaign"]
Response
{
"success": true,
"campaign": {
"id": "NBCXrhqGPSFsd6MV7pRo",
"name": "Inbound WhatsApp Leads",
"type": "Incoming from Unknown Contacts",
"status": "Live",
"language": "en",
"ai_mode": true,
"enabled": true,
"archived": false,
"created_at": 1700000000000,
"enabled_channels": ["whatsapp", "instagram"],
"bot": {
"instructions": "Greet warmly and ask about their goals.",
"goal": "Book a discovery call.",
"ai_speed": "balanced",
"anthropic_model": "standard",
"max_messages": 20
}
}
}
Note: A campaign owned by a different account returns 404 Campaign not found (not 403), so you can’t tell whether an ID exists on another account.
Create a campaign
POST /campaigns
Creates a new campaign. name and type are required; everything else is optional. You can include any other campaign field in the same request — for example language, ai_mode, or a full bot configuration object — and it will be stored with the new campaign. The owner and creation time are set automatically.
Request fields
| Field | Required | Description |
|---|---|---|
name |
Yes | The campaign name. |
type |
Yes | One of the four campaign types above. |
language |
No | Language the bot replies in (e.g. "en"). |
ai_mode |
No | Whether AI mode is on (true/false). On a campaign answered by an AI Agent, reads return the Agent’s Active toggle rather than a stored value — see the note under updating below. |
bot |
No | The bot configuration object (see Bot configuration fields). |
list_id |
No | ID of the contact list to attach. |
event_id |
No | ID of the event type the AI may book. |
event_ids |
No | Several event types at once, as an array of event type IDs — the first one is the default. Send either event_id or event_ids, not both. |
cURL
curl -X POST "https://api.youraiconnector.com/v1/campaigns?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Spring Promo",
"type": "Outgoing",
"language": "en",
"ai_mode": true,
"bot": {
"instructions": "Greet warmly and ask about their goals.",
"goal": "Book a discovery call."
}
}'
JavaScript
const res = await fetch("https://api.youraiconnector.com/v1/campaigns", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Spring Promo",
type: "Outgoing",
language: "en",
ai_mode: true,
bot: {
instructions: "Greet warmly and ask about their goals.",
goal: "Book a discovery call.",
},
}),
});
const { campaign_id } = await res.json();
Python
import requests
res = requests.post(
"https://api.youraiconnector.com/v1/campaigns",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"name": "Spring Promo",
"type": "Outgoing",
"language": "en",
"ai_mode": True,
"bot": {
"instructions": "Greet warmly and ask about their goals.",
"goal": "Book a discovery call.",
},
},
)
campaign_id = res.json()["campaign_id"]
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo"
}
Update a campaign
PUT /campaigns/{campaignId}
Partially updates a campaign — send only the fields you want to change. This is the only general update verb; there is no PATCH /campaigns/{campaignId} (the two PATCH routes are the narrow enable and archive toggles).
Which fields you can change. Everything the campaign editor writes, including name, status, type, language, ai_mode, enabled_channels, the trigger and drip settings, the booking and follow-up flags, the Instagram/Facebook monitoring fields, and the whole bot configuration. Identity and ownership are locked for the life of the campaign: user, id, and created_at are rejected, and so is any field name the endpoint doesn’t recognise. Rejection is per request, not per field — one unknown key returns a 400 and nothing in that request is written.
ai_mode on an Agent-backed campaign reflects the Agent. When a campaign is answered by an AI Agent, reading the campaign returns ai_mode derived from that Agent’s Active toggle — the one switch that actually decides whether the AI replies. Writing ai_mode on such a campaign is accepted but won’t change what you read back; turn the Agent’s Active toggle on or off instead (in the dashboard, or via the Agents API). On classic campaigns with no Agent, ai_mode reads and writes the stored value as before.
Bot fields merge, they don’t overwrite. Send bot settings either as dotted keys ("bot.instructions": "...") or as a nested object ("bot": { "instructions": "..." }) — both write leaf by leaf, so the fields you leave out keep their current values. bot.instructions, bot.goal, bot.rules, and bot.personality are all editable this way, as is every other bot setting listed under Bot configuration fields. The same applies to test_bot, frequency, and follow_up_config.
To replace a bot configuration wholesale — deleting any field you don’t send — use bot_replace (or test_bot_replace) with the complete object. You can’t combine a replace and a merge for the same object in one request; that returns a 400.
Note: Writing bot.* through the API takes effect immediately on the live campaign. The dashboard editor works differently: edits there are saved as a draft and only go live when the client clicks Publish. So if a client has unpublished dashboard changes, they sit in test_bot and an API read of bot correctly shows what the AI is using right now.
A few fields are set through a dedicated key rather than written directly: use list_id for the contact list, event_id for the event type (or event_ids, an ordered array of event type IDs, to let the AI book several — the first is the default; an empty array unlinks them all), and contact_ids (an array of contact IDs) for the campaign’s contacts. Knowledge-base entries are managed through the FAQs API, not this endpoint.
Tags replace, they don’t merge. Send tags as the complete array and it becomes the campaign’s tag set — see Campaign tags for the fields and for the endpoints that add or edit a single tag.
cURL
curl -X PUT "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Spring Promo v2", "enabled_channels": ["whatsapp"] }'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
{
method: "PUT",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Spring Promo v2",
enabled_channels: ["whatsapp"],
}),
}
);
const data = await res.json();
Python
import requests
res = requests.put(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"name": "Spring Promo v2", "enabled_channels": ["whatsapp"]},
)
data = res.json()
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo"
}
Delete a campaign
DELETE /campaigns/{campaignId}
Permanently deletes a campaign. This cannot be undone — if you might need the campaign again, archive it instead.
cURL
curl -X DELETE "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo?apiKey=YOUR_API_KEY"
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
{ method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
Python
import requests
res = requests.delete(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
Response
{
"success": true
}
Duplicate a campaign
POST /campaigns/{campaignId}/duplicate
Creates a copy of the campaign with all of its settings preserved. The copy starts disabled and its name gets a (copy) suffix, so it never sends messages until you explicitly enable it.
cURL
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/duplicate?apiKey=YOUR_API_KEY"
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/duplicate",
{ method: "POST", headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const { campaign_id } = await res.json();
Python
import requests
res = requests.post(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/duplicate",
headers={"X-API-Key": "YOUR_API_KEY"},
)
new_campaign_id = res.json()["campaign_id"]
Response
{
"success": true,
"campaign_id": "aZ9plnewCopyId01234"
}
Duplicate copies within one account.
Enable or disable a campaign
PATCH /campaigns/{campaignId}/enabled
Turns a campaign on or off. A disabled campaign stops engaging contacts but keeps all of its configuration.
Request fields
| Field | Required | Description |
|---|---|---|
enabled |
Yes | true to enable, false to disable. Must be a boolean. |
cURL
curl -X PATCH "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/enabled?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": true }'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/enabled",
{
method: "PATCH",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ enabled: true }),
}
);
const data = await res.json();
Python
import requests
res = requests.patch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/enabled",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"enabled": True},
)
data = res.json()
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"enabled": true
}
Archive or restore a campaign
PATCH /campaigns/{campaignId}/archived
Archives or restores a campaign. Archived campaigns are hidden from the default campaign list but keep all of their data and can be restored at any time.
Request fields
| Field | Required | Description |
|---|---|---|
archived |
Yes | true to archive, false to restore. Must be a boolean. |
cURL
curl -X PATCH "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/archived?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "archived": true }'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/archived",
{
method: "PATCH",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ archived: true }),
}
);
const data = await res.json();
Python
import requests
res = requests.patch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/archived",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"archived": True},
)
data = res.json()
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"archived": true
}
Update the bot configuration
PUT /campaigns/{campaignId}/bot-config
This is the safe way to change individual bot settings. Each field you send is merged into the existing bot configuration, so any fields you leave out are preserved. Use this instead of the campaign-update endpoint whenever you only want to tweak part of the bot.
Field keys must use letters, numbers, underscores, and hyphens only.
cURL
curl -X PUT "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/bot-config?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"instructions": "Always answer in a friendly, concise tone.",
"ai_speed": "balanced"
}'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/bot-config",
{
method: "PUT",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
instructions: "Always answer in a friendly, concise tone.",
ai_speed: "balanced",
}),
}
);
const data = await res.json();
Python
import requests
res = requests.put(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/bot-config",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"instructions": "Always answer in a friendly, concise tone.",
"ai_speed": "balanced",
},
)
data = res.json()
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo"
}
Bot configuration fields
All bot fields are optional. Send only the ones you want to set. Any additional bot fields beyond the ones listed here are accepted and stored as-is.
| Field | Type | Description |
|---|---|---|
instructions |
string | The primary instructions that steer how the bot talks to contacts. |
rules |
string | Hard rules the bot must always follow. |
goal |
string | The outcome the bot should work towards in each conversation. |
personality |
string | Tone-of-voice and personality description for the bot. |
ai_speed |
string | How much reasoning the AI applies before replying. One of fast, fast_thinker, balanced, thorough. |
anthropic_model |
string | The AI quality tier used for this campaign’s replies. One of standard, economy (deprecated), max, mini. max and mini only take effect on accounts eligible for those tiers. |
max_messages |
integer | Maximum number of bot messages per conversation. |
alert_human_when |
string | Conditions under which the bot should alert a human teammate. |
availability |
object | The bot’s active-hours schedule. You can set this here, or use the dedicated active-hours endpoint. |
follow_up_config |
object | Follow-up behaviour configuration, stored as provided. |
Set the bot active hours
PUT /campaigns/{campaignId}/active-hours
Sets the bot’s availability schedule. Outside the configured windows the bot does not reply automatically. This writes the availability field of the bot configuration.
Request fields
| Field | Required | Description |
|---|---|---|
availability |
Yes | An object keyed by weekday. Allowed keys are monday through sunday; any other key returns a 400. Days you leave out are unchanged. |
Each weekday holds either a single time window or an array of windows. A window has a start_time and end_time in 24-hour HH:MM format.
cURL
curl -X PUT "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/active-hours?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"availability": {
"monday": { "start_time": "09:00", "end_time": "17:00" },
"tuesday": [
{ "start_time": "09:00", "end_time": "12:00" },
{ "start_time": "13:00", "end_time": "17:00" }
]
}
}'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/active-hours",
{
method: "PUT",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
availability: {
monday: { start_time: "09:00", end_time: "17:00" },
tuesday: [
{ start_time: "09:00", end_time: "12:00" },
{ start_time: "13:00", end_time: "17:00" },
],
},
}),
}
);
const data = await res.json();
Python
import requests
res = requests.put(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/active-hours",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"availability": {
"monday": {"start_time": "09:00", "end_time": "17:00"},
"tuesday": [
{"start_time": "09:00", "end_time": "12:00"},
{"start_time": "13:00", "end_time": "17:00"},
],
}
},
)
data = res.json()
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo"
}
List a campaign’s custom functions
GET /campaigns/{campaignId}/custom-functions
Returns the custom functions linked to this campaign, resolved into full definitions. Custom functions are external HTTP actions the bot can call during a conversation — for example, checking stock in your store or creating a record in your CRM.
cURL
curl "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/custom-functions?apiKey=YOUR_API_KEY"
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/custom-functions",
{ headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const { custom_functions } = await res.json();
Python
import requests
res = requests.get(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/custom-functions",
headers={"X-API-Key": "YOUR_API_KEY"},
)
custom_functions = res.json()["custom_functions"]
Response
{
"success": true,
"custom_functions": [
{
"id": "fn_abc123",
"name": "check_stock",
"description": "Looks up whether a product is in stock.",
"url": "https://example.com/api/stock",
"method": "POST",
"input": [
{ "name": "sku", "type": "string" }
],
"ai_action": "Tell the customer whether the item is available.",
"created_at": 1700000000000,
"updated_at": 1700000500000
}
]
}
Link a custom function to a campaign
POST /campaigns/{campaignId}/custom-functions
Links an existing custom function to this campaign so the bot can call it during a conversation. Linking a function that’s already linked is a no-op.
| Field | Required | Description |
|---|---|---|
custom_function_id |
Yes | ID of the custom function to link. |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/custom-functions?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "custom_function_id": "fn_abc123" }'
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"custom_function_id": "fn_abc123"
}
Unlink a custom function from a campaign
DELETE /campaigns/{campaignId}/custom-functions/{customFunctionId}
Unlinking a function that isn’t linked is a no-op.
curl -X DELETE "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/custom-functions/fn_abc123?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"custom_function_id": "fn_abc123"
}
Link a knowledge-base source to a campaign
POST /campaigns/{campaignId}/kb-sources
Links a knowledge-base source (created via the FAQs API) to this campaign so the bot can draw on it when answering. Linking a source that’s already linked is a no-op.
| Field | Required | Description |
|---|---|---|
kb_source_id |
Yes | ID of the knowledge-base source to link. |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/kb-sources?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "kb_source_id": "kb_abc123" }'
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"kb_source_id": "kb_abc123"
}
Unlink a knowledge-base source from a campaign
DELETE /campaigns/{campaignId}/kb-sources/{kbSourceId}
Unlinking a source that isn’t linked is a no-op.
curl -X DELETE "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/kb-sources/kb_abc123?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"kb_source_id": "kb_abc123"
}
Link an MCP server to a campaign
POST /campaigns/{campaignId}/mcp-servers
Links an MCP server to this campaign, giving the bot access to that server’s tools during a conversation. Linking a server that’s already linked is a no-op.
| Field | Required | Description |
|---|---|---|
mcp_server_id |
Yes | ID of the MCP server to link. |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/mcp-servers?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "mcp_server_id": "mcp_abc123" }'
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"mcp_server_id": "mcp_abc123"
}
Unlink an MCP server from a campaign
DELETE /campaigns/{campaignId}/mcp-servers/{mcpServerId}
Unlinking a server that isn’t linked is a no-op.
curl -X DELETE "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/mcp-servers/mcp_abc123?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"mcp_server_id": "mcp_abc123"
}
Campaign media library
The media library holds images, videos, documents, and voice notes the bot can send during a conversation.
List a campaign’s media library
GET /campaigns/{campaignId}/media-library
curl "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/media-library?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"media_items": [
{
"id": "media_abc123",
"item_id": "media_abc123",
"title": "Pricing sheet",
"description": "Send when the contact asks about pricing.",
"media_url": "https://example.com/pricing.pdf",
"media_content_type": "application/pdf",
"type": "document",
"agent_id": "",
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"media_home": "campaign"
}
]
}
media_url is a signed URL captured at upload time — it may already be expired by the time you read it back; the dashboard re-signs it on demand.
Upload a media item
POST /campaigns/{campaignId}/media-library
| Field | Required | Description |
|---|---|---|
base64Data |
Yes | The file, base64-encoded (no data-URL prefix). |
mimeType |
Yes | MIME type of the file (e.g. image/png). |
title |
Yes | Short label shown in the library and in the AI prompt. |
description |
Yes | Instruction telling the bot when to send this item. |
fileName |
No | Original filename, used to build the storage object name. |
sendMessage |
No | Preferred wording the bot should use when it sends this item. |
maxSendsPerConversation |
No | Max times the bot may send this item to one contact in a conversation. Defaults to 1. |
sendAsVoiceNote |
No | For an audio upload, transcode it into a WhatsApp voice note. Defaults to false (stored as a plain audio file). |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/media-library?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"base64Data": "iVBORw0KGgoAAAANSUhEUgAA...",
"mimeType": "image/png",
"title": "Product photo",
"description": "Send when the contact asks what the product looks like."
}'
Response
{
"success": true,
"itemId": "media_abc123",
"mediaUrl": "https://example.com/product.png",
"storagePath": "ai_media/campaigns/NBCXrhqGPSFsd6MV7pRo/media_abc123.png",
"mediaContentType": "image/png",
"type": "image",
"isVoiceNote": false
}
Update a media item
PATCH /campaigns/{campaignId}/media-library/{itemId}
Edits the item’s metadata only — to replace the file itself, delete the item and upload a new one.
| Field | Description |
|---|---|
title |
Short label. |
description |
When-to-send instruction. |
send_message |
Preferred wording for the bot to use. |
max_sends_per_conversation |
Non-negative integer, or null to clear the cap. |
curl -X PATCH "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/media-library/media_abc123?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Updated pricing sheet" }'
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"item_id": "media_abc123"
}
Delete a media item
DELETE /campaigns/{campaignId}/media-library/{itemId}
Deleting an item that’s already gone is a no-op.
curl -X DELETE "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/media-library/media_abc123?apiKey=YOUR_API_KEY"
Response
{ "success": true, "deleted": true }
Campaign tags
A campaign tag is a label you teach the bot to apply to a contact during a conversation — hot-lead, not-interested, booked-a-call. Each tag has three parts:
| Field | Type | Description |
|---|---|---|
name |
string, required | The label itself. This is what the bot applies to the contact and what you match on later, so keep it short and stable. |
description |
string | The instruction telling the bot when to apply this tag. This is the part that does the work — “the person confirms they joined the community” gets used, “hot lead” does not. |
webhook |
string | A URL that receives a POST the moment the tag lands on a contact. Leave it out if you don’t need one. |
tag_id |
string | Optional. Links this entry to an existing tag in your account instead of a fresh one. Supply it if you want to address this specific tag later with the single-tag endpoints below. |
Tag names must be unique within a campaign. The bot applies tags by name, so two entries sharing one name have no defined winner.
Set all of a campaign’s tags
PUT /campaigns/{campaignId} with a tags array.
This replaces the campaign’s tags with exactly what you send, which is the same thing the dashboard’s Tags tab does when you save it. Send the complete array every time — a tag you leave out is a tag you deleted. Sending [] clears them all.
cURL
curl -X PUT "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tags": [
{
"name": "hot-lead",
"description": "The person confirms they want to buy, or asks how to get started right away.",
"webhook": "https://example.com/hooks/campaign-events"
},
{
"name": "not-interested",
"description": "The person declines the offer or says they are not a fit."
}
]
}'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
{
method: "PUT",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
tags: [
{
name: "hot-lead",
description:
"The person confirms they want to buy, or asks how to get started right away.",
webhook: "https://example.com/hooks/campaign-events",
},
{
name: "not-interested",
description: "The person declines the offer or says they are not a fit.",
},
],
}),
}
);
const data = await res.json();
Python
import requests
res = requests.put(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"tags": [
{
"name": "hot-lead",
"description": "The person confirms they want to buy, or asks how to get started right away.",
"webhook": "https://example.com/hooks/campaign-events",
},
{
"name": "not-interested",
"description": "The person declines the offer or says they are not a fit.",
},
]
},
)
data = res.json()
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo"
}
Read the tags back with GET /campaigns/{campaignId}.
Add one tag
POST /campaigns/{campaignId}/tags
Appends a single tag without resending the rest. Use this when you are adding to a set you did not build in this request.
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/tags?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "tag": { "name": "booked-a-call", "description": "The person confirms a booked time." } }'
Posting the exact same tag twice does nothing the second time. Posting the same tag_id with a different name or description appends a second entry rather than editing the first — use the endpoint below to edit in place.
Update or remove one tag
PUT /campaigns/{campaignId}/tags/{tagId}
DELETE /campaigns/{campaignId}/tags/{tagId}
These address one entry by its tag_id, so they only work on tags that were created with one. If a tag has no tag_id, change it with the whole-array PUT /campaigns/{campaignId} above.
curl -X PUT "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/tags/tag_abc123?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "tag": { "name": "hot-lead", "description": "Updated instruction." } }'
A tagId that is not on the campaign returns 404 with "Tag not found in campaign tags".
Toggle a campaign’s channels
POST /campaigns/{campaignId}/channels
Adds or removes channels from the campaign’s enabled_channels array without resending the whole array — safer than PUT /campaigns/{campaignId} when something else might be editing the campaign at the same time.
Send either a single toggle or a batch — not both in the same request:
{ "channel": "whatsapp", "action": "add" }
{ "add": ["whatsapp", "instagram"], "remove": ["sms"] }
| Field | Description |
|---|---|
channel |
One channel to toggle. Pair with action. |
action |
"add" or "remove". Pair with channel. |
add |
Array of channels to add. Batch form — use instead of channel/action. |
remove |
Array of channels to remove. Batch form. |
Valid channels: whatsapp, whatsapp_web, sms, instagram, messenger, facebook, chat_widget, custom_channel, imessage, telegram, instagram_private, line, viber, tiktok, email, linkedin, skool.
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/channels?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "channel": "whatsapp", "action": "add" }'
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"added": ["whatsapp"],
"removed": []
}
This only changes which channels the campaign advertises — it doesn’t decide who answers a channel. See Campaign types above and Route a campaign to incoming channels below for that.
Comment-to-DM (Instagram and Facebook)
Comment-to-DM turns a comment on one of your posts into a private conversation: someone comments, the bot sends them a DM, and the campaign takes the conversation from there. It is configured entirely through the campaign object, so there is nothing UI-only about it.
Connect the Facebook Page first — see Channel Connection. Then set the fields below with PUT /campaigns/{campaignId}.
The campaign must be
Live. Comment monitoring only picks up campaigns whosestatusisLive(any casing — see Campaign types). Any other status silently disables it, and an invented one like"Active"is now rejected with a400rather than stored. Valid statuses includeDraft,Pending Approval,Scheduled,Live,Paused,Completed,SentandFailed.
Fields
| Field | Type | Description |
|---|---|---|
monitor_instagram_posts |
boolean | Watch every Instagram post on the connected page. |
instagram_post_ids |
string[] | Watch only these Instagram posts. Leave unset when monitor_instagram_posts is on. |
instagram_comment_delay_minutes |
number | Wait this many minutes after a comment before sending the DM. |
monitor_facebook_posts |
boolean | Watch every Facebook post on the connected page. |
facebook_post_ids |
string[] | Watch only these Facebook posts. |
facebook_comment_delay_minutes |
number | Delay before the DM, in minutes. |
public_comment_reply_instructions |
string | Guidance for the visible reply left on the comment itself. Overrides the default “check your DMs” wording. |
first_response_mode |
string | "ai" (default) generates the first DM and the public reply. "exact_text" sends your wording verbatim, with no AI generation and no credit charge. |
first_response_exact_text |
string | The verbatim first DM, used when first_response_mode is "exact_text". Required for that mode to take effect. |
first_response_exact_text_variants |
string[] | Extra wordings for the first DM. One is picked at random per send, so repeated DMs are not byte-identical. |
public_comment_reply_exact_text |
string | The verbatim public reply in "exact_text" mode. Leave blank to skip the public reply and send only the DM. |
public_comment_reply_exact_text_variants |
string[] | Extra wordings for the public reply. |
monitor_instagram_followers |
boolean | Treat a new follower as a trigger and send an opening DM (Instagram personal accounts). |
follower_outreach_instructions |
string | Guidance for that new-follower opening DM. |
respond_to_instagram_story_replies |
boolean | Whether the AI answers replies to your Instagram Stories. Default true. Set false to have Story replies land in the chat (with the Story attached) without an AI reply. Live setting — not part of the draft, so it does not need publishing. |
Clearing a field
These fields are removed rather than set to null when you send null, so the bot falls back to its defaults: instagram_post_ids, facebook_post_ids, instagram_comment_delay_minutes, facebook_comment_delay_minutes, public_comment_reply_instructions, follower_outreach_instructions, first_response_exact_text, first_response_exact_text_variants, public_comment_reply_exact_text, public_comment_reply_exact_text_variants.
One unknown key rejects the whole request.
PUT /campaigns/{campaignId}validates the entire body against an allow-list. A key that isn’t recognised returns400for the request as a whole — it is not silently ignored, and none of the other fields in that body are written.
cURL
curl -X PUT "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "Live",
"monitor_instagram_posts": true,
"instagram_comment_delay_minutes": 2,
"first_response_mode": "exact_text",
"first_response_exact_text": "Hey! Sending the details over now.",
"first_response_exact_text_variants": [
"Hi there, here are the details you asked for.",
"Thanks for commenting, here is what you need."
],
"public_comment_reply_exact_text": "Just sent you a DM."
}'
JavaScript
const res = await fetch(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
{
method: "PUT",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
status: "Live",
monitor_instagram_posts: true,
instagram_comment_delay_minutes: 2,
first_response_mode: "ai",
public_comment_reply_instructions:
"Tell them to check their message requests folder too.",
}),
}
);
const data = await res.json();
Python
import requests
res = requests.put(
"https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"status": "Live",
"monitor_facebook_posts": True,
"facebook_post_ids": None,
"facebook_comment_delay_minutes": 5,
},
)
data = res.json()
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo"
}
The visible reply left on the comment requires the comment-reply feature on your plan. Without it the DM still sends and the public reply is skipped.
Optimize a campaign with AI
POST /campaigns/{campaignId}/optimize
Runs the same AI rewrite as the dashboard’s Optimize and thumbs-down feedback flows: takes your feedback, rewrites the bot’s instructions, and stages the result as a new draft revision for you to review.
| Field | Required | Description |
|---|---|---|
user_feedback |
One of these two is required | Freeform feedback describing what to improve. |
thumbs_down_feedback |
One of these two is required | Feedback captured from a thumbs-down on a specific bot reply. |
thumbs_down_message |
No | The bot message the thumbs-down feedback refers to. |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/optimize?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "user_feedback": "Make the tone more casual and mention the free trial earlier." }'
Response (202 — the rewrite runs in the background)
{ "success": true, "campaign_id": "NBCXrhqGPSFsd6MV7pRo" }
Poll GET /campaigns/{campaignId} and watch test_bot.status: it flips to "Optimizing" right away, then back to "Draft" once the rewrite lands in test_bot. From there it behaves like any dashboard draft — review it, then publish it in the dashboard to make it live. A 409 means an optimization is already running for this campaign.
Optimizing costs credits, the same as any other AI operation on your account.
Assign a contact to a campaign
POST /campaigns/{campaignId}/contacts/{contactId}/assign
Puts an existing contact into a campaign and, if you ask for it, sends the campaign’s opening message right away. This is the way to send a campaign’s approved WhatsApp template to one contact: the template that a campaign was approved with belongs to that campaign, so it does not appear in the Templates API library and cannot be sent through /whatsapp-templates/send.
| Field | Required | Description |
|---|---|---|
sendOpeningMessage |
No | true sends the campaign’s opening message (the approved WhatsApp template on a WhatsApp campaign) as soon as the contact is assigned. Defaults to false. |
triggerAIResponse |
No | true lets the AI write its own first message instead. Defaults to false. |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/contacts/contact_abc123/assign?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sendOpeningMessage": true }'
Response
{
"success": true,
"data": { "contactId": "contact_abc123", "campaignId": "NBCXrhqGPSFsd6MV7pRo" }
}
Credits: Sending the opening message on a WhatsApp campaign is charged like any template send, priced by the recipient’s country and the template’s category. On other channels the opening message is a normal outgoing message.
Route a campaign to incoming channels
These endpoints manage which campaign answers new, unknown contacts on a channel. Prefer Entry Points for new integrations (see the note under Campaign types) — these stay useful for working with campaigns that route the older way, and for resolving a channel-ownership conflict between two incoming campaigns.
Assign a campaign to incoming channels
POST /campaigns/{campaignId}/incoming-routing
| Field | Required | Description |
|---|---|---|
channels |
Yes | Array of channels this campaign should answer for new, unknown contacts. |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/incoming-routing?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "channels": ["whatsapp", "instagram"] }'
Response
{
"success": true,
"uid": "abc123",
"campaignId": "NBCXrhqGPSFsd6MV7pRo",
"channels": ["whatsapp", "instagram"],
"failed": []
}
channels lists only the channels that were actually routed to this campaign; failed lists any that weren’t. If every requested channel fails, the request itself fails.
Clear a campaign’s incoming routing
DELETE /campaigns/{campaignId}/incoming-routing
| Field | Required | Description |
|---|---|---|
channelToUnassign |
No | Clear routing for just this one channel. Omit to clear every channel this campaign currently answers. |
curl -X DELETE "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/incoming-routing?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "channelToUnassign": "instagram" }'
Response
{
"success": true,
"uid": "abc123",
"campaignId": "NBCXrhqGPSFsd6MV7pRo",
"channelsRemoved": ["instagram"]
}
Reactivate a dormant campaign
POST /campaigns/{campaignId}/reactivate
Brings a campaign back from Ended, Completed, Paused, or Draft and re-claims its channels. Only works on Incoming from Unknown Contacts or Combined campaigns — a campaign that’s already Live is treated as success with nothing to do.
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/reactivate?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"data": {
"success": true,
"channelsReactivated": ["whatsapp"],
"channelsBlockedByConflict": [],
"campaignType": "Incoming from Unknown Contacts"
}
}
A channel already claimed by a different campaign’s agent shows up in channelsBlockedByConflict rather than failing the whole call — use stop a conflicting incoming campaign below to free it up first if you want this campaign to take it over. A 400 is returned for a campaign type that doesn’t support reactivation, or a status that isn’t one of the dormant ones above.
Stop a conflicting incoming campaign
POST /campaigns/{campaignId}/stop-incoming
Frees this campaign’s channels from whichever OTHER campaign currently holds them, so this campaign can claim them next. This is the REST version of what the dashboard does automatically when you launch an incoming campaign into a channel someone else is already answering.
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/stop-incoming?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"ended_campaign_ids": [],
"released_channels": ["whatsapp"],
"cleared_entire_field": false
}
released_channels comes back empty when this campaign already owns every channel it advertises — there’s nothing to take over.
Cost estimates
Estimate what launching a campaign will cost before you send it.
WhatsApp template cost estimate
GET /campaigns/{campaignId}/template-cost-estimate
curl "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/template-cost-estimate?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"billing_mode": "credits",
"data": {
"countries": [
{
"countryCode": "1",
"name": "United States",
"iso": "US",
"flag": "🇺🇸",
"contactCount": 120,
"costPerContact": 2,
"subtotal": 240
}
],
"totalContacts": 120,
"totalTemplateCost": 240,
"templateCategory": "marketing",
"billing_mode": "credits",
"service_messages_billable_soon": false
}
}
billing_mode is "credits" on the managed WhatsApp lane. On a lane where Meta bills your own WhatsApp Business Account directly, costPerContact, subtotal, and totalTemplateCost come back null — never 0, which would read as free — since there’s no credit figure to report.
SMS cost estimate
GET /campaigns/{campaignId}/sms-cost-estimate
curl "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/sms-cost-estimate?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"billing_mode": "twilio_direct",
"data": {
"totalContacts": 120,
"messageLength": 87,
"segmentsPerMessage": 1,
"totalSegments": 120,
"estimatedCostUsd": 0.96,
"priceUnit": "USD per segment",
"billedByTwilio": true
}
}
SMS is always sent through your own Twilio account (see SMS provider), so this is always billed by Twilio directly — estimatedCostUsd is an estimate of that Twilio bill, not a credit charge.
Limit checks
Check a limit before you launch, instead of finding out from a failed send.
Campaign-scoped checks
GET /campaigns/{campaignId}/limits/ai-credit-messaging — whether launching or scheduling this campaign would exceed your account’s AI-credit messaging limit.
GET /campaigns/{campaignId}/limits/messaging — whether it would exceed your account’s daily messaging limit.
curl "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/limits/messaging?apiKey=YOUR_API_KEY"
Response (limit not exceeded)
{
"success": true,
"data": "Campaign is within the daily messaging limit."
}
A 400 is returned instead when the limit would be exceeded, with the reason in error.
Account-scoped checks
GET /campaigns/limits/campaigns — whether you’ve hit your subscription’s monthly campaign-creation limit.
GET /campaigns/limits/contacts — whether you’ve hit your subscription’s contact limit.
curl "https://api.youraiconnector.com/v1/campaigns/limits/campaigns?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"data": "You can create 3 more campaigns this month."
}
Campaign stat totals
GET /campaigns/stats/totals
Sent and replied totals for every campaign AND every AI agent on your account, over a trailing window — the same numbers the campaign list page shows next to each row, in one call instead of one request per campaign.
| Query parameter | Description |
|---|---|
days |
Size of the trailing window, 1-365. Defaults to 90. |
curl "https://api.youraiconnector.com/v1/campaigns/stats/totals?days=30&apiKey=YOUR_API_KEY"
Response
{
"success": true,
"byCampaign": {
"NBCXrhqGPSFsd6MV7pRo": { "sent": 1204, "replied": 318 }
},
"byAgent": {
"agent_abc123": { "sent": 1204, "replied": 318 }
},
"windowDays": 30
}
byAgent is its own roll-up, not a sum of byCampaign — an AI-Agent-native account’s traffic can carry no campaign at all, so it would otherwise be invisible here.
Test a campaign in the playground
The playground lets you hold a conversation with a campaign’s bot without touching a real channel or a real contact. It is the same sandbox as the dashboard’s try-out panel, and it is fully available over the API.
The flow is: create a hidden test contact, send a message, then poll the campaign for the bot’s reply. Replies are generated asynchronously, so they arrive in test_messages on the campaign rather than in the response body.
Playground runs over the API cost credits. A test conversation started with an API key is charged at the normal AI-message rate, the same as a real reply, and appears in your usage history as a regular entry. Testing from the dashboard stays free. The difference is deliberate: a test run does the same AI work as a live one, so an unmetered API playground would be a way to run unlimited AI on someone else’s tab.
Step 1 - Create the test contact
POST /campaigns/{campaignId}/try-out/contact
Creates the hidden test contact and links it to the campaign. All body fields are optional; anything you leave out falls back to a built-in sample identity (John Doe).
| Field | Required | Description |
|---|---|---|
first_name |
No | Test contact’s first name. |
last_name |
No | Test contact’s last name. |
email |
No | Test contact’s email. |
phone |
No | Test contact’s phone number. |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/try-out/contact?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "first_name": "Maria", "last_name": "Lopez" }'
Response
{
"success": true,
"contactId": "8kQx1vNbA2fLpR7d"
}
Step 2 - Record the incoming message
POST /campaigns/{campaignId}/try-out/messages
Appends messages to the test thread. Send the visitor’s message here first, so it appears in the conversation history the bot reads.
| Field | Required | Description |
|---|---|---|
messages |
Yes | Array of message objects, max 200 per request. |
messages[].body |
Yes | The message text. |
messages[].direction |
Yes | "inbound" for the visitor, "outbound" for the bot. |
messages[].timestamp |
No | ISO-8601 string or epoch milliseconds. |
messages[].role |
No | Optional role label. |
messages[].name |
No | Optional display name. |
ignoreCounter |
No | Integer. Resets the campaign’s ignore counter in the same write. |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/try-out/messages?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"body": "Do you ship to Belgium?",
"direction": "inbound",
"timestamp": "2026-07-22T09:30:00Z"
}
]
}'
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo",
"appended": 1
}
Step 3 - Ask the bot to reply
POST /campaigns/{campaignId}/try-out/test-message
Dispatches the message to the AI pipeline. This is the call that actually produces a bot response.
| Field | Required | Description |
|---|---|---|
message |
Yes | The visitor’s latest message text. |
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/try-out/test-message?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "Do you ship to Belgium?" }'
Response
{
"success": true,
"data": "Published"
}
"Published" means the message went to the AI pipeline. "Ignored" means a newer test message superseded this one — the playground folds a rapid burst into a single reply, roughly four seconds after the last message, the same way a real conversation waits for someone to finish typing. Because of that fold window, this call takes a few seconds to return.
Step 4 - Read the reply
GET /campaigns/{campaignId}
The bot’s reply is appended to the campaign’s test_messages array. Poll the campaign until a new outbound entry appears.
{
"success": true,
"campaign": {
"id": "NBCXrhqGPSFsd6MV7pRo",
"test_messages": [
{ "body": "Do you ship to Belgium?", "direction": "inbound" },
{ "body": "Yes, we ship across the EU.", "direction": "outbound" }
]
}
}
Reset the playground
POST /campaigns/{campaignId}/try-out/reset
Clears the whole sandbox: deletes the test contact, wipes test_messages, and releases the bot’s response locks. Use this between test runs.
curl -X POST "https://api.youraiconnector.com/v1/campaigns/NBCXrhqGPSFsd6MV7pRo/try-out/reset?apiKey=YOUR_API_KEY"
Response
{
"success": true,
"campaign_id": "NBCXrhqGPSFsd6MV7pRo"
}
Other playground endpoints
| Endpoint | What it does |
|---|---|
DELETE /campaigns/{campaignId}/try-out/contact |
Deletes just the current test contact and unlinks it, leaving test_messages intact. Succeeds even when no contact is linked. |
POST /campaigns/{campaignId}/try-out/transfer |
Starts a fresh playground seeded with an existing conversation, in one request: replaces the test contact and overwrites test_messages. Body takes first_name, last_name, messages (may be empty) and ignoreCounter. Prefer this over delete-then-create-then-append, which triples your rate-limit spend. |
POST /campaigns/{campaignId}/try-out/messages/replace |
Overwrites test_messages wholesale instead of appending. Use for truncating or rewinding a thread. |
POST /campaigns/{campaignId}/try-out/contact/reset-ignore-counter |
Resets only the test contact’s ignore counter, for redo and repeat flows after a send. |
Campaigns API errors
Campaign endpoints return the standard error envelope:
{
"success": false,
"error": "Campaign not found"
}
| Status | When it happens on a campaign endpoint |
|---|---|
400 |
A required field is missing or invalid (for example a bad type, a non-boolean enabled, or an unknown weekday key). Also returned by a limit check endpoint when the limit would be exceeded, and by reactivate for a campaign type or status that doesn’t support it. |
404 |
The campaign was not found — either it doesn’t exist or it belongs to another account. |
409 |
An optimization is already running for this campaign. |
The shared codes every endpoint can return — 401, 403 (your plan does not include API access), 429 (rate limit) and 500 — are listed with retry guidance in Errors & Pagination.
Related
- Route a channel to a campaign — point Instagram, WhatsApp, or any other channel at the AI Agent that should answer it, using Entry Points.
- Generate follow-up templates with AI — start a background job that writes a campaign’s WhatsApp follow-up templates.
- FAQs API — manage the question-and-answer entries your campaigns use.
- API Access — generate your API key.
- Authentication — all the ways to pass your key.