Docs/Developer Guide/REST API Integration

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.

⚠️
Attribution requirement — legally binding. Every chat interface built using this API must display the "Powered by AiChatVault" attribution to end users. The API embeds this attribution directly inside the 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 caseRecommended approach
Public website or landing pageWidget embed — paste one script tag, done
Dedicated support pageIframe embed
Custom mobile app (iOS, Android, Flutter)REST API — full control of UI
Internal CRM or dashboardREST API — render inside your own components
Server-to-server automationREST API — no browser needed
Existing chat UI you want to power with AIREST 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}/conversations to open a session and receive a conversation_id and session_id. Store both.
  • 3. For each user message, call POST …/conversations/{id}/messages. Set data.reply as innerHTML of your message bubble — it is pre-formatted HTML.
  • 4. Optionally call POST …/messages/rate after 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
ℹ️
Your API key is scoped to your organisation. You can only access chatbots you own. See the API Keys page for how to create and manage keys.

Quick start — 3 steps

1

Verify your key

Confirm authentication is working before building anything:
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" }
}
2

Get your chatbot slug

List your chatbots to find the slug you need:
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 }
}
3

Start a conversation and send a message

Use the slug to open a session and get an AI reply:
# 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...", ... } }
💡
Save the 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

MethodEndpointDescription
GET/api/v1/chatbotsList all your active chatbots
GET/api/v1/chatbots/{slug}Get chatbot config — name, greeting, colors, branding, knowledge base status
POST/api/v1/chatbots/{slug}/conversationsStart a new conversation session
POST/api/v1/chatbots/{slug}/conversations/{id}/messagesSend a user message and receive an AI reply (HTML formatted)
POST/api/v1/chatbots/{slug}/conversations/{id}/messages/rateRate 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}/endEnd a conversation and record its duration
POST/api/v1/chatbots/{slug}/conversations/{id}/leadsSubmit 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-256.webp"
    }
  }
}
FieldDescription
display_nameBot name to show in your chat header (may differ from the internal name).
greeting_messageShow this as the opening bot message when the chat loads — no extra API call needed.
placeholder_textSuggested placeholder for your message input box.
appearance.primary_colorHex color for the send button, header background, and user message bubbles.
appearance.bubble_colorColor 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_imageURL of the bot avatar image. Show this in the chat header and next to bot messages.
branding.requiredtrue 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_urlURL of the AiChatVault logo icon — already embedded in reply HTML when branding.required is true.
💡
Use 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.

FieldTypeRequiredDescription
visitor_idstringNoYour identifier for this user (e.g. user UUID from your database). Passed through for analytics.
visitor_namestringNoDisplay name of the visitor
visitor_emailstringNoEmail address of the visitor
visitor_phonestringNoPhone number of the visitor
metadataobjectNoAny 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
  }
}
ℹ️
Store both 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.

FieldTypeRequiredDescription
messagestringYesThe 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

FieldDescription
data.replyPre-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_idThe database ID of the assistant message. Store this and pass it to the rate endpoint to record thumbs up/down feedback.
data.cannot_answertrue when the bot has no relevant information in its knowledge base. Show a "Contact us" option in your UI when this is true.
data.sourcesArray 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.actionsAny AI actions triggered during this message (e.g. email sent, webhook fired).
data.modelThe AI model used for this response (e.g. gpt-4o-mini, gemini-2.5-flash).
data.response_msTime in milliseconds the AI took to generate the reply. Useful for latency monitoring.
meta.credits_usedTotal credits consumed by your organisation this month.
meta.credits_remainingCredits left before your plan limit. Monitor this to avoid interruptions.
meta.this_messageCredits deducted for this single message (varies by AI model).
⚠️
If 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.
ℹ️
The 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.

FieldTypeRequiredDescription
message_idintegerYesThe message_id returned by the send_message endpoint. Identifies which assistant message is being rated.
ratingstringYes"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"
  }
}
💡
Store the 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" }
    ]
  }
}
ℹ️
Messages are in chronological order. Assistant 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
  }
}
💡
Ended conversations still appear in your dashboard analytics and trigger the 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 statusErrorWhat to do
401Invalid or missing API keyCheck your Authorization header. The key must start with sk_live_.
403Chatbot not found or wrong organisationThe slug exists but belongs to a different account. Verify the slug in your dashboard.
404Conversation not foundThe conversation_id does not exist or belongs to a different agent.
422Validation errorCheck the errors field in the response for the specific field that failed.
429Credit limit reachedMonthly credits exhausted. Upgrade your plan or wait for the monthly reset.
500Server errorRetry 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' },
});
⚠️
Never embed your 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.reply as innerHTML — do not strip HTML tags or parse out the acv-branding element.
  • Do not apply CSS that sets display:none, visibility:hidden, opacity:0, or negative positioning to the .acv-branding element 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.
ℹ️
Conversations created via the REST API are tagged with 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?