Your AI Connector Docs

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_KEY query form, others use the X-API-Key header. 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 with GET /entry-points/routing-status, clear it with DELETE /entry-points/channel-defaults. POST /channels/campaign still 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
    }
  ]
}

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"
}

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"
}

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"
}

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"
}

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"
}

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 whose status is Live (any casing — see Campaign types). Any other status silently disables it, and an invented one like "Active" is now rejected with a 400 rather than stored. Valid statuses include Draft, Pending Approval, Scheduled, Live, Paused, Completed, Sent and Failed.

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 returns 400 for 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.