All documentation
/// REFERENCE

The API and the MCP server.

Everything is read-only. Five REST endpoints, nine webhook events, four MCP tools — and exactly how to authenticate against all three.

Base address https://convosia.app

Authenticating

One mechanism for the API, outgoing webhooks and the MCP server: a bearer token. You create it in your settings, API tab, and it is shown once — we keep only a SHA-256 fingerprint, and nobody here can read it back.

A token always starts with “cvk_”, followed by forty random characters. The prefix is not decorative: GitHub and others sweep public repositories for known patterns, and a secret published by mistake with a recognisable prefix gets spotted and reported.

Every call carries the header:

Authorization: Bearer cvk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

curl :

curl -s https://convosia.app/api/v1/me \
  -H "Authorization: Bearer $CONVOSIA_TOKEN"
{
  "organization": { "id": "01K5...", "name": "Boutique Awa" },
  "token": {
    "name": "Intégration Zapier",
    "prefix": "cvk_a4f2c1",
    "scopes": ["conversations:read", "knowledge:read"],
    "expires_at": null
  }
}

Scopes

A token opens only what you tick. The scope is declared on the route itself, not buried in a controller: you open the routes file and see at a glance what a token opens.

conversations:read List conversations, read a full thread with its messages.
contacts:read List contacts and their channel identities.
knowledge:read Query the merchant’s knowledge base.

`/v1/me` requires no scope: “does this token work?” must have an answer, even for a token that opens nothing.

Endpoints

Five, all read-only, all version-prefixed. The version number costs four characters today and avoids a dead end later: an API published without one can never change shape again without breaking someone.

Call
Scope
What it returns
GET /api/v1/me —
The token’s organisation, its name, its scopes and its expiry.
GET /api/v1/conversations conversations:read
Conversations, newest first.
GET /api/v1/conversations/{id} conversations:read
The full thread: every message, in order.
GET /api/v1/contacts contacts:read
Contacts with their identities — WhatsApp number, address, widget visitor.
GET · POST /api/v1/knowledge/search knowledge:read
The document excerpts that best answer a question.

Search accepts GET and POST: a three-hundred-character question in a URL gets truncated by intermediaries.

curl -s "https://convosia.app/api/v1/conversations?status=open&limit=10" \
  -H "Authorization: Bearer $CONVOSIA_TOKEN"
{
  "data": [
    {
      "id": "01K5...",
      "contact_id": "01K5...",
      "status": "open",
      "awaits_human": true,
      "last_inbound_at": "2026-08-23T21:14:07+00:00",
      "last_outbound_at": "2026-08-23T21:14:31+00:00"
    }
  ],
  "next_cursor": null
}
curl -s https://convosia.app/api/v1/knowledge/search \
  -H "Authorization: Bearer $CONVOSIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"q":"quels sont vos délais de livraison ?","limit":3}'
{
  "data": [
    {
      "source_id": "01K5...",
      "source_title": "Conditions de vente.pdf",
      "ordinal": 4,
      "content": "Livraison à Abidjan sous 24 h…",
      "score": 0.8123,
      "corroborated": true
    }
  ]
}

Filters and pagination

Pagination is by cursor, never by page number: page 3 changes content the moment a conversation arrives, and you silently skip a row.

Parameter
Where
Values
limit
conversations
1 to 100. Defaults to 25.
limit
contacts
1 to 200. Defaults to 50.
limit
knowledge/search
1 to 20. Defaults to 5.
status
conversations
open, pending, closed. Omit for all.
cursor
conversations, contacts
The `next_cursor` from the previous response.
q
knowledge/search
The question, in plain language. Required.

`next_cursor` is `null` as soon as the page is not full: a half-filled page is the last one, and making you call again to learn that costs one request on every pass.

Errors

Always the same shape: a stable code a program tests, a message a human reads. An integrator who has to match French sentences to tell two errors apart writes code that breaks on the first rewording.

{ "error": { "code": "insufficient_scope", "message": "…" } }
Status
Code
When
401 missing_token
No `Authorization` header.
401 invalid_token
Unknown, expired or revoked token. All three give the SAME message: telling “expired” from “unknown” would tell whoever is trying tokens at random which one was close.
403 insufficient_scope
The token is valid but lacks the scope. The message names the missing scope: you are already authenticated, you deserve an error that says what to do.
404 not_found
The conversation does not exist — or belongs to someone else, which gives the same answer.
422 missing_query
Search without the `q` parameter.
429 —
Rate limit reached.

Rate limiting

One hundred and twenty calls per minute per token. Twenty per minute per IP address for requests with no token — they are refused anyway, and without a limit someone could try tokens at random as fast as the network allows.

The limit is on the TOKEN, not the address: two merchants behind the same corporate network must not get in each other’s way, and an integrator who changes address must not bypass their limit. The counter key is a fingerprint of the token, never the token itself — a secret in a cache key is a secret in a store nobody watches.

