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.
https://morv.ai/api/v1
All requests and responses use JSON. Include Content-Type: application/json for POST requests.
By using this API you agree to our Terms of Service and Privacy Policy.
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:
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.
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 Methods | GET, POST, OPTIONS |
| Allowed Headers | Content-Type, X-API-Key, X-API-Secret, X-Chatbot-Session-Token |
| Max Age | 86,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 limit | 20 requests per 60 seconds |
| Scope | Per bot_id + IP address |
| Exceeded response | 429 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": true,
"message": "Success",
"data": { ... }
}
{
"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
| Code | Meaning |
|---|---|
200 | Success |
400 | Bad request — missing or invalid parameters |
401 | Unauthorized — 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. |
402 | Payment required — insufficient credits |
403 | Forbidden — domain not allowed, chatbot inactive, or chatbot not yet configured with knowledge or an integration |
404 | Not found — chatbot does not exist |
405 | Method not allowed — wrong HTTP verb for this endpoint |
429 | Too many requests — rate limit exceeded |
503 | Service 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
| Name | Location | Type | Required | Description |
|---|---|---|---|---|
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
{
"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
400 | Missing or invalid bot_id format |
403 | Domain 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 |
404 | Chatbot not found |
429 | Rate 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
| Name | Type | Required | Description |
|---|---|---|---|
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
{
"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
400 | Missing or invalid bot_id, session_id, or message |
402 | Insufficient credits or spending limit exceeded |
403 | Chatbot 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) |
429 | Rate limit exceeded. Most limits carry a machine code in errors.code |
503 | The 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
| Name | Type | Required | Description |
|---|---|---|---|
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
{
"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:
{
"success": true,
"message": "Success",
"data": {
"conversation_id": null,
"messages": []
}
}
Error Responses
400 | Missing or invalid bot_id or session_id |
403 | Domain not authorized or chatbot inactive |
404 | Chatbot not found |
429 | Rate 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.
<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.