REST API Integration
The Chatbot REST API lets you embed AIChatVault chatbots directly inside your own application — mobile apps, CRM portals, customer dashboards, support systems — using plain HTTP requests. Your server talks to the API; you control the entire UI.
reply HTML field. You must render the acv-branding element as-is — removing, hiding, stripping, or suppressing it by any technical means is a breach of the Terms of Service (§10) and may result in account termination and legal action. The only authorised way to remove attribution is the Brand Removal add-on.When to use the REST API vs the widget
| Use case | Recommended approach |
|---|---|
| Public website or landing page | Widget embed — paste one script tag, done |
| Dedicated support page | Iframe embed |
| Custom mobile app (iOS, Android, Flutter) | REST API — full control of UI |
| Internal CRM or dashboard | REST API — render inside your own components |
| Server-to-server automation | REST API — no browser needed |
| Existing chat UI you want to power with AI | REST API — swap the backend, keep your UI |
Typical conversation flow
Every chat session follows these steps in order:
- •1. Call
GET /chatbots/{slug}once on app start to load the bot name, greeting, colors, and branding. - •2. Call
POST /chatbots/{slug}/conversationsto open a session and receive aconversation_idandsession_id. Store both. - •3. For each user message, call
POST …/conversations/{id}/messages. Setdata.replyasinnerHTMLof your message bubble — it is pre-formatted HTML. - •4. Optionally call
POST …/messages/rateafter any assistant message to record 👍 or 👎 feedback. - •5. When contact details are collected, call
POST …/conversations/{id}/leads. - •6. When the chat closes, call
POST …/conversations/{id}/end.
Base URL & authentication
All REST API endpoints live under:
https://aichatvault.com/api/v1
Every request must include your API key (from Settings → API Keys) in the Authorization header:
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json
Quick start — 3 steps
Verify your key
curl https://aichatvault.com/api/v1/auth/test \
-H "Authorization: Bearer sk_live_your_key_here"
# Response
{
"success": true,
"message": "Authentication successful",
"data": { "user_id": 42, "organization_id": 7, "email": "you@example.com" }
}Get your chatbot slug
curl https://aichatvault.com/api/v1/chatbots \
-H "Authorization: Bearer sk_live_your_key_here"
# Response
{
"success": true,
"data": [
{ "id": 16, "slug": "support-bot-XyZ123", "name": "Support Bot", "is_active": true }
],
"meta": { "total": 1 }
}Start a conversation and send a message
# 1. Start a conversation
curl -X POST https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "visitor_id": "user-123", "visitor_name": "Alice" }'
# → { "success": true, "data": { "conversation_id": 1802, "session_id": "api_6a63b583280b0", "visitor_id": "user-123" } }
# 2. Send a message
curl -X POST https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations/1802/messages \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "message": "What is your return policy?" }'
# → { "success": true, "data": { "reply": "Our return policy is 30 days...", ... } }conversation_id from step 1 — you need it for every subsequent message in that session. It is safe to create one conversation per user session and reuse it across multiple messages.All endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/chatbots | List all your active chatbots |
| GET | /api/v1/chatbots/{slug} | Get chatbot config — name, greeting, colors, branding, knowledge base status |
| POST | /api/v1/chatbots/{slug}/conversations | Start a new conversation session |
| POST | /api/v1/chatbots/{slug}/conversations/{id}/messages | Send a user message and receive an AI reply (HTML formatted) |
| POST | /api/v1/chatbots/{slug}/conversations/{id}/messages/rate | Rate an assistant message as good or bad |
| GET | /api/v1/chatbots/{slug}/conversations/{id} | Retrieve full conversation history |
| POST | /api/v1/chatbots/{slug}/conversations/{id}/end | End a conversation and record its duration |
| POST | /api/v1/chatbots/{slug}/conversations/{id}/leads | Submit visitor contact details as a lead |
GET /chatbots
Returns all active chatbots in your organisation. Use this to let users pick which chatbot to chat with, or to discover your slug programmatically.
curl https://aichatvault.com/api/v1/chatbots \
-H "Authorization: Bearer sk_live_your_key_here"
{
"success": true,
"data": [
{
"id": 16,
"slug": "support-bot-XyZ123",
"name": "Support Bot",
"description": "Handles product and billing questions",
"language": "en",
"is_active": true,
"created_at": "2026-01-15T09:00:00Z"
}
],
"meta": { "total": 1 }
}GET /chatbots/{slug}
Returns full chatbot configuration — name, greeting, colors, avatar, branding, and knowledge base status. Call this once when your app starts to configure the chat UI before the first message.
curl https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123 \
-H "Authorization: Bearer sk_live_your_key_here"
{
"success": true,
"data": {
"id": 16,
"slug": "support-bot-XyZ123",
"name": "Support Bot",
"display_name": "Support Bot",
"description": "Handles product and billing questions",
"language": "en",
"is_active": true,
"greeting_message": "Hi! How can I help you today?",
"fallback_message": "I don't have that information. Please contact support.",
"placeholder_text": "Type your question...",
"has_knowledge_base": true,
"created_at": "2026-01-15T09:00:00Z",
"appearance": {
"primary_color": "#667eea",
"bubble_color": "#667eea",
"widget_theme": "light",
"profile_image": "https://aichatvault.com/storage/avatars/bot-16.png",
"chat_icon": null,
"bubble_message": "Hi! Need any help?",
"show_bubble_message": true
},
"branding": {
"required": true,
"text": "Powered by AiChatVault",
"url": "https://www.aichatvault.com",
"icon_url": "https://aichatvault.com/images/logo_icon.png"
}
}
}| Field | Description |
|---|---|
| display_name | Bot name to show in your chat header (may differ from the internal name). |
| greeting_message | Show this as the opening bot message when the chat loads — no extra API call needed. |
| placeholder_text | Suggested placeholder for your message input box. |
| appearance.primary_color | Hex color for the send button, header background, and user message bubbles. |
| appearance.bubble_color | Color for the floating chat bubble launcher (if you build one). |
| appearance.widget_theme | "light" or "dark" — use this to set the initial color scheme of your chat UI. |
| appearance.profile_image | URL of the bot avatar image. Show this in the chat header and next to bot messages. |
| branding.required | true when the account has not purchased the Brand Removal add-on. When true, every reply already contains an embedded "Powered by AiChatVault" block — you do not need to add it yourself. |
| branding.icon_url | URL of the AiChatVault logo icon — already embedded in reply HTML when branding.required is true. |
greeting_message as the first message displayed in your UI when the chat opens — this way your UX matches the widget exactly without an extra API call.POST /chatbots/{slug}/conversations
Creates a new conversation session. Call this once when a user opens the chat, then reuse the returned conversation_id for all their messages.
| Field | Type | Required | Description |
|---|---|---|---|
| visitor_id | string | No | Your identifier for this user (e.g. user UUID from your database). Passed through for analytics. |
| visitor_name | string | No | Display name of the visitor |
| visitor_email | string | No | Email address of the visitor |
| visitor_phone | string | No | Phone number of the visitor |
| metadata | object | No | Any custom key-value data to attach to this conversation |
curl -X POST https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"visitor_id": "user-abc-789",
"visitor_name": "Alice Smith",
"visitor_email": "alice@example.com",
"metadata": { "plan": "pro", "account_age_days": 90 }
}'
{
"success": true,
"data": {
"conversation_id": 1802,
"session_id": "api_6a63b583280b0",
"visitor_id": "user-abc-789",
"started_at": "2026-07-24T17:20:49Z",
"is_ended": false
}
}conversation_id and session_id from this response. The conversation_id is used in all subsequent endpoint URLs. The session_id is used internally by the rating system to link thumbs up/down feedback to this conversation in your analytics dashboard — pass it through to the rate endpoint.POST /chatbots/{slug}/conversations/{id}/messages
Sends a user message and returns the AI-generated reply. This is the core endpoint — call it each time the user submits a message.
| Field | Type | Required | Description |
|---|---|---|---|
| message | string | Yes | The user's message text (max 10,000 characters) |
curl -X POST https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations/1802/messages \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "message": "What is your refund policy?" }'
{
"success": true,
"data": {
"reply": "<p>We offer full refunds within 30 days of purchase...</p><div class="acv-branding" style="...">...</div>",
"conversation_id": 1802,
"message_id": 6674,
"cannot_answer": false,
"sources": [
{ "name": "Refund Policy", "type": "url", "score": 0.92, "url": "https://example.com/refunds" }
],
"actions": [],
"model": "gpt-4o-mini",
"response_ms": 1823
},
"meta": {
"credits_used": 3,
"credits_max": 500,
"credits_remaining": 497,
"this_message": 1
}
}Understanding the message response
| Field | Description |
|---|---|
| data.reply | Pre-formatted HTML. Render directly as innerHTML in your chat bubble. Markdown (bold, lists, code blocks) is already converted. When branding is required, a "Powered by AiChatVault" block with logo is embedded at the end — it cannot be removed without the Brand Removal add-on. |
| data.message_id | The database ID of the assistant message. Store this and pass it to the rate endpoint to record thumbs up/down feedback. |
| data.cannot_answer | true when the bot has no relevant information in its knowledge base. Show a "Contact us" option in your UI when this is true. |
| data.sources | Array of knowledge base sources used. Each source has { name, type, score, url }. When type is "url" and url is set, render it as a clickable link so users can read the original content. |
| data.actions | Any AI actions triggered during this message (e.g. email sent, webhook fired). |
| data.model | The AI model used for this response (e.g. gpt-4o-mini, gemini-2.5-flash). |
| data.response_ms | Time in milliseconds the AI took to generate the reply. Useful for latency monitoring. |
| meta.credits_used | Total credits consumed by your organisation this month. |
| meta.credits_remaining | Credits left before your plan limit. Monitor this to avoid interruptions. |
| meta.this_message | Credits deducted for this single message (varies by AI model). |
meta.credits_remaining reaches 0, the next message will return HTTP 429. Show a warning in your UI before this happens and direct the user to upgrade their plan.reply field is pre-formatted HTML. On web, set it as innerHTML of your message bubble. On mobile, use a WebView or an HTML renderer component. To copy the plain text to clipboard, strip HTML tags and remove the .acv-branding element.POST /chatbots/{slug}/conversations/{id}/messages/rate
Records a thumbs up or thumbs down rating for an assistant message. Ratings appear in your dashboard analytics (Conversations → feedback column and the Analytics → Feedback chart). You can call this after any assistant message — typically when the user taps the 👍 or 👎 button in your UI.
| Field | Type | Required | Description |
|---|---|---|---|
| message_id | integer | Yes | The message_id returned by the send_message endpoint. Identifies which assistant message is being rated. |
| rating | string | Yes | "good" (thumbs up) or "bad" (thumbs down) |
curl -X POST https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations/1802/messages/rate \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"message_id": 6674,
"rating": "good"
}'
{
"success": true,
"data": {
"message_id": 6674,
"rating": "good"
}
}message_id from each send_message response alongside the displayed message. When the user taps 👍 or 👎, POST to this endpoint. Calling it again for the same message updates the rating rather than creating a duplicate.GET /chatbots/{slug}/conversations/{id}
Retrieves the full message history for a conversation. Use this to restore a previous session if the user returns.
curl https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations/1802 \
-H "Authorization: Bearer sk_live_your_key_here"
{
"success": true,
"data": {
"id": 1802,
"visitor_id": "user-abc-789",
"visitor_name": "Alice Smith",
"visitor_email": "alice@example.com",
"is_ended": false,
"source": "api",
"started_at": "2026-07-24T17:20:49Z",
"last_message_at": "2026-07-24T17:21:02Z",
"ended_at": null,
"message_count": 4,
"messages": [
{ "id": 6671, "role": "user", "content": "What is your refund policy?", "sources": [], "created_at": "2026-07-24T17:20:50Z" },
{ "id": 6672, "role": "assistant", "content": "<p>We offer full refunds within 30 days...</p>...", "sources": [{...}], "created_at": "2026-07-24T17:20:52Z" }
]
}
}content is pre-formatted HTML (same as data.reply from send_message) — render it as innerHTML. User content is plain text — escape it before rendering. Regenerated messages are automatically excluded.POST /chatbots/{slug}/conversations/{id}/end
Marks the conversation as ended and records the total duration. Call this when the user closes the chat window or explicitly ends the session. It is safe to call multiple times — subsequent calls are no-ops.
curl -X POST https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations/1802/end \
-H "Authorization: Bearer sk_live_your_key_here"
{
"success": true,
"data": {
"message": "Conversation ended.",
"conversation_id": 1802,
"ended_at": "2026-07-24T17:35:00Z",
"duration_seconds": 851,
"message_count": 8
}
}conversation_ended webhook event.POST /chatbots/{slug}/conversations/{id}/leads
Submits a visitor's contact details as a lead. The lead appears in your Leads dashboard and fires the lead_captured webhook. At least one of name, email, or phone is required.
curl -X POST https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations/1802/leads \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Alice Smith",
"email": "alice@example.com",
"phone": "+1 555-0100",
"message": "Interested in the Pro plan"
}'
{
"success": true,
"data": {
"lead_id": 183,
"conversation_id": 1802,
"name": "Alice Smith",
"email": "alice@example.com",
"phone": "+1 555-0100"
}
}Error reference
| HTTP status | Error | What to do |
|---|---|---|
| 401 | Invalid or missing API key | Check your Authorization header. The key must start with sk_live_. |
| 403 | Chatbot not found or wrong organisation | The slug exists but belongs to a different account. Verify the slug in your dashboard. |
| 404 | Conversation not found | The conversation_id does not exist or belongs to a different agent. |
| 422 | Validation error | Check the errors field in the response for the specific field that failed. |
| 429 | Credit limit reached | Monthly credits exhausted. Upgrade your plan or wait for the monthly reset. |
| 500 | Server error | Retry once. If it persists, contact support with your conversation_id. |
Full integration example (Node.js)
const BASE = 'https://aichatvault.com/api/v1';
const KEY = process.env.AICHATVAULT_API_KEY;
const SLUG = 'support-bot-XyZ123';
const headers = {
'Authorization': `Bearer ${KEY}`,
'Content-Type': 'application/json',
};
// 1. Load chatbot config (name, colors, greeting) — call once on startup
async function getChatbot() {
const res = await fetch(`${BASE}/chatbots/${SLUG}`, { headers });
const { data } = await res.json();
return data; // use data.display_name, data.appearance, data.greeting_message
}
// 2. Open a session when the user starts chatting
async function startChat(userId, userName) {
const res = await fetch(`${BASE}/chatbots/${SLUG}/conversations`, {
method: 'POST', headers,
body: JSON.stringify({ visitor_id: userId, visitor_name: userName }),
});
const { data } = await res.json();
// Store both — conversation_id for all calls, session_id for the rating system
return { conversationId: data.conversation_id, sessionId: data.session_id };
}
// 3. Send each message and get the HTML reply
async function chat(conversationId, userMessage) {
const res = await fetch(`${BASE}/chatbots/${SLUG}/conversations/${conversationId}/messages`, {
method: 'POST', headers,
body: JSON.stringify({ message: userMessage }),
});
const { success, data, meta } = await res.json();
if (!success) throw new Error(data?.error ?? 'Chat request failed');
if (data.cannot_answer) showContactForm(); // bot has no relevant info
if (meta.credits_remaining < 10) console.warn('Credits low!');
// data.reply is pre-formatted HTML — set as innerHTML, not textContent
return { replyHtml: data.reply, messageId: data.message_id, sources: data.sources };
}
// 4. Rate a message after the user taps 👍 or 👎
async function rateMessage(conversationId, messageId, rating) {
await fetch(`${BASE}/chatbots/${SLUG}/conversations/${conversationId}/messages/rate`, {
method: 'POST', headers,
body: JSON.stringify({ message_id: messageId, rating }), // rating: 'good' | 'bad'
});
}
// 5. Capture a lead when you collect contact info
async function saveLead(conversationId, name, email, phone) {
await fetch(`${BASE}/chatbots/${SLUG}/conversations/${conversationId}/leads`, {
method: 'POST', headers,
body: JSON.stringify({ name, email, phone }),
});
}
// 6. Close the session when the user leaves
async function endChat(conversationId) {
await fetch(`${BASE}/chatbots/${SLUG}/conversations/${conversationId}/end`, {
method: 'POST', headers,
});
}
// Example usage
(async () => {
const bot = await getChatbot();
console.log('Bot name:', bot.display_name);
console.log('Greeting:', bot.greeting_message);
const { conversationId } = await startChat('user-123', 'Alice');
const { replyHtml, messageId } = await chat(conversationId, 'What are your pricing plans?');
// In a browser: document.getElementById('bubble').innerHTML = replyHtml;
console.log('Message ID:', messageId);
await rateMessage(conversationId, messageId, 'good'); // user clicked 👍
await saveLead(conversationId, 'Alice Smith', 'alice@example.com', '+1555010');
await endChat(conversationId);
})();Full integration example (Python)
import os, requests
from html.parser import HTMLParser
BASE = 'https://aichatvault.com/api/v1'
KEY = os.environ['AICHATVAULT_API_KEY']
SLUG = 'support-bot-XyZ123'
HDR = {'Authorization': f'Bearer {KEY}', 'Content-Type': 'application/json'}
def get_chatbot() -> dict:
"""Load bot config — name, colors, greeting."""
r = requests.get(f'{BASE}/chatbots/{SLUG}', headers=HDR)
r.raise_for_status()
return r.json()['data']
def start_chat(visitor_id: str, visitor_name: str = None) -> dict:
"""Returns dict with conversation_id and session_id."""
r = requests.post(f'{BASE}/chatbots/{SLUG}/conversations',
json={'visitor_id': visitor_id, 'visitor_name': visitor_name}, headers=HDR)
r.raise_for_status()
return r.json()['data'] # {'conversation_id': 1802, 'session_id': 'api_...', ...}
def chat(conversation_id: int, message: str) -> dict:
"""Returns {'reply_html': str, 'message_id': int, 'sources': list}."""
r = requests.post(f'{BASE}/chatbots/{SLUG}/conversations/{conversation_id}/messages',
json={'message': message}, headers=HDR)
r.raise_for_status()
body = r.json()
if body['data']['cannot_answer']:
print('Bot cannot answer — show contact form')
print(f"Credits remaining: {body['meta']['credits_remaining']}")
return {
'reply_html': body['data']['reply'], # pre-formatted HTML — render as innerHTML
'message_id': body['data']['message_id'],
'sources': body['data']['sources'],
}
def rate_message(conversation_id: int, message_id: int, rating: str) -> None:
"""rating is 'good' or 'bad'."""
requests.post(f'{BASE}/chatbots/{SLUG}/conversations/{conversation_id}/messages/rate',
json={'message_id': message_id, 'rating': rating}, headers=HDR)
def save_lead(conversation_id: int, name: str, email: str, phone: str = None) -> None:
requests.post(f'{BASE}/chatbots/{SLUG}/conversations/{conversation_id}/leads',
json={'name': name, 'email': email, 'phone': phone}, headers=HDR)
def end_chat(conversation_id: int) -> None:
requests.post(f'{BASE}/chatbots/{SLUG}/conversations/{conversation_id}/end', headers=HDR)
# Example usage
bot = get_chatbot()
print(f"Bot: {bot['display_name']} — greeting: {bot['greeting_message']}")
session = start_chat('user-456', 'Bob')
conv_id = session['conversation_id']
result = chat(conv_id, 'Do you offer enterprise pricing?')
print('HTML reply:', result['reply_html'][:80], '...') # render as innerHTML in your UI
rate_message(conv_id, result['message_id'], 'good') # user clicked 👍
save_lead(conv_id, 'Bob Jones', 'bob@example.com')
end_chat(conv_id)Full integration example (PHP)
Works with any PHP framework (Laravel, Symfony, WordPress, plain PHP). Requires ext-curl (standard on all PHP hosts).
<?php
class AiChatVault
{
private string $base = 'https://aichatvault.com/api/v1';
private string $key;
private string $slug;
public function __construct(string $apiKey, string $chatbotSlug)
{
$this->key = $apiKey;
$this->slug = $chatbotSlug;
}
/** Load bot config — display_name, greeting_message, appearance, branding. */
public function getChatbot(): array
{
return $this->get("/chatbots/{$this->slug}");
}
/** Returns ['conversation_id' => int, 'session_id' => string, ...]. */
public function startConversation(string $visitorId, ?string $visitorName = null): array
{
return $this->post("/chatbots/{$this->slug}/conversations", [
'visitor_id' => $visitorId,
'visitor_name' => $visitorName,
]);
}
/**
* Returns ['reply' => HTML string, 'message_id' => int, 'cannot_answer' => bool,
* 'sources' => array, 'model' => string, 'response_ms' => int].
* reply is pre-formatted HTML — set as innerHTML, not textContent.
*/
public function sendMessage(int $conversationId, string $message): array
{
return $this->post("/chatbots/{$this->slug}/conversations/{$conversationId}/messages", [
'message' => $message,
]);
}
/** Rate a message 'good' or 'bad' (thumbs up / thumbs down). */
public function rateMessage(int $conversationId, int $messageId, string $rating): void
{
$this->post("/chatbots/{$this->slug}/conversations/{$conversationId}/messages/rate", [
'message_id' => $messageId,
'rating' => $rating,
]);
}
public function getHistory(int $conversationId): array
{
return $this->get("/chatbots/{$this->slug}/conversations/{$conversationId}");
}
public function endConversation(int $conversationId): void
{
$this->post("/chatbots/{$this->slug}/conversations/{$conversationId}/end", []);
}
public function saveLead(int $conversationId, string $name, string $email, ?string $phone = null): void
{
$this->post("/chatbots/{$this->slug}/conversations/{$conversationId}/leads", [
'name' => $name,
'email' => $email,
'phone' => $phone,
]);
}
private function get(string $path): array
{
return $this->request('GET', $path);
}
private function post(string $path, array $body): array
{
return $this->request('POST', $path, $body);
}
private function request(string $method, string $path, ?array $body = null): array
{
$ch = curl_init($this->base . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->key,
'Content-Type: application/json',
'Accept: application/json',
],
]);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new \RuntimeException("AiChatVault API error {$status}: {$response}");
}
$json = json_decode($response, true);
return $json['data'] ?? $json;
}
}
// Usage
$bot = new AiChatVault($_ENV['AICHATVAULT_API_KEY'], 'support-bot-XyZ123');
// Load bot config once on startup
$config = $bot->getChatbot();
echo "Bot: " . $config['display_name'] . "\n";
echo "Greeting: " . $config['greeting_message'] . "\n";
// Start a conversation
$session = $bot->startConversation('user-789', 'Charlie');
$convId = $session['conversation_id']; // use in all subsequent calls
// Send a message — reply is pre-formatted HTML
$result = $bot->sendMessage($convId, 'What payment methods do you accept?');
echo $result['reply']; // render as innerHTML in your UI
if ($result['cannot_answer']) {
echo "Bot could not answer — showing contact form.";
}
// Rate the message (thumbs up)
$bot->rateMessage($convId, $result['message_id'], 'good');
$bot->saveLead($convId, 'Charlie Brown', 'charlie@example.com', '+447700900000');
$bot->endConversation($convId);Full integration example (.NET / C#)
Uses HttpClient and System.Text.Json — both built into .NET 6+. No NuGet packages required.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
public class AiChatVaultClient
{
private readonly HttpClient _http;
private readonly string _slug;
private const string Base = "https://aichatvault.com/api/v1";
public AiChatVaultClient(string apiKey, string chatbotSlug)
{
_slug = chatbotSlug;
_http = new HttpClient();
_http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
_http.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
}
public async Task<int> StartConversationAsync(string visitorId, string? visitorName = null)
{
var body = new { visitor_id = visitorId, visitor_name = visitorName };
var res = await PostAsync($"/chatbots/{_slug}/conversations", body);
return res.GetProperty("data").GetProperty("conversation_id").GetInt32();
}
public async Task<ChatReply> SendMessageAsync(int conversationId, string message)
{
var body = new { message };
var res = await PostAsync($"/chatbots/{_slug}/conversations/{conversationId}/messages", body);
var data = res.GetProperty("data");
var meta = res.GetProperty("meta");
return new ChatReply(
Reply: data.GetProperty("reply").GetString()!,
CannotAnswer: data.GetProperty("cannot_answer").GetBoolean(),
CreditsRemaining: meta.GetProperty("credits_remaining").GetInt32()
);
}
public async Task EndConversationAsync(int conversationId)
=> await PostAsync($"/chatbots/{_slug}/conversations/{conversationId}/end", new { });
public async Task SaveLeadAsync(int conversationId, string name, string email, string? phone = null)
=> await PostAsync($"/chatbots/{_slug}/conversations/{conversationId}/leads",
new { name, email, phone });
private async Task<JsonElement> PostAsync(string path, object body)
{
var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await _http.PostAsync(Base + path, content);
var json = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"AiChatVault error {(int)response.StatusCode}: {json}");
return JsonDocument.Parse(json).RootElement;
}
}
public record ChatReply(string Reply, bool CannotAnswer, int CreditsRemaining);
// Program.cs — usage
var bot = new AiChatVaultClient(
Environment.GetEnvironmentVariable("AICHATVAULT_API_KEY")!,
"support-bot-XyZ123");
int convId = await bot.StartConversationAsync("user-101", "Dana");
var reply = await bot.SendMessageAsync(convId, "How do I reset my password?");
Console.WriteLine($"Bot: {reply.Reply}");
if (reply.CannotAnswer) Console.WriteLine("Showing contact form...");
if (reply.CreditsRemaining < 20) Console.WriteLine("Warning: credits low!");
await bot.SaveLeadAsync(convId, "Dana Lee", "dana@example.com");
await bot.EndConversationAsync(convId);Full integration example (React Native)
Works with Expo and bare React Native. The API key must live on your backend server — never bundle it in the app. The example below shows a minimal backend-proxy pattern.
// ─── YOUR BACKEND (Node.js / any language) ──────────────────────
// Create a thin proxy so the API key never leaves your server.
// POST /proxy/chat → calls AiChatVault and returns the reply
app.post('/proxy/start', async (req, res) => {
const r = await fetch('https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.AICHATVAULT_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ visitor_id: req.body.userId, visitor_name: req.body.name }),
});
res.json(await r.json());
});
app.post('/proxy/message', async (req, res) => {
const { conversationId, message } = req.body;
const r = await fetch(`https://aichatvault.com/api/v1/chatbots/support-bot-XyZ123/conversations/${conversationId}/messages`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.AICHATVAULT_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
res.json(await r.json());
});
// ─── REACT NATIVE APP ─────────────────────────────────────────
import React, { useState, useRef } from 'react';
import { View, TextInput, FlatList, Text, TouchableOpacity, StyleSheet } from 'react-native';
const YOUR_BACKEND = 'https://your-backend.com';
export default function ChatScreen({ userId, userName }) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [convId, setConvId] = useState(null);
const [loading, setLoading] = useState(false);
const ensureConversation = async () => {
if (convId) return convId;
const res = await fetch(`${YOUR_BACKEND}/proxy/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, name: userName }),
});
const { data } = await res.json();
setConvId(data.conversation_id);
return data.conversation_id;
};
const send = async () => {
if (!input.trim() || loading) return;
const userMsg = input.trim();
setInput('');
setMessages(prev => [...prev, { role: 'user', content: userMsg }]);
setLoading(true);
try {
const id = await ensureConversation();
const res = await fetch(`${YOUR_BACKEND}/proxy/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ conversationId: id, message: userMsg }),
});
const { data } = await res.json();
setMessages(prev => [...prev, { role: 'assistant', content: data.reply }]);
} finally {
setLoading(false);
}
};
return (
<View style={styles.container}>
<FlatList
data={messages}
keyExtractor={(_, i) => String(i)}
renderItem={({ item }) => (
<View style={[styles.bubble, item.role === 'user' ? styles.user : styles.bot]}>
<Text style={styles.text}>{item.content}</Text>
</View>
)}
/>
<View style={styles.inputRow}>
<TextInput
style={styles.input}
value={input}
onChangeText={setInput}
placeholder="Type a message..."
onSubmitEditing={send}
/>
<TouchableOpacity onPress={send} style={styles.sendBtn} disabled={loading}>
<Text style={styles.sendText}>{loading ? '...' : 'Send'}</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f5f5' },
bubble: { margin: 8, padding: 12, borderRadius: 16, maxWidth: '80%' },
user: { alignSelf: 'flex-end', backgroundColor: '#6366f1' },
bot: { alignSelf: 'flex-start', backgroundColor: '#fff', borderWidth: 1, borderColor: '#e5e7eb' },
text: { fontSize: 15 },
inputRow: { flexDirection: 'row', padding: 8, backgroundColor: '#fff', borderTopWidth: 1, borderColor: '#e5e7eb' },
input: { flex: 1, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 20, paddingHorizontal: 14, paddingVertical: 8, fontSize: 15 },
sendBtn: { marginLeft: 8, backgroundColor: '#6366f1', borderRadius: 20, paddingHorizontal: 18, justifyContent: 'center' },
sendText: { color: '#fff', fontWeight: '600' },
});sk_live_ API key directly in a React Native (or any mobile) app. App bundles can be extracted and the key stolen. Always proxy calls through your own backend server.Full integration example (Flutter / Dart)
Uses http package (flutter pub add http). Same backend-proxy pattern as React Native — the API key lives on your server.
// ─── chat_service.dart ────────────────────────────────────────
// Calls your backend proxy, not AiChatVault directly.
import 'dart:convert';
import 'package:http/http.dart' as http;
class ChatService {
final String backendBase;
int? _conversationId;
ChatService({required this.backendBase});
Future<int> _ensureConversation(String userId, String? userName) async {
if (_conversationId != null) return _conversationId!;
final res = await http.post(
Uri.parse('$backendBase/proxy/start'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'userId': userId, 'name': userName}),
);
final data = jsonDecode(res.body)['data'];
_conversationId = data['conversation_id'] as int;
return _conversationId!;
}
Future<ChatReply> sendMessage(String userId, String message, {String? userName}) async {
final convId = await _ensureConversation(userId, userName);
final res = await http.post(
Uri.parse('$backendBase/proxy/message'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'conversationId': convId, 'message': message}),
);
if (res.statusCode != 200) throw Exception('Chat error: ${res.body}');
final body = jsonDecode(res.body);
return ChatReply.fromJson(body['data'], body['meta']);
}
Future<void> endConversation() async {
if (_conversationId == null) return;
await http.post(
Uri.parse('$backendBase/proxy/end'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'conversationId': _conversationId}),
);
_conversationId = null;
}
}
class ChatReply {
final String reply;
final bool cannotAnswer;
final int creditsRemaining;
ChatReply({required this.reply, required this.cannotAnswer, required this.creditsRemaining});
factory ChatReply.fromJson(Map<String, dynamic> data, Map<String, dynamic> meta) => ChatReply(
reply: data['reply'] as String,
cannotAnswer: data['cannot_answer'] as bool,
creditsRemaining: meta['credits_remaining'] as int,
);
}
// ─── chat_screen.dart ─────────────────────────────────────────
import 'package:flutter/material.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _service = ChatService(backendBase: 'https://your-backend.com');
final _controller = TextEditingController();
final _messages = <Map<String, String>>[];
bool _loading = false;
Future<void> _send() async {
final text = _controller.text.trim();
if (text.isEmpty || _loading) return;
_controller.clear();
setState(() { _messages.add({'role': 'user', 'content': text}); _loading = true; });
try {
final reply = await _service.sendMessage('user-123', text, userName: 'Alice');
setState(() => _messages.add({'role': 'assistant', 'content': reply.reply}));
if (reply.cannotAnswer) _showContactForm();
} catch (e) {
setState(() => _messages.add({'role': 'assistant', 'content': 'Sorry, something went wrong.'}));
} finally {
setState(() => _loading = false);
}
}
void _showContactForm() => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('I could not find that — would you like to contact us?')));
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Support Chat')),
body: Column(children: [
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _messages.length,
itemBuilder: (_, i) {
final m = _messages[i];
final me = m['role'] == 'user';
return Align(
alignment: me ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: me ? const Color(0xFF6366F1) : Colors.white,
borderRadius: BorderRadius.circular(18),
border: me ? null : Border.all(color: const Color(0xFFE5E7EB)),
),
child: Text(m['content']!, style: TextStyle(color: me ? Colors.white : Colors.black87, fontSize: 15)),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Row(children: [
Expanded(child: TextField(controller: _controller, onSubmitted: (_) => _send(), decoration: const InputDecoration(hintText: 'Type a message...', border: OutlineInputBorder()))),
const SizedBox(width: 8),
ElevatedButton(onPressed: _loading ? null : _send, child: _loading ? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Send')),
]),
),
]),
);
}Full integration example (Android — Kotlin)
Uses OkHttp and Gson (add to build.gradle: implementation 'com.squareup.okhttp3:okhttp:4.12.0' and implementation 'com.google.code.gson:gson:2.10.1'). API calls are made through your backend proxy.
// ChatRepository.kt — calls your backend proxy
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
data class ChatReply(val reply: String, val cannotAnswer: Boolean, val creditsRemaining: Int)
class ChatRepository(private val backendBase: String) {
private val client = OkHttpClient()
private val gson = Gson()
private val json = "application/json".toMediaType()
private var conversationId: Int? = null
private suspend fun post(path: String, body: Map<String, Any?>): Map<*, *> = withContext(Dispatchers.IO) {
val req = Request.Builder()
.url("$backendBase$path")
.post(gson.toJson(body).toRequestBody(json))
.build()
val res = client.newCall(req).execute()
gson.fromJson(res.body!!.string(), Map::class.java)
}
private suspend fun ensureConversation(userId: String, userName: String?): Int {
conversationId?.let { return it }
val res = post("/proxy/start", mapOf("userId" to userId, "name" to userName))
val data = res["data"] as Map<*, *>
conversationId = (data["conversation_id"] as Double).toInt()
return conversationId!!
}
suspend fun sendMessage(userId: String, message: String, userName: String? = null): ChatReply {
val convId = ensureConversation(userId, userName)
val res = post("/proxy/message", mapOf("conversationId" to convId, "message" to message))
val data = res["data"] as Map<*, *>
val meta = res["meta"] as Map<*, *>
return ChatReply(
reply = data["reply"] as String,
cannotAnswer = data["cannot_answer"] as Boolean,
creditsRemaining = (meta["credits_remaining"] as Double).toInt()
)
}
suspend fun endConversation() {
conversationId?.let { post("/proxy/end", mapOf("conversationId" to it)) }
conversationId = null
}
}
// ChatViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
data class Message(val role: String, val content: String)
class ChatViewModel : ViewModel() {
private val repo = ChatRepository("https://your-backend.com")
private val _messages = MutableStateFlow<List<Message>>(emptyList())
val messages: StateFlow<List<Message>> = _messages
val loading = MutableStateFlow(false)
fun send(message: String, userId: String) {
viewModelScope.launch {
_messages.value += Message("user", message)
loading.value = true
try {
val reply = repo.sendMessage(userId, message)
_messages.value += Message("assistant", reply.reply)
} catch (e: Exception) {
_messages.value += Message("assistant", "Something went wrong. Please try again.")
} finally {
loading.value = false
}
}
}
override fun onCleared() { viewModelScope.launch { repo.endConversation() } }
}Full integration example (iOS — Swift)
Uses URLSession (no third-party dependencies). Requires iOS 15+ for async/await. API key is kept on your backend server.
// ChatService.swift — calls your backend proxy
import Foundation
struct ChatReply: Decodable {
let reply: String
let cannotAnswer: Bool
let creditsRemaining: Int
enum CodingKeys: String, CodingKey {
case reply
case cannotAnswer = "cannot_answer"
case creditsRemaining = "credits_remaining"
}
}
@MainActor
class ChatService: ObservableObject {
private let backendBase: String
private var conversationId: Int?
@Published var messages: [(role: String, content: String)] = []
@Published var isLoading = false
init(backendBase: String) { self.backendBase = backendBase }
func send(_ text: String, userId: String, userName: String? = nil) async {
messages.append((role: "user", content: text))
isLoading = true
defer { isLoading = false }
do {
let id = try await ensureConversation(userId: userId, userName: userName)
let reply = try await postMessage(conversationId: id, message: text)
messages.append((role: "assistant", content: reply.reply))
} catch {
messages.append((role: "assistant", content: "Something went wrong. Please try again."))
}
}
func endConversation() async {
guard let id = conversationId else { return }
_ = try? await post(path: "/proxy/end", body: ["conversationId": id])
conversationId = nil
}
// MARK: - Private
private func ensureConversation(userId: String, userName: String?) async throws -> Int {
if let id = conversationId { return id }
var body: [String: Any] = ["userId": userId]
if let name = userName { body["name"] = name }
let res = try await post(path: "/proxy/start", body: body)
let data = res["data"] as? [String: Any]
let id = data?["conversation_id"] as? Int ?? 0
conversationId = id
return id
}
private func postMessage(conversationId: Int, message: String) async throws -> ChatReply {
let res = try await post(path: "/proxy/message", body: ["conversationId": conversationId, "message": message])
let data = res["data"] as? [String: Any] ?? [:]
let meta = res["meta"] as? [String: Any] ?? [:]
return ChatReply(
reply: data["reply"] as? String ?? "",
cannotAnswer: data["cannot_answer"] as? Bool ?? false,
creditsRemaining: meta["credits_remaining"] as? Int ?? 0
)
}
private func post(path: String, body: [String: Any]) async throws -> [String: Any] {
var req = URLRequest(url: URL(string: backendBase + path)!)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, _) = try await URLSession.shared.data(for: req)
return try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
}
}
// ContentView.swift — SwiftUI chat UI
import SwiftUI
struct ChatView: View {
@StateObject private var service = ChatService(backendBase: "https://your-backend.com")
@State private var input = ""
let userId = "user-456"
let userName = "Eve"
var body: some View {
VStack(spacing: 0) {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(Array(service.messages.enumerated()), id: \.offset) { i, msg in
HStack {
if msg.role == "user" { Spacer() }
Text(msg.content)
.padding(12)
.background(msg.role == "user" ? Color.indigo : Color(.systemGray6))
.foregroundColor(msg.role == "user" ? .white : .primary)
.clipShape(RoundedRectangle(cornerRadius: 16))
.frame(maxWidth: 280, alignment: msg.role == "user" ? .trailing : .leading)
if msg.role == "assistant" { Spacer() }
}
.id(i)
}
}
.padding()
}
.onChange(of: service.messages.count) { _ in
proxy.scrollTo(service.messages.count - 1, anchor: .bottom)
}
}
Divider()
HStack(spacing: 8) {
TextField("Type a message...", text: $input)
.textFieldStyle(.roundedBorder)
.onSubmit { Task { await sendMessage() } }
Button(action: { Task { await sendMessage() } }) {
Image(systemName: service.isLoading ? "ellipsis" : "arrow.up.circle.fill")
.font(.title2)
.foregroundColor(.indigo)
}
.disabled(service.isLoading || input.trimmingCharacters(in: .whitespaces).isEmpty)
}
.padding()
}
.navigationTitle("Support Chat")
.task { }
}
private func sendMessage() async {
let text = input.trimmingCharacters(in: .whitespaces)
guard !text.isEmpty else { return }
input = ""
await service.send(text, userId: userId, userName: userName)
}
}Security best practices
- •Never expose your API key in frontend JavaScript or mobile app binaries. Always make REST API calls from your server and proxy results to the client.
- •Store the key in an environment variable (AICHATVAULT_API_KEY), not in source code.
- •Rotate keys periodically using Settings → API Keys → Regenerate.
- •Create separate keys for each environment (development, staging, production) so you can revoke one without affecting others.
- •Monitor credits_remaining in API responses and set up an alert before limits are reached.
Attribution compliance checklist
- •Render
data.replyasinnerHTML— do not strip HTML tags or parse out theacv-brandingelement. - •Do not apply CSS that sets
display:none,visibility:hidden,opacity:0, or negative positioning to the.acv-brandingelement or any of its ancestors. - •On mobile (React Native, Flutter, iOS, Android), display the HTML reply using a WebView or HTML renderer — do not extract only the text content.
- •When loading conversation history (
GET …/conversations/{id}), render assistant messages the same way — attribution is embedded there too. - •If you have the Brand Removal add-on active, the attribution block will not be present in API responses — no special handling needed.
- •Keep a copy of your key creation confirmation as evidence of your attribution agreement — this is logged automatically with timestamp and IP.
source: "api" in your dashboard. You can filter by this source in the Conversations section to see only API-originated sessions.Was this page helpful?
