Skip to content
Mevichat

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's Origin header, 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 a done event 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" }
}
FieldTypeRequiredNotes
public_keystringyesThe mvc_pk_ key created in the dashboard.
session_idUUID stringyesGenerated client-side. Reuse across turns to preserve history.
messagestringyesThe user's turn. Max 4000 characters.
visitor_idstringnoYour own stable identifier for the end user.
visitor_metadataobjectnoFree-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:

  1. meta — session info. Always first.
    event: meta
    data: {"session_id":"5f3e8c7c-...","message_id":128472}
  2. token — streaming content deltas. Zero or more, in order.
    event: token
    data: {"text":"Our refund "}
    event: token
    data: {"text":"policy is 30 days."}
  3. blocked — optional. Emitted when a moderation pattern matches. Replaces token events for that turn. Always followed by done.
    event: blocked
    data: {"reason":"blocked_pattern","fallback":"I can't help with that."}
  4. error — optional. LLM provider auth failures, quota exhaustion, or internal errors. Always followed by done.
    event: error
    data: {"code":"quota_exceeded","plan":"free","used":100,"cap":100,"message":"..."}
  5. 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:

Statuserror codeCause
400invalid_jsonBody is not valid JSON.
400invalid_messageMissing public_key, session_id, or message.
400message_too_longmessage exceeds 4000 characters.
400invalid_session_idsession_id is not a UUID.
401invalid_api_keyKey revoked or not found.
403origin_not_allowedRequest Origin isn't in the key's whitelist.
405method_not_allowedOnly POST is accepted.
429rate_limited60 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_xxx query parameter + matching Origin header.
  • Cache: public, max-age=60 when called with public_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

StatusBodyCause
401{"detail":"authentication_required"}No JWT or invalid JWT.
401{"detail":"no_workspace"}Authenticated user has no workspace membership.
404Session 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.