Skip to main content

Developers

The Morv Chatbot API allows you to integrate AI-powered chatbot conversations into your applications. Use it to send messages, retrieve conversation history, and configure chatbot behaviour programmatically.

Base URL https://morv.ai/api/v1

All requests and responses use JSON. Include Content-Type: application/json for POST requests.

Quick Start

Send your first message to the chatbot:

curl -X POST https://morv.ai/api/v1/chatbot/message \
  -H "X-API-Key: pk_live_your_public_key" \
  -H "X-API-Secret: sk_live_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_id": "bot_YOUR_BOT_ID",
    "session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "message": "Hello!"
  }'
const response = await fetch("https://morv.ai/api/v1/chatbot/message", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "pk_live_your_public_key",
    "X-API-Secret": "sk_live_your_secret_key"
  },
  body: JSON.stringify({
    bot_id: "bot_YOUR_BOT_ID",
    session_id: crypto.randomUUID().replace(/-/g, ""),
    message: "Hello!"
  })
});

const data = await response.json();
console.log(data.data.response); // Bot's reply
$ch = curl_init("https://morv.ai/api/v1/chatbot/message");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        "Content-Type: application/json",
        "X-API-Key: pk_live_your_public_key",
        "X-API-Secret: sk_live_your_secret_key",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "bot_id"     => "bot_YOUR_BOT_ID",
        "session_id" => bin2hex(random_bytes(16)),
        "message"    => "Hello!",
    ]),
]);

$response = json_decode(curl_exec($ch), true);
curl_close($ch);

echo $response["data"]["response"]; // Bot's reply
import requests, secrets

response = requests.post(
    "https://morv.ai/api/v1/chatbot/message",
    headers={
        "X-API-Key": "pk_live_your_public_key",
        "X-API-Secret": "sk_live_your_secret_key",
    },
    json={
        "bot_id": "bot_YOUR_BOT_ID",
        "session_id": secrets.token_hex(16),
        "message": "Hello!",
    },
)

data = response.json()
print(data["data"]["response"])  # Bot's reply
require "net/http"
require "json"
require "securerandom"

uri = URI("https://morv.ai/api/v1/chatbot/message")
http = Net::HTTP.new(uri.host, uri.port)

request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request["X-API-Key"] = "pk_live_your_public_key"
request["X-API-Secret"] = "sk_live_your_secret_key"
request.body = {
  bot_id: "bot_YOUR_BOT_ID",
  session_id: SecureRandom.hex(16),
  message: "Hello!"
}.to_json

response = http.request(request)
data = JSON.parse(response.body)
puts data["data"]["response"] # Bot's reply
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    body, _ := json.Marshal(map[string]string{
        "bot_id":     "bot_YOUR_BOT_ID",
        "session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
        "message":    "Hello!",
    })

    req, _ := http.NewRequest("POST", "https://morv.ai/api/v1/chatbot/message", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-API-Key", "pk_live_your_public_key")
    req.Header.Set("X-API-Secret", "sk_live_your_secret_key")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    data := result["data"].(map[string]interface{})
    fmt.Println(data["response"]) // Bot's reply
}
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "pk_live_your_public_key");
client.DefaultRequestHeaders.Add("X-API-Secret", "sk_live_your_secret_key");

var payload = new {
    bot_id = "bot_YOUR_BOT_ID",
    session_id = Guid.NewGuid().ToString("N"),
    message = "Hello!"
};

var content = new StringContent(
    JsonSerializer.Serialize(payload),
    Encoding.UTF8, "application/json");

var response = await client.PostAsync(
    "https://morv.ai/api/v1/chatbot/message", content);

var json = await response.Content.ReadAsStringAsync();
var data = JsonDocument.Parse(json);
Console.WriteLine(data.RootElement
    .GetProperty("data")
    .GetProperty("response")
    .GetString()); // Bot's reply

For browser-based integrations where API keys shouldn't be exposed, use domain allowlist authentication instead, or try the embeddable widget for a drop-in solution.

