REST API
API reference
Every endpoint of the v1 API. All requests use HTTPS and JSON, and authenticate with Authorization: Bearer YOUR_API_KEY.
Basics#
| Base URL | https://convoai.cloud/api/v1/ |
| Auth | Bearer API key. See Authentication. |
| Body | JSON, with Content-Type: application/json on writes. |
| IDs | Integers. Timestamps are ISO 8601 with a UTC offset. |
| Lists | Paginated, 50 per page: { count, next, previous, results }. See Pagination. |
| Trailing slash | Every path ends in /. |
| Spec | OpenAPI 3.1 at /api/v1/openapi.json. |
Paths below are relative to the base URL. An agent is visible to you if you own it or belong to its workspace.
Account
- GET
/me/Your account, plan and rate limit
Agents#
An agent is one configured AI assistant: its business details, persona, knowledge, channels and chat widget. New agents start as draft; train them, then activate.
- GET
/agents/List agents - POST
/agents/Create an agent - GET
/agents/{agent_id}/Retrieve full configuration - PATCH
/agents/{agent_id}/Update configuration - DELETE
/agents/{agent_id}/Delete an agent (204) - POST
/agents/{agent_id}/train/Rebuild the prompt and mark ready - POST
/agents/{agent_id}/activate/Set status to active - POST
/agents/{agent_id}/pause/Set status to paused
The agent object
List responses return this summary. GET /agents/{agent_id}/, POST and PATCH return it plus grouped configuration: business, persona, messages, features, capabilities, sales, ai, training and keywords.
{
"id": 42,
"name": "My Sales Agent",
"business_name": "Acme Dental",
"phone_number": "+14155550123",
"status": "active", // draft | active | paused
"training_status": "ready", // untrained | ready
"ai_enabled": true,
"language": "en",
"industry": "generic",
"tone": "professional",
"knowledge_count": 12,
"widget": { … }, // see Chat widget
"created_at": "2026-01-15T10:30:00+00:00",
"updated_at": "2026-05-10T08:14:22+00:00"
}POST /agents/ · PATCH /agents/{agent_id}/
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | POST only | Display name, up to 100 characters. |
| industry | string | Optional | Industry template. On create it is applied first, so its defaults fill fields you don't send. Must be one of the model's choices. |
| tone · language · conversion_mode · ai_model | string | Optional | Validated against the allowed values; an invalid value returns a validation_error listing them. |
| business_name, business_description, website_url, business_hours, … | string | Optional | Business details, persona, instructions, canned messages, branding and keyword fields. Strings are trimmed and truncated to each field's maximum length. |
| booking_enabled, lead_capture_enabled, capability_*, … | boolean | Optional | Feature and capability flags. Must be JSON true or false. |
| max_response_length | integer | Optional | 50 to 4000. |
| response_delay_seconds | integer | Optional | 0 to 10. |
| faqs · available_services | array | Optional | Up to 200 entries. |
Unknown keys are ignored, so you can send back a document you just read. Changing anything other than branding regenerates the agent's system prompt. Creating an agent past your plan's agent limit returns 403 plan_limit.
POST /agents/{agent_id}/train/ response
{
"id": 42,
"training_status": "ready",
"system_prompt": "…",
"can_activate": true
}Activate and pause return the agent summary.
Chat widget#
The website chatbox for an agent: its embed key, branding and a ready-made snippet to paste into a site.
- GET
/agents/{agent_id}/widget/Widget config and embed snippet - PATCH
/agents/{agent_id}/widget/Update branding or enable/disable - POST
/agents/{agent_id}/widget/rotate-key/Issue a new embed key
{
"enabled": true,
"embed_key": "3f6c2a9e-…",
"brand_name": "Acme Dental",
"logo_url": "",
"avatar_url": "",
"primary_color": "#2292A4",
"secondary_color": "#D96C06",
"button_text": "",
"tagline": "",
"welcome_message": "Hi! How can we help?",
"script_url": "https://convoai.cloud/widget/convoai-widget.js",
"loader_url": "https://convoai.cloud/widget/42/chat.js",
"embed_snippet": "<script src=\"https://convoai.cloud/widget/convoai-widget.js\" data-agent=\"3f6c2a9e-…\" data-color=\"#2292A4\" defer></script>",
"endpoints": {
"config": "https://convoai.cloud/api/v1/widget/3f6c2a9e-…/config/",
"send": "https://convoai.cloud/api/v1/widget/3f6c2a9e-…/send/",
"history": "https://convoai.cloud/api/v1/widget/3f6c2a9e-…/history/"
}
}PATCH accepts the same keys it returns (enabled, brand_name, logo_url, avatar_url, primary_color, secondary_color, button_text, tagline, welcome_message). Colours must be six-digit hex like #2292A4.
Chat#
Talk to the agent's AI directly: the same pipeline that answers on WhatsApp and the web widget, with intent detection, knowledge retrieval and memory. Use it to put your agent in mobile apps, backend services or any custom channel.
- POST
/agents/{agent_id}/chat/Send a message, get the AI reply - GET
/agents/{agent_id}/chat/history/?session_id=Messages in a chat session
POST /agents/{agent_id}/chat/
| Field | Type | Required | Description |
|---|---|---|---|
| message | string | Required | The user's message, up to 4,000 characters. |
| session_id | uuid | Optional | Omit on the first request and one is generated and returned. Send it back to continue the conversation. |
| contact | object | Optional | Attach the chat to a known contact: { name, email, phone }. With phone it joins that contact's record; with only email it matches by email. |
| context | string | Optional | Extra context added to the AI prompt (current page, cart contents, account status). Truncated to 2,000 characters. |
curl -X POST https://convoai.cloud/api/v1/agents/42/chat/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Do you have availability this Friday?",
"contact": {"name": "Jane Doe", "email": "jane@example.com"},
"context": "Visitor is on the pricing page."
}'{
"reply": "Yes! We have openings Friday at 10am and 2pm…",
"action": "respond",
"intent": "booking_inquiry",
"confidence": 0.94,
"session_id": "7f0c9a4e-8f21-4b58-9d3a-1c2b3d4e5f60",
"conversation_id": 1287,
"contact_id": 553,
"message_id": 98213,
"inbound_message_id": 98212
}If a person has taken over the conversation, the message is saved but the AI does not answer: the response has "reply": null and "action": "human_handling", and you can hand it back with POST /conversations/{id}/resume-ai/. If the AI pipeline fails, the API returns 502 server_error; retry the request. When the plan's AI replies for the period are used up (Free: 65 AI replies every 14 days, across all channels), the API returns 402 ai_quota_exceeded with resets_at: the message is saved to the inbox and no AI reply is generated.
The history endpoint returns { session_id, conversation_id, contact_id, messages } with up to the last 50 messages. It finds sessions by session_id; for a chat attached to a contact by phone, read the transcript with GET /conversations/{conversation_id}/messages/ instead.
Conversations#
A conversation is the thread between an agent and one contact on one channel, with its status and whether the AI or a person is handling it.
- GET
/conversations/List across your agents (?agent_id=, ?status=) - GET
/agents/{agent_id}/conversations/List for one agent, with filters - GET
/conversations/{id}/Retrieve a conversation - GET
/conversations/{id}/messages/Messages, oldest first - POST
/conversations/{id}/reply/Post a human reply - POST
/conversations/{id}/resolve/Mark resolved - POST
/conversations/{id}/handoff/Hand off to a human (AI stops replying) - POST
/conversations/{id}/resume-ai/Give the conversation back to the AI
The conversation object
{
"id": 1287,
"agent_id": 42,
"contact_id": 553,
"contact_name": "Sofia Ramirez",
"status": "open", // open | resolved | pending | waiting_customer | waiting_business
// | booked | quoted | spam | closed
"channel": "whatsapp", // whatsapp | email | sms | instagram | messenger | webchat | api
"ai_enabled": true,
"takeover_mode": "ai", // ai | human | supervisor
"priority": "normal", // low | normal | high | urgent
"last_message_at": "2026-05-10T09:47:12+00:00",
"created_at": "2026-05-10T09:00:00+00:00"
}GET /agents/{agent_id}/conversations/ query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | Optional | Filter by status. |
| since | ISO 8601 | Optional | Created at or after this time. |
| until | ISO 8601 | Optional | Created at or before this time. |
| lead_score_min | integer | Optional | Minimum lead score of the contact (0 to 100). |
| page | integer | Optional | Page number. Default 1. |
POST /conversations/{id}/reply/
Stores a human reply (sent_by_ai: false) in the conversation, whatever channel it started on, so a visitor in the web chatbox sees it through the widget. Body: { "message": "…" }, required, truncated to 4,000 characters. Returns the message object with 201.
handoff sets takeover_mode to human and ai_enabled to false; resume-ai reverses both. Each returns the updated conversation.
Messages#
Send outbound WhatsApp messages from your backend, or read stored messages. Messages must follow Meta's WhatsApp Business Policy: free-form text can only be sent inside the 24-hour customer service window; outside it, use an approved template.
- POST
/agents/{agent_id}/messages/Send a WhatsApp message - GET
/messages/?conversation_id=List messages in a conversation - GET
/messages/{id}/Retrieve a message - POST
/messages/send/Legacy send endpoint
POST /agents/{agent_id}/messages/
| Field | Type | Required | Description |
|---|---|---|---|
| to | string | Required | Recipient phone number, as stored on the contact (E.164, e.g. +14155559876). |
| type | string | Optional | text (default) or template. |
| text | string | If type is text | Message body. |
| template_name | string | If type is template | Meta-approved template name. |
| template_params | string[] | Optional | Values for the template placeholders, in order. |
| language_code | string | Optional | Template language. Default en_US. |
{
"to": "+14155559876",
"type": "template",
"template_name": "appointment_reminder",
"template_params": ["Sofia", "May 15", "10:00 AM"],
"language_code": "en_US"
}{
"status": "queued",
"agent_id": 42,
"to": "+14155559876"
}403 contact_opted_out. If the agent is not connected to WhatsApp the API returns 422 unprocessable; if WhatsApp rejects the send, 502 server_error.The message object
{
"id": 98213,
"conversation_id": 1287,
"direction": "outbound", // inbound | outbound
"type": "text",
"content": "Yes! We have openings Friday at 10am and 2pm…",
"sent_by_ai": true,
"status": "sent",
"created_at": "2026-05-10T09:47:12+00:00"
}The legacy POST /messages/send/ takes { "agent_id", "phone_number", "message" }, sends text only and returns { "status": "sent", "agent_id", "to" } with 200. Prefer the agent-scoped endpoint.
Contacts#
Contacts are the people who talk to your agents. ConvoAI creates one on first contact and keeps it up to date from conversations.
- GET
/contacts/List contacts (?agent_id=) - POST
/contacts/Create or find by phone - GET
/contacts/{id}/Retrieve a contact - PATCH
/contacts/{id}/Update a contact - DELETE
/contacts/{id}/Delete a contact (204) - POST
/contacts/{id}/tags/Add tags - DELETE
/contacts/{id}/tags/{tag_name}/Remove a tag - POST
/contacts/{id}/opt-out/Mark as opted out
The contact object
{
"id": 553,
"agent_id": 42,
"name": "Sofia Ramirez",
"phone": "+14155559876",
"email": "sofia@example.com",
"stage": "lead", // lead | prospect | customer | churned
"lead_score": 91,
"opted_in": true,
"tags": ["hot-lead", "enterprise"],
"created_at": "2026-04-01T14:00:00+00:00",
"last_seen_at": "2026-05-10T09:47:12+00:00"
}POST /contacts/
| Field | Type | Required | Description |
|---|---|---|---|
| agent_id | integer | Required | The agent the contact belongs to. |
| phone | string | Required | Phone number (also accepted as phone_number). |
| name | string | Optional | Display name. |
| string | Optional | Email address. | |
| stage | string | Optional | Default lead. |
If a contact with that phone already exists for the agent, it is returned with 200 (empty name and email are filled in); a new contact returns 201.
PATCH accepts name, email, stage, notes, budget and location (strings). Add tags with { "tags": ["vip", "spanish"] }; removing a tag that doesn't exist is a no-op. Tag and opt-out calls return the updated contact.
Knowledge base#
Knowledge documents are what the agent searches before it answers, so replies stay grounded in your content.
- GET
/agents/{agent_id}/knowledge/List documents - POST
/agents/{agent_id}/knowledge/Create a document - GET
/knowledge/{id}/Retrieve a document - PATCH
/knowledge/{id}/Update (re-indexes when content changes) - DELETE
/knowledge/{id}/Delete and remove from the index (204) - POST
/knowledge/{id}/reindex/Force re-indexing
The knowledge document object
{
"id": 311,
"agent_id": 42,
"title": "Pricing 2026",
"doc_type": "text", // text | faq | url | file
"content": "Cleanings are $89…",
"source_url": "",
"is_active": true,
"chunk_count": 4,
"created_at": "2026-05-10T09:05:00+00:00",
"updated_at": "2026-05-10T09:05:00+00:00"
}POST /agents/{agent_id}/knowledge/
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | Required | Human-readable title. |
| doc_type | string | Optional | text (default), faq, url or file. |
| content | string | Unless doc_type is url | The document text. |
| url | string | If doc_type is url | Page address, stored as source_url. |
| is_active | boolean | Optional | Include in searches. Default true. |
Indexing starts when a document is created. PATCH accepts title, content and is_active. reindex returns { "id", "reindexed", "chunk_count" }.
Appointments#
- GET
/appointments/List appointments (?agent_id=, ?status=)
{
"id": 77,
"agent_id": 42,
"contact_id": 553,
"contact_name": "Sofia Ramirez",
"title": "New-patient exam",
"start_time": "2026-05-15T10:00:00+00:00",
"end_time": "2026-05-15T10:45:00+00:00",
"status": "confirmed", // pending | confirmed | cancelled | completed
"notes": "",
"created_at": "2026-05-10T09:20:00+00:00",
"updated_at": "2026-05-10T09:20:00+00:00"
}Results are ordered by start time, newest first.
Analytics#
Pre-aggregated metrics for one agent. Every endpoint takes optional since and until (ISO 8601) and defaults to the last 30 days.
- GET
/agents/{agent_id}/analytics/Summary for the period - GET
/agents/{agent_id}/analytics/conversations/Conversations per day - GET
/agents/{agent_id}/analytics/leads/Contacts by stage and temperature - GET
/agents/{agent_id}/analytics/csat/Feedback, CSAT and SLA metrics
{
"period": "2026-05-01 / 2026-05-31",
"conversations_total": 1247,
"ai_resolved_pct": 91.4,
"handoff_pct": 8.6,
"appointments_booked": 67,
"messages_sent": 5832,
"messages_received": 4109
}{
"agent_id": 42,
"period": "2026-05-01 / 2026-05-31",
"results": [ { "date": "2026-05-01", "count": 38 }, … ]
}{
"agent_id": 42,
"period": "2026-05-01 / 2026-05-31",
"total_contacts": 310,
"by_stage": { "lead": 240, "prospect": 45, "customer": 25 },
"hot_leads": 52, // lead score 70 and above
"warm_leads": 98, // 40 to 69
"cold_leads": 160 // below 40
}The CSAT endpoint also accepts days (1 to 365, default 30) and returns message_feedback (positive, negative, total, satisfaction_rate), csat and sla. Where enterprise analytics are not available it returns 501.
Webhooks#
- GET
/agents/{agent_id}/webhooks/List webhooks - POST
/agents/{agent_id}/webhooks/Register a webhook - GET
/agents/{agent_id}/webhooks/{webhook_id}/Retrieve a webhook - PATCH
/agents/{agent_id}/webhooks/{webhook_id}/Update a webhook - DELETE
/agents/{agent_id}/webhooks/{webhook_id}/Delete a webhook (204) - POST
/agents/{agent_id}/webhooks/{webhook_id}/test/Send a test event
Payloads, signatures and retries are covered in the webhooks guide.
Agent resources#
Everything else an agent uses is plain CRUD under the agent. List endpoints are paginated (funnel steps come back all at once); POST creates (201), PATCH updates only the fields you send, DELETE returns 204. Unknown keys are ignored.
| Path under /agents/{agent_id}/ | What it holds | Required on create |
|---|---|---|
| products/ | Catalog items: name, description, price, currency, image_url, sku, is_active | name, price |
| funnels/ | Conversation funnels: name, description, trigger_keyword, is_active | name |
| funnels/{funnel_id}/steps/ | Funnel steps: order, step_type, content, save_response_to, delay_seconds. No order appends to the end. | — |
| quick-answers/ | Saved replies: shortcut, title, content | shortcut, title, content |
| procedures/ | Step-by-step procedures the agent follows, with triggers, criteria and allowed actions | name |
| guidance/ | Guidance rules: category, title, content, priority, roles and stages they apply to | title, content |
| actions/ | Automated actions: action_type, trigger_on, config, conditions, cooldown and limits | name, action_type |
| roles/ | Role overrides keyed by role_key. POST upserts: one row per key. | role_key |
| journey-stages/ | Journey stage instructions keyed by stage_key. POST upserts. | stage_key |
Every resource has GET, PATCH and DELETE on {path}{item_id}/, except funnel steps, which support PATCH and DELETE only.
E-commerce and channels
- GET
/agents/{agent_id}/ecommerce/Store configuration - PATCH
/agents/{agent_id}/ecommerce/Update store configuration - GET
/agents/{agent_id}/channels/Every channel and whether it is live - POST
/agents/{agent_id}/channels/Connect or update one channel - DELETE
/agents/{agent_id}/channels/{channel_type}/Disconnect a channel (204)
- E-commerce credentials (Shopify, Shippo, Klaviyo, Stripe keys) are write-only: they are never returned. Enabling the store also turns on the agent's commerce features.
- Credential values inside a channel's
configcome back as__set__; send__set__back unchanged to keep the stored secret. - The web chatbox is configured through /agents/{agent_id}/widget/, not as a channel. WhatsApp's state reflects its Meta connection.
Public widget endpoints#
These power the embeddable chatbox. They take no API key: the agent is identified by its embed_key, and they allow cross-origin requests. You normally use them through the widget script rather than directly.
- GET
/widget/{embed_key}/config/Branding for the chatbox - POST
/widget/{embed_key}/send/Send a visitor message, get the reply - GET
/widget/{embed_key}/history/Last 50 messages of a session - POST
/widget/{embed_key}/visitor/Attach a visitor's name and email
Errors#
Errors use standard HTTP status codes and a JSON body with error and detail. Field-specific validation errors also include field.
{
"error": "validation_error",
"detail": "to is required.",
"field": "to"
}| Status | error | Meaning |
|---|---|---|
| 400 | bad_request | Body is not valid JSON, or a value has the wrong type |
| 400 | validation_error | A required field is missing or a value is out of range |
| 401 | authentication_required | Missing or malformed Authorization header |
| 401 | invalid_api_key | Key not recognised or revoked |
| 401 | api_key_expired | Key is older than 90 days; generate a new one |
| 403 | forbidden | Your plan does not include API access |
| 403 | plan_limit | Agent limit reached for your plan |
| 403 | contact_opted_out | The contact has opted out of messages |
| 402 | ai_quota_exceeded | The plan's AI replies for this period are used up (chat); see resets_at |
| 404 | not_found | The resource does not exist or is not in your workspace |
| 422 | unprocessable | Agent not connected to WhatsApp, or template sending unavailable |
| 429 | rate_limited | Too many requests; see Retry-After |
| 502 | server_error | WhatsApp send failed, or the AI could not generate a reply |
A method an endpoint doesn't support returns 405 with an empty body.
