S’authentifier
Un seul mécanisme pour l’API, les webhooks sortants et le serveur MCP : un jeton porteur. Le jeton se crée dans vos réglages, onglet API, et n’est affiché qu’une fois — nous n’en gardons qu’une empreinte SHA-256, et personne chez nous ne peut le relire.
Un jeton commence toujours par « cvk_ », suivi de quarante caractères tirés au sort. Le préfixe n’est pas décoratif : GitHub et consorts balaient les dépôts publics à la recherche de motifs connus, et un secret publié par mégarde avec un préfixe reconnaissable se fait repérer et signaler.
Chaque appel porte l’en-tête :
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
}
}
Les portées
Un jeton n’ouvre que ce que vous cochez. La portée est déclarée sur la route elle-même, pas enfouie dans un contrôleur : on ouvre le fichier de routes et on voit d’un coup d’œil ce qu’un jeton ouvre.
conversations:read
Lister les conversations, lire un fil complet avec ses messages.
contacts:read
Lister les contacts et leurs identités de canal.
knowledge:read
Interroger la base de connaissances du marchand.
`/v1/me` n’exige aucune portée : « ce jeton marche-t-il ? » doit avoir une réponse, même pour un jeton qui n’ouvre rien.
Les points d’entrée
Cinq, tous en lecture, tous préfixés par la version. Le numéro de version coûte quatre caractères aujourd’hui et évite une impasse plus tard : une API publiée sans lui ne peut plus jamais changer de forme sans casser quelqu’un.
GET /api/v1/me
—
GET /api/v1/conversations
conversations:read
GET /api/v1/conversations/{id}
conversations:read
GET /api/v1/contacts
contacts:read
GET · POST /api/v1/knowledge/search
knowledge:read
La recherche accepte GET et POST : une question de trois cents caractères dans une URL se fait tronquer par les intermédiaires.
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
}
]
}
Filtres et pagination
La pagination se fait au curseur, jamais par numéro de page : une page 3 change de contenu dès qu’une conversation arrive, et l’on saute alors une ligne sans le voir.
limit
limit
limit
status
cursor
q
`next_cursor` vaut `null` dès que la page n’est pas pleine : une page à moitié remplie est la dernière, et faire faire un appel de plus pour l’apprendre coûte une requête à chaque parcours.
Les erreurs
Toujours la même forme : un code stable qu’un programme teste, un message que lit un humain. Un intégrateur qui doit faire correspondre des phrases françaises pour distinguer deux erreurs écrit du code qui casse à la première reformulation.
{ "error": { "code": "insufficient_scope", "message": "…" } }
401
missing_token
401
invalid_token
403
insufficient_scope
404
not_found
422
missing_query
429
—
Limitation de débit
Cent vingt appels par minute et par jeton. Vingt par minute et par adresse IP pour les requêtes sans jeton — elles se font refuser de toute façon, et sans limite on pourrait essayer des jetons au hasard aussi vite que le réseau le permet.
La limite porte sur le JETON, pas sur l’adresse : deux marchands derrière le même réseau d’entreprise ne doivent pas se gêner, et un intégrateur qui change d’adresse ne doit pas contourner sa limite. La clé du compteur est une empreinte du jeton, jamais le jeton lui-même — un secret dans une clé de cache est un secret dans un magasin que personne ne surveille.
Webhooks sortants
Un point de livraison se déclare dans vos réglages. Chaque envoi est signé, horodaté, et rejoué jusqu’à huit fois en cas d’échec.
Les événements
Vous choisissez lesquels vous intéressent, point de livraison par point de livraison.
conversation.opened
escalation.opened
escalation.resolved
order.placed
order.confirmed
appointment.booked
appointment.cancelled
contact.created
form.completed
La charge utile
{
"id": "evt_01K5...",
"type": "escalation.opened",
"created_at": "2026-08-23T21:14:07+00:00",
"organization_id": "01K5...",
"data": { }
}
`id` est là pour que vous puissiez écarter un doublon : une reprise de file peut rejouer une livraison, et un receveur bien écrit garde les identifiants vus et ne traite chacun qu’une fois.
Les en-têtes
Convosia-Signature
t={horodatage},v1={HMAC-SHA256}
Convosia-Event
Le type de l’événement, pour router sans décoder la charge.
Convosia-Delivery
L’identifiant de CETTE tentative de livraison.
User-Agent
Convosia-Webhooks/1.0
Vérifier la signature
La signature porte sur « horodatage.corps », et non sur le corps seul : sans l’horodatage dans ce qui est signé, une charge interceptée resterait valable indéfiniment. Comparez en temps constant, et refusez au-delà de cinq 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;
}
Reprises et extinction
Huit tentatives étalées en arrière exponentielle. Une adresse qui échoue vingt fois de suite est désactivée automatiquement — ce n’est pas une punition : un point de livraison mort qu’on continue d’appeler finit par faire prendre notre domaine pour un importun. L’adresse est revérifiée AVANT CHAQUE ENVOI, et pas seulement à l’enregistrement : un nom de domaine peut se mettre à pointer vers un réseau privé entre deux livraisons.
Le serveur MCP
Un seul point d’entrée en POST, qui parle JSON-RPC 2.0. Il permet à un modèle — Claude, ou tout client qui parle le protocole — d’interroger le compte du marchand directement.
POST https://convosia.app/mcp
· JSON-RPC 2.0 · 2025-06-18
Hors du préfixe `/v1/`, et ce n’est pas un oubli : sa version est celle du PROTOCOLE, négociée à la poignée de main. Lui coller un numéro de version à nous ferait deux versions à accorder pour une seule surface.
Les méthodes
initialize
La poignée de main. Rend la version du protocole, les capacités et le nom du serveur.
ping
Rend un résultat vide. Sert à savoir si la connexion tient.
tools/list
Le catalogue des outils, FILTRÉ par les portées du jeton.
tools/call
Exécute un outil. Attend `name` et `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" }
}
}
Les outils
Un modèle ne voit dans `tools/list` que ce qu’il peut réellement appeler. Un outil visible mais refusé à l’appel apprend au modèle à réessayer, et il réessaie.
lister_conversations
conversations:read
Les conversations récentes. Arguments : `statut` (open, pending, closed), `limite` (1 à 100).
lire_conversation
conversations:read
Le fil complet d’une conversation. Argument : `id`.
lister_contacts
contacts:read
Les contacts et leurs identités de canal. Argument : `limite` (1 à 200).
chercher_connaissances
knowledge:read
Cherche dans les documents du marchand. Arguments : `question`, `limite` (1 à 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 }
}
}'
Brancher un client
La plupart des clients MCP acceptent un serveur distant en HTTP avec un en-tête. Voici la forme attendue :
{
"mcpServers": {
"convosia": {
"url": "https://convosia.app/mcp",
"headers": {
"Authorization": "Bearer cvk_…"
}
}
}
}
Les erreurs JSON-RPC
-32600
Requête JSON-RPC invalide.
-32601
Méthode inconnue.
-32602
Paramètre attendu manquant — `name` sur `tools/call`.
Aucun outil n’écrit dans le compte du marchand, et ce n’est pas une étape qu’on franchira distraitement plus tard : laisser un modèle envoyer des messages à de vrais clients demande des garde-fous que ce serveur n’a pas — confirmation humaine, limitation par contact, journal séparé. Le jour où ce sera fait, ce sera un chantier à soi.
Ce qui vous protège des autres marchands
Le jeton désigne une organisation, et le tenant est posé AVANT que le contrôleur ne s’exécute. Après ce geste, toute requête à la base est bornée : même un contrôleur écrit distraitement ne peut pas lire les conversations d’une autre organisation, parce qu’il n’y a plus d’« autre organisation » atteignable. L’isolation est structurelle, pas vigilante — et douze suites de tests le vérifient.