Authentication

The API supports two authentication methods. Use whichever suits your integration:

1. Domain Allowlist (Browser/Widget)

For client-side integrations, the API validates the Origin header against your chatbot's configured allowed domains. No API keys needed — the browser sends the Origin header automatically.

Configure allowed domains in your chatbot settings. Wildcard subdomains are supported (e.g. *.example.com).

Requests to /chatbot/message and /chatbot/history must also send a session token in the X-Chatbot-Session-Token header. It is returned by the config call as data.session_token (alongside data.session_token_ttl), and may be rotated in later message/history responses — resend the most recent one. The embeddable widget handles this automatically. A missing token returns 401 (errors.code: token_required); an expired or invalid one also returns 401.

2. API Keys (Server-Side)

For server-side integrations, scripts, or testing, include your API key pair in request headers:

Headers
X-API-Key: pk_live_your_public_key
X-API-Secret: sk_live_your_secret_key

Generate API keys from your project dashboard under API Keys. Both headers are required — partial credentials return 401.

Never expose your secret key (sk_live_...) in client-side code or public repositories. It is only shown once at creation.

CORS

The API supports Cross-Origin Resource Sharing (CORS) for browser-based requests. Preflight OPTIONS requests are handled automatically.

Allowed MethodsGET, POST, OPTIONS
Allowed HeadersContent-Type, X-API-Key, X-API-Secret, X-Chatbot-Session-Token
Max Age86,400 seconds (24 hours)

The Access-Control-Allow-Origin header echoes the request origin when the domain is in your chatbot's allowlist or valid API keys are provided.

Rate Limiting

API requests are rate-limited per chatbot per IP address.

Default limit20 requests per 60 seconds
ScopePer bot_id + IP address
Exceeded response429 Too Many Requests

Additional per-IP and per-endpoint limits apply on top of this (and are tightened for traffic flagged as suspicious), so treat a 429 as "back off and retry shortly" rather than a single fixed budget. For IPv6 clients the per-IP scope is the /64 prefix.

Three 429 codes are not short-lived, and retrying them within seconds achieves nothing: daily_budget_reached and ip_daily_budget_reached clear at midnight (UK time), and llm_cap_reached clears at the start of the owner's next billing period (or when they buy a top-up). Check errors.code before retrying.

Response Format

All responses use a consistent JSON envelope:

Success Response
{
  "success": true,
  "message": "Success",
  "data": { ... }
}
Error Response
{
  "success": false,
  "message": "Description of what went wrong"
}

Some errors also carry a machine-readable errors.code (e.g. token_required, unavailable, ip_blocked, api_key_rate_limited) alongside the human-readable message.

HTTP Status Codes

CodeMeaning
200Success
400Bad request — missing or invalid parameters
401Unauthorized — invalid API credentials, or a missing/expired widget session token. A request with no API credentials is treated as a widget request and gets 403 “Domain not authorized” unless it comes from an allowed domain.
402Payment required — insufficient credits
403Forbidden — domain not allowed, chatbot inactive, or chatbot not yet configured with knowledge or an integration
404Not found — chatbot does not exist
405Method not allowed — wrong HTTP verb for this endpoint
429Too many requests — rate limit exceeded
503Service unavailable — API temporarily disabled

Key Concepts

bot_id

A unique identifier for your chatbot, in the format bot_ followed by 32 hex characters (e.g. bot_a1b2c3d4e5f6...). Find it on your chatbot's settings or embed page in the dashboard.

session_id

A client-generated 32-character hex string that identifies a visitor session. Messages with the same session_id are grouped into the same conversation.

const sessionId = crypto.randomUUID().replace(/-/g, "");
$sessionId = bin2hex(random_bytes(16));
import secrets
session_id = secrets.token_hex(16)
require "securerandom"
session_id = SecureRandom.hex(16)
import "crypto/rand"
import "encoding/hex"

b := make([]byte, 16)
rand.Read(b)
sessionId := hex.EncodeToString(b)
var sessionId = Guid.NewGuid().ToString("N");

conversation_id

A numeric ID assigned by the server after the first message in a session. Pass it back in subsequent requests to maintain conversation context. If omitted, the API will find or create a conversation based on the session_id.

GET /api/v1/chatbot/config/{bot_id}

Retrieve the chatbot's public configuration including display name, welcome message, colours, and branding settings.

Parameters

NameLocationTypeRequiredDescription
bot_id Path string Yes Chatbot identifier. Format: bot_[a-f0-9]{32}

Example Request

curl https://morv.ai/api/v1/chatbot/config/bot_YOUR_BOT_ID \
  -H "X-API-Key: pk_live_your_public_key" \
  -H "X-API-Secret: sk_live_your_secret_key"
const response = await fetch("https://morv.ai/api/v1/chatbot/config/bot_YOUR_BOT_ID", {
  headers: {
    "X-API-Key": "pk_live_your_public_key",
    "X-API-Secret": "sk_live_your_secret_key"
  }
});
const data = await response.json();
$ch = curl_init("https://morv.ai/api/v1/chatbot/config/bot_YOUR_BOT_ID");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: pk_live_your_public_key",
        "X-API-Secret: sk_live_your_secret_key",
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
import requests

response = requests.get(
    "https://morv.ai/api/v1/chatbot/config/bot_YOUR_BOT_ID",
    headers={
        "X-API-Key": "pk_live_your_public_key",
        "X-API-Secret": "sk_live_your_secret_key",
    },
)
data = response.json()
require "net/http"
require "json"

uri = URI("https://morv.ai/api/v1/chatbot/config/bot_YOUR_BOT_ID")
request = Net::HTTP::Get.new(uri)
request["X-API-Key"] = "pk_live_your_public_key"
request["X-API-Secret"] = "sk_live_your_secret_key"

response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
data = JSON.parse(response.body)
req, _ := http.NewRequest("GET", "https://morv.ai/api/v1/chatbot/config/bot_YOUR_BOT_ID", nil)
req.Header.Set("X-API-Key", "pk_live_your_public_key")
req.Header.Set("X-API-Secret", "sk_live_your_secret_key")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "pk_live_your_public_key");
client.DefaultRequestHeaders.Add("X-API-Secret", "sk_live_your_secret_key");

var response = await client.GetAsync(
    "https://morv.ai/api/v1/chatbot/config/bot_YOUR_BOT_ID");

var json = await response.Content.ReadAsStringAsync();
var data = JsonDocument.Parse(json);

Example Response

200 OK
{
  "success": true,
  "message": "Success",
  "data": {
    "bot_id": "bot_a1b2c3d4...",
    "name": "Support Bot",
    "status": "active",
    "display_name": "Support Bot",
    "welcome_message": "Hello! How can I help you today?",
    "placeholder_text": "Type a message...",
    "primary_color": "#6366f1",
    "position": "bottom-right",
    "show_branding": true
  }
}

Error Responses

400Missing or invalid bot_id format
403Domain not authorized, no allowed domains configured, the chatbot has no knowledge base content and no active integration, or the chatbot is currently unavailable because its owner's subscription is inactive or their credit balance cannot fund a message
404Chatbot not found
429Rate limit exceeded

POST /api/v1/chatbot/message

Send a message to the chatbot and receive an AI-generated response. Each message consumes credits based on the AI model and token usage. See Pricing for details.

Parameters

NameTypeRequiredDescription
bot_id string Yes Chatbot identifier. Format: bot_[a-f0-9]{32}
session_id string Yes Client-generated session identifier. Format: 32 lowercase hex characters ([a-f0-9]{32})
message string Yes The user's message. Default maximum 160 characters (configurable per chatbot, up to a 250-character platform ceiling). Over-length messages return 400.
conversation_id string No Numeric conversation ID from a previous response. If omitted, resolved from session_id.

Example Request

curl -X POST https://morv.ai/api/v1/chatbot/message \
  -H "X-API-Key: pk_live_your_public_key" \
  -H "X-API-Secret: sk_live_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_id": "bot_YOUR_BOT_ID",
    "session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "message": "What are your opening hours?"
  }'
const response = await fetch("https://morv.ai/api/v1/chatbot/message", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "pk_live_your_public_key",
    "X-API-Secret": "sk_live_your_secret_key"
  },
  body: JSON.stringify({
    bot_id: "bot_YOUR_BOT_ID",
    session_id: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    message: "What are your opening hours?"
  })
});
const data = await response.json();
$ch = curl_init("https://morv.ai/api/v1/chatbot/message");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        "Content-Type: application/json",
        "X-API-Key: pk_live_your_public_key",
        "X-API-Secret: sk_live_your_secret_key",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "bot_id"     => "bot_YOUR_BOT_ID",
        "session_id" => "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
        "message"    => "What are your opening hours?",
    ]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
import requests

response = requests.post(
    "https://morv.ai/api/v1/chatbot/message",
    headers={
        "X-API-Key": "pk_live_your_public_key",
        "X-API-Secret": "sk_live_your_secret_key",
    },
    json={
        "bot_id": "bot_YOUR_BOT_ID",
        "session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
        "message": "What are your opening hours?",
    },
)
data = response.json()
require "net/http"
require "json"

uri = URI("https://morv.ai/api/v1/chatbot/message")
http = Net::HTTP.new(uri.host, uri.port)

request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request["X-API-Key"] = "pk_live_your_public_key"
request["X-API-Secret"] = "sk_live_your_secret_key"
request.body = {
  bot_id: "bot_YOUR_BOT_ID",
  session_id: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  message: "What are your opening hours?"
}.to_json

response = http.request(request)
data = JSON.parse(response.body)
body, _ := json.Marshal(map[string]string{
    "bot_id":     "bot_YOUR_BOT_ID",
    "session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "message":    "What are your opening hours?",
})

req, _ := http.NewRequest("POST", "https://morv.ai/api/v1/chatbot/message", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", "pk_live_your_public_key")
req.Header.Set("X-API-Secret", "sk_live_your_secret_key")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "pk_live_your_public_key");
client.DefaultRequestHeaders.Add("X-API-Secret", "sk_live_your_secret_key");

var payload = new {
    bot_id = "bot_YOUR_BOT_ID",
    session_id = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    message = "What are your opening hours?"
};

var content = new StringContent(
    JsonSerializer.Serialize(payload),
    Encoding.UTF8, "application/json");

var response = await client.PostAsync(
    "https://morv.ai/api/v1/chatbot/message", content);

var json = await response.Content.ReadAsStringAsync();
var data = JsonDocument.Parse(json);

Example Response

200 OK
{
  "success": true,
  "message": "Success",
  "data": {
    "response": "Our opening hours are Monday to Friday, 9am to 5pm.",
    "bot_id": "bot_YOUR_BOT_ID",
    "conversation_id": "42"
  }
}

If the AI service fails while answering, the endpoint still returns 200, with an apology in response and "fallback": true in data. The message is not saved to the conversation and the failed AI call is not billed — treat a fallback as "try again", not as an answer.

Error Responses

400Missing or invalid bot_id, session_id, or message
402Insufficient credits or spending limit exceeded
403Chatbot inactive, domain not authorized, the chatbot has no knowledge base content and no active integration (not_configured), or the owner's subscription payment is overdue or the plan is no longer active (subscription_inactive)
429Rate limit exceeded. Most limits carry a machine code in errors.code
503The service is temporarily unavailable (service_unavailable)

POST /api/v1/chatbot/history

Retrieve the message history for a conversation. Returns the most recent 15 messages, in chronological order.

Parameters

NameTypeRequiredDescription
bot_id string Yes Chatbot identifier. Format: bot_[a-f0-9]{32}
session_id string Yes Session identifier. Format: 32 lowercase hex characters ([a-f0-9]{32})
conversation_id string No Numeric conversation ID. If omitted, looks up the latest conversation for this session.

Example Request

curl -X POST https://morv.ai/api/v1/chatbot/history \
  -H "X-API-Key: pk_live_your_public_key" \
  -H "X-API-Secret: sk_live_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_id": "bot_YOUR_BOT_ID",
    "session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
  }'
const response = await fetch("https://morv.ai/api/v1/chatbot/history", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "pk_live_your_public_key",
    "X-API-Secret": "sk_live_your_secret_key"
  },
  body: JSON.stringify({
    bot_id: "bot_YOUR_BOT_ID",
    session_id: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
  })
});
const data = await response.json();
$ch = curl_init("https://morv.ai/api/v1/chatbot/history");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        "Content-Type: application/json",
        "X-API-Key: pk_live_your_public_key",
        "X-API-Secret: sk_live_your_secret_key",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "bot_id"     => "bot_YOUR_BOT_ID",
        "session_id" => "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    ]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
import requests

response = requests.post(
    "https://morv.ai/api/v1/chatbot/history",
    headers={
        "X-API-Key": "pk_live_your_public_key",
        "X-API-Secret": "sk_live_your_secret_key",
    },
    json={
        "bot_id": "bot_YOUR_BOT_ID",
        "session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    },
)
data = response.json()
require "net/http"
require "json"

uri = URI("https://morv.ai/api/v1/chatbot/history")
http = Net::HTTP.new(uri.host, uri.port)

request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request["X-API-Key"] = "pk_live_your_public_key"
request["X-API-Secret"] = "sk_live_your_secret_key"
request.body = {
  bot_id: "bot_YOUR_BOT_ID",
  session_id: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
}.to_json

response = http.request(request)
data = JSON.parse(response.body)
body, _ := json.Marshal(map[string]string{
    "bot_id":     "bot_YOUR_BOT_ID",
    "session_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
})

req, _ := http.NewRequest("POST", "https://morv.ai/api/v1/chatbot/history", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", "pk_live_your_public_key")
req.Header.Set("X-API-Secret", "sk_live_your_secret_key")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "pk_live_your_public_key");
client.DefaultRequestHeaders.Add("X-API-Secret", "sk_live_your_secret_key");

var payload = new {
    bot_id = "bot_YOUR_BOT_ID",
    session_id = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
};

var content = new StringContent(
    JsonSerializer.Serialize(payload),
    Encoding.UTF8, "application/json");

var response = await client.PostAsync(
    "https://morv.ai/api/v1/chatbot/history", content);

var json = await response.Content.ReadAsStringAsync();
var data = JsonDocument.Parse(json);

Example Response

200 OK
{
  "success": true,
  "message": "Success",
  "data": {
    "conversation_id": "42",
    "messages": [
      {
        "role": "user",
        "content": "What are your opening hours?",
        "time": "2026-03-28 14:30:00"
      },
      {
        "role": "bot",
        "content": "Our opening hours are Monday to Friday, 9am to 5pm.",
        "time": "2026-03-28 14:30:02"
      }
    ]
  }
}

If no conversation is found for the given session, the response returns an empty messages array:

200 OK (no history)
{
  "success": true,
  "message": "Success",
  "data": {
    "conversation_id": null,
    "messages": []
  }
}

Error Responses

400Missing or invalid bot_id or session_id
403Domain not authorized or chatbot inactive
404Chatbot not found
429Rate limit exceeded

Widget Integration

If you prefer a drop-in solution over building a custom API integration, Morv offers an embeddable chat widget. Add a single script tag to your website and the widget handles the UI, session management, and API calls automatically.

HTML
<script src="https://morv.ai/public/js/chatbot.min.js"
  data-bot-id="bot_YOUR_BOT_ID"></script>

The widget uses Shadow DOM for CSS isolation and supports customisation (colours, position, messages) via your chatbot settings in the dashboard. Get your embed code from the Chatbot → Embed page in your project.