REST API
Three endpoints cover everything a widget or external script needs to do.
Base URL
https://app.mevichat.com
For self-hosted deployments, substitute your own origin. All examples in this page use the hosted URL.
POST /api/chat/
Stream an assistant reply for a widget conversation.
- Auth: public API key in the request body (
public_key) + the browser'sOriginheader, which must match the key's allowed domains. - Rate limit: 60 requests/minute per client IP.
- Response:
Content-Type: text/event-stream(Server-Sent Events). The connection stays open until adoneevent is emitted.
Request body
{
"public_key": "mvc_pk_live_xxx",
"session_id": "5f3e8c7c-5d4a-4a91-9a4e-c9a7e6d2b0f1",
"message": "What is your refund policy?",
"visitor_id": "optional-stable-id",
"visitor_metadata": { "plan": "enterprise" }
}
| Field | Type | Required | Notes |
|---|---|---|---|
public_key | string | yes | The mvc_pk_ key created in the dashboard. |
session_id | UUID string | yes | Generated client-side. Reuse across turns to preserve history. |
message | string | yes | The user's turn. Max 4000 characters. |
visitor_id | string | no | Your own stable identifier for the end user. |
visitor_metadata | object | no | Free-form JSON. Stored with the session for dashboard viewing. |
SSE event format
Each event has a name (event: line) and a JSON data: line. The server emits events in this order:
meta— session info. Always first.event: meta data: {"session_id":"5f3e8c7c-...","message_id":128472}token— streaming content deltas. Zero or more, in order.event: token data: {"text":"Our refund "} event: token data: {"text":"policy is 30 days."}blocked— optional. Emitted when a moderation pattern matches. Replacestokenevents for that turn. Always followed bydone.event: blocked data: {"reason":"blocked_pattern","fallback":"I can't help with that."}error— optional. LLM provider auth failures, quota exhaustion, or internal errors. Always followed bydone.event: error data: {"code":"quota_exceeded","plan":"free","used":100,"cap":100,"message":"..."}done— stream terminator with final metering. Always last.event: done data: {"finish_reason":"stop","tokens_in":184,"tokens_out":47,"latency_ms":1823,"model":"anthropic:claude-haiku-4-5"}
The done event carries metering even when finish_reason is error, blocked, or quota_exceeded. Use finish_reason === "stop" to detect a clean completion.
Errors
Validation errors return JSON before the stream opens:
| Status | error code | Cause |
|---|---|---|
| 400 | invalid_json | Body is not valid JSON. |
| 400 | invalid_message | Missing public_key, session_id, or message. |
| 400 | message_too_long | message exceeds 4000 characters. |
| 400 | invalid_session_id | session_id is not a UUID. |
| 401 | invalid_api_key | Key revoked or not found. |
| 403 | origin_not_allowed | Request Origin isn't in the key's whitelist. |
| 405 | method_not_allowed | Only POST is accepted. |
| 429 | rate_limited | 60 req/min IP cap hit. |
Once the SSE connection is open, errors arrive as error events — see above.
Example — curl
curl -N -X POST https://app.mevichat.com/api/chat/ \
-H "Content-Type: application/json" \
-H "Origin: https://example.com" \
-d '{
"public_key": "mvc_pk_live_xxx",
"session_id": "5f3e8c7c-5d4a-4a91-9a4e-c9a7e6d2b0f1",
"message": "What is your refund policy?"
}'
The -N flag disables output buffering so SSE events print as they arrive.
Example — Node fetch
const res = await fetch("https://app.mevichat.com/api/chat/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Origin": "https://example.com",
},
body: JSON.stringify({
public_key: "mvc_pk_live_xxx",
session_id: crypto.randomUUID(),
message: "What is your refund policy?",
}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const parts = buf.split("\n\n");
buf = parts.pop() ?? "";
for (const part of parts) {
const lines = part.split("\n");
const event = lines.find(l => l.startsWith("event: "))?.slice(7);
const data = lines.find(l => l.startsWith("data: "))?.slice(6);
if (event && data) console.log(event, JSON.parse(data));
}
}
Example — Python requests
import json
import requests
with requests.post(
"https://app.mevichat.com/api/chat/",
headers={
"Content-Type": "application/json",
"Origin": "https://example.com",
},
json={
"public_key": "mvc_pk_live_xxx",
"session_id": "5f3e8c7c-5d4a-4a91-9a4e-c9a7e6d2b0f1",
"message": "What is your refund policy?",
},
stream=True,
) as r:
r.raise_for_status()
for raw in r.iter_lines(decode_unicode=True):
if raw.startswith("event: "):
event = raw[7:]
elif raw.startswith("data: "):
print(event, json.loads(raw[6:]))
GET /api/widget/config/
Returns the bot's display configuration (name, welcome message, theme, quick replies). Called once at widget boot.
- Auth:
?public_key=mvc_pk_live_xxxquery parameter + matchingOriginheader. - Cache:
public, max-age=60when called withpublic_key. Use the same key to amortize boot latency across visitors.
Response
{
"bot": {
"name": "Support",
"welcome_message": "Hi! How can I help?",
"logo_url": "https://cdn.example.com/logo.png",
"primary_color": "#4F46E5",
"position": "right",
"theme_mode": "light",
"font_family": "Inter",
"border_radius": 12,
"quick_replies": ["Talk to a human", "Return an item"],
"custom_css_vars": { "--mvc-bg": "#ffffff" }
}
}
Errors use the same error codes as /api/chat/ (invalid_api_key, origin_not_allowed, etc.).
GET /api/conversations/:session_id/export.json
Download a full transcript of a single conversation as JSON. Useful for compliance archives and CRM integrations.
- Auth: JWT (
Authorization: Bearer ...). The session's bot must belong to the caller's workspace. - Rate limit: 30 requests/minute per user (per IP for unauthenticated callers, which are rejected anyway).
- Response:
Content-Type: application/json,Content-Disposition: attachment; filename="session-<uuid>.json".
Response body
{
"session": {
"id": "5f3e8c7c-5d4a-4a91-9a4e-c9a7e6d2b0f1",
"bot_slug": "support",
"source": "widget",
"visitor_id": "user-1842",
"visitor_metadata": { "plan": "enterprise" },
"referrer": "https://example.com/pricing",
"user_agent": "Mozilla/5.0 ...",
"started_at": "2026-04-01T14:22:08.123456+00:00",
"last_activity_at": "2026-04-01T14:27:51.992311+00:00"
},
"messages": [
{
"id": "128472",
"role": "user",
"content": "What is your refund policy?",
"rating": "",
"model": "",
"tokens_in": 0,
"tokens_out": 0,
"latency_ms": 0,
"status": "done",
"error_message": "",
"rag_chunks_used": [],
"tools_called": [],
"created_at": "2026-04-01T14:22:08.456789+00:00"
},
{
"id": "128473",
"role": "assistant",
"content": "Our refund policy is 30 days from purchase.",
"rating": "up",
"model": "anthropic:claude-haiku-4-5",
"tokens_in": 184,
"tokens_out": 47,
"latency_ms": 1823,
"status": "done",
"error_message": "",
"rag_chunks_used": [41, 42],
"tools_called": [],
"created_at": "2026-04-01T14:22:10.511223+00:00"
}
]
}
Errors
| Status | Body | Cause |
|---|---|---|
| 401 | {"detail":"authentication_required"} | No JWT or invalid JWT. |
| 401 | {"detail":"no_workspace"} | Authenticated user has no workspace membership. |
| 404 | — | Session doesn't exist, belongs to a different workspace, or has been soft-deleted. |
| 429 | {"detail":"..."} | Rate-limited. |
Example — curl
curl -H "Authorization: Bearer $JWT" \
-o session.json \
"https://app.mevichat.com/api/conversations/5f3e8c7c-5d4a-4a91-9a4e-c9a7e6d2b0f1/export.json"
What's not here
A few things intentionally aren't part of the public API:
- Bot CRUD, API key management, knowledge sources, workspace settings — managed in the Mevichat dashboard, not exposed as a public endpoint.
- Widget preview iframe tokens — issued and consumed entirely by the first-party customer panel. Not part of the public API surface.
- Stripe Checkout / Customer Portal — opened from the dashboard's billing settings.