Outgoing webhooks

You declare an endpoint in your settings. Every send is signed, timestamped, and retried up to eight times on failure.

Events

You pick which ones interest you, endpoint by endpoint.

conversation.opened escalation.opened escalation.resolved order.placed order.confirmed appointment.booked appointment.cancelled contact.created form.completed

The payload

{
  "id": "evt_01K5...",
  "type": "escalation.opened",
  "created_at": "2026-08-23T21:14:07+00:00",
  "organization_id": "01K5...",
  "data": { }
}

`id` is there so you can discard a duplicate: a queue retry can replay a delivery, and a well-written receiver keeps the ids it has seen and handles each one once.

Headers

Convosia-Signature t={timestamp},v1={HMAC-SHA256}
Convosia-Event The event type, so you can route without decoding the body.
Convosia-Delivery The id of THIS delivery attempt.
User-Agent Convosia-Webhooks/1.0

Verifying the signature

The signature covers “timestamp.body”, not the body alone: without the timestamp inside what is signed, an intercepted payload would stay valid forever. Compare in constant time, and refuse anything older than five minutes.

// Node.js
const crypto = require('crypto');

function verifie(corpsBrut, entete, secret) {
  const parts = Object.fromEntries(
    entete.split(',').map((p) => p.split('='))
  );
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (!Number.isFinite(age) || age > 300) return false;

  const attendu = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${corpsBrut}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(attendu),
    Buffer.from(parts.v1 ?? '')
  );
}
# PHP
$parts = [];
parse_str(str_replace(',', '&', $_SERVER['HTTP_CONVOSIA_SIGNATURE']), $parts);

if (abs(time() - (int) $parts['t']) > 300) {
    http_response_code(400);
    exit;
}

$attendu = hash_hmac('sha256', $parts['t'] . '.' . $corpsBrut, $secret);

if (! hash_equals($attendu, $parts['v1'] ?? '')) {
    http_response_code(400);
    exit;
}

Retries and shutdown

Eight attempts spread with exponential backoff. An address that fails twenty times in a row is disabled automatically — not as punishment: a dead endpoint we keep calling ends up getting our domain treated as a nuisance. The address is re-checked BEFORE EVERY SEND, not only when saved: a hostname can start pointing at a private network between two deliveries.

The MCP server

A single POST endpoint speaking JSON-RPC 2.0. It lets a model — Claude, or any client that speaks the protocol — query the merchant’s account directly.

POST https://convosia.app/mcp · JSON-RPC 2.0 · 2025-06-18

Outside the `/v1/` prefix, and that is not an oversight: its version is the PROTOCOL’s, negotiated at the handshake. Giving it a version of ours would mean two versions to reconcile for one surface.

Methods

initialize The handshake. Returns the protocol version, the capabilities and the server name.
ping Returns an empty result. Tells you the connection holds.
tools/list The tool catalogue, FILTERED by the token’s scopes.
tools/call Runs a tool. Expects `name` and `arguments`.
curl -s https://convosia.app/mcp \
  -H "Authorization: Bearer $CONVOSIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": { "name": "convosia", "version": "1.0.0" }
  }
}

Tools

A model sees in `tools/list` only what it can actually call. A tool that is visible but refused on call teaches the model to retry, and it retries.

lister_conversations conversations:read

Recent conversations. Arguments: `statut` (open, pending, closed), `limite` (1 to 100).

lire_conversation conversations:read

The full thread of one conversation. Argument: `id`.

lister_contacts contacts:read

Contacts and their channel identities. Argument: `limite` (1 to 200).

chercher_connaissances knowledge:read

Searches the merchant’s documents. Arguments: `question`, `limite` (1 to 20).

curl -s https://convosia.app/mcp \
  -H "Authorization: Bearer $CONVOSIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "chercher_connaissances",
      "arguments": { "question": "délais de livraison", "limite": 3 }
    }
  }'

Connecting a client

Most MCP clients accept a remote HTTP server with a header. Here is the expected shape:

{
  "mcpServers": {
    "convosia": {
      "url": "https://convosia.app/mcp",
      "headers": {
        "Authorization": "Bearer cvk_…"
      }
    }
  }
}

JSON-RPC errors

-32600 Invalid JSON-RPC request.
-32601 Unknown method.
-32602 Missing expected parameter — `name` on `tools/call`.
Everything is read-only

No tool writes into the merchant’s account, and that is not a step to be taken absent-mindedly later: letting a model send messages to real customers needs guardrails this server does not have — human confirmation, per-contact rate limiting, a separate log. The day it is done, it will be a project of its own.

What protects you from other merchants

The token designates an organisation, and the tenant is set BEFORE the controller runs. After that, every database query is bounded: even a carelessly written controller cannot read another organisation’s conversations, because there is no reachable “other organisation” any more. The isolation is structural, not vigilant — and twelve test suites check it.

A question about the API? Write to us