API et webhooks
Dernière mise à jour : 15 octobre 2026
Spécification OpenAPI : openapi.json
Une question : bonjour@marius.qd.je
Démarrer
Marius qualifie les visiteurs de votre site et en fait des prospects. L’API et les webhooks les envoient dans vos outils : votre CRM (HubSpot, Pipedrive, Salesforce, Axonaut…), un tableur, Zapier, Make, n8n ou votre propre logiciel.
- Les webhooks envoient chaque nouveau prospect à une adresse de votre choix, dès qu’il arrive. C’est le plus simple pour alimenter un CRM.
- L’API permet à votre outil de tout relire quand il le veut : prospects, conversations, sites, chiffres du mois. Elle permet aussi de marquer un prospect comme traité et d’ajouter des notes, qui apparaissent dans votre espace Marius.
L’API est ouverte aux clients dont le site est en ligne, toutes formules confondues. Le propriétaire de l’espace crée les clés et les webhooks dans son espace : Mon compte → API et webhooks.
Toutes les adresses commencent par https://marius.qd.je/api/v1. Elles répondent en JSON (UTF-8). Les dates suivent la norme ISO 8601, en UTC.
curl https://marius.qd.je/api/v1/me \
-H "Authorization: Bearer mr_live_votre_cle"Clés et authentification
Chaque appel porte une clé dans l’en-tête Authorization: Bearer mr_live_…. Une clé commence par mr_live_ et s’affiche une seule fois, à sa création : Marius n’en garde qu’une empreinte, personne ne peut la retrouver. Si elle est perdue, supprimez-la et créez-en une autre.
- Portée. Une clé ouvre toutes les données de votre espace (tous vos sites), jamais celles d’un autre client.
- Lecture seule ou modifications. Par défaut, une clé ne fait que lire. Cochez « Autoriser les modifications » à sa création pour qu’elle puisse marquer un prospect comme traité et ajouter des notes.
- Expiration. Chaque clé a une date d’expiration, choisie à sa création (deux ans au plus). Le propriétaire reçoit un e-mail sept jours avant.
- Suppression. Une clé supprimée cesse de marcher tout de suite. Elle appartient à votre entreprise, pas à une personne : elle reste valable si le propriétaire de l’espace change.
- Côté serveur seulement. Une clé se garde comme un mot de passe. Ne la mettez jamais dans le code d’une page web ou d’une application mobile : l’API refuse d’ailleurs les appels faits depuis un navigateur.
Listes et synchronisation
Les listes (/leads, /conversations) renvoient une page à la fois :
{
"object": "list",
"data": [ … ],
"has_more": true,
"next_cursor": "MjAyNi0xMC0xNVQwOToxMzowMS40ODIrMDA6MDB8NmQxYzRiMGUt…"
}| Paramètre | Rôle |
|---|---|
limit | Éléments par page, de 1 à 100 (25 par défaut). |
cursor | Page suivante : la valeur next_cursor de la page précédente, telle quelle. |
order | desc (par défaut) : les plus récents d’abord. asc : les plus anciens d’abord, pour une synchronisation. |
updated_since | Seulement ce qui a changé après cette date (ISO 8601 avec fuseau, par exemple 2026-10-15T09:00:00Z). |
Pour tenir un CRM à jour sans webhook, gardez le plus grand updated_at reçu et repartez de là au passage suivant :
# 1re fois : tout l'historique, du plus ancien au plus récent
curl "https://marius.qd.je/api/v1/leads?order=asc&limit=100" -H "Authorization: Bearer $MARIUS_KEY"
# page suivante : reprendre next_cursor tant que has_more vaut true
curl "https://marius.qd.je/api/v1/leads?order=asc&limit=100&cursor=MjAyNi0x…" -H "Authorization: Bearer $MARIUS_KEY"
# ensuite, toutes les 15 minutes : seulement ce qui a changé depuis
# le plus grand updated_at déjà reçu
curl "https://marius.qd.je/api/v1/leads?order=asc&updated_since=2026-10-15T09:13:01.482Z" -H "Authorization: Bearer $MARIUS_KEY"Un paramètre inconnu est refusé (erreur 400) : une faute de frappe ne passe jamais inaperçue.
Prospects
Un prospect est un visiteur qui a laissé son e-mail ou son numéro. Les essais faits avec « Tester Marius » n’apparaissent jamais.
GET/leads | Liste. Filtres : site (identifiant d’un site), status (new, done), score (hot, warm, cold), plus ceux de la pagination. Triée par updated_at. |
GET/leads/{id} | Un prospect, avec les notes de votre équipe (notes). |
PATCH/leads/{id} | Change le statut : {"status": "done"} (traité) ou {"status": "new"} (à traiter). Clé avec modifications. |
GET/leads/{id}/notes | Notes de l’équipe sur ce prospect. |
POST/leads/{id}/notes | Ajoute une note ({"text": "…"}, 2 000 caractères au plus), signée « API · nom de la clé ». Clé avec modifications. |
{
"id": "6d1c4b0e-2f7a-4c55-9a1e-3b8d2c7f0a19",
"object": "lead",
"site": { "id": "290583f4-e9d2-4528-b615-83b8e23fc7e1", "name": "Atelier Martin", "url": "https://www.atelier-martin.fr" },
"conversation_id": "a3b9e1c2-5d4f-4e8a-b7c6-1f2e3d4c5b6a",
"status": "new",
"done_at": null,
"done_by": null,
"contact": { "name": "Claire Dubois", "email": "claire.dubois@exemple.fr", "phone": "06 12 34 56 78" },
"answers": [
{ "key": "projet", "label": "Quel est votre projet ?", "value": "Rénover la cuisine", "answered_at": "2026-10-15T09:12:44Z" },
{ "key": "delai", "label": "Pour quand ?", "value": "Avant Noël", "answered_at": "2026-10-15T09:13:01Z" }
],
"score": {
"level": "hot",
"points": 80,
"criteria": [ { "id": "budget", "label": "Budget indiqué", "points": 25 } ]
},
"estimated_value_eur": 8500,
"summary": {
"text": "Claire veut rénover sa cuisine avant Noël et a un budget défini.",
"need": "Rénovation complète de cuisine",
"next_step": "La rappeler cette semaine pour fixer une visite.",
"score_reason": "Coordonnées laissées, budget et délai donnés."
},
"qualified_at": "2026-10-15T09:45:02.118+00:00",
"created_at": "2026-10-15T09:11:20.405+00:00",
"updated_at": "2026-10-15T09:45:02.118+00:00",
"last_message_at": "2026-10-15T09:14:37.902+00:00",
"espace_url": "https://marius.qd.je/espace/prospects?p=6d1c4b0e-2f7a-4c55-9a1e-3b8d2c7f0a19"
}statusnew(à traiter) oudone(traité, avecdone_atetdone_by).contact- Nom, e-mail et téléphone laissés par le visiteur ;
nullpour ce qu’il n’a pas donné. answers- Réponses aux questions de votre questionnaire, dans son ordre.
score- Niveau (
hot,warm,cold), points sur 100 et critères remplis.nulltant que la fiche n’est pas faite. estimated_value_eur- Valeur estimée, si vous avez renseigné vos tarifs ; sinon
null. summary- Résumé, besoin, prochaine étape conseillée et raison du score. Fait une fois la conversation retombée (30 minutes sans message), refait si elle reprend.
espace_url- La fiche dans votre espace Marius, à mettre en lien dans votre CRM.
curl -X PATCH https://marius.qd.je/api/v1/leads/6d1c4b0e-2f7a-4c55-9a1e-3b8d2c7f0a19 \
-H "Authorization: Bearer $MARIUS_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "done"}'curl -X POST https://marius.qd.je/api/v1/leads/6d1c4b0e-2f7a-4c55-9a1e-3b8d2c7f0a19/notes \
-H "Authorization: Bearer $MARIUS_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Devis envoyé le 16/10."}'
# réponse (201)
{ "id": "f0c1…", "object": "note", "author": "API · HubSpot", "text": "Devis envoyé le 16/10.",
"created_at": "2026-10-16T08:02:11+00:00", "edited_at": null }Conversations
GET/conversations | Liste, triée par date du dernier message. Filtres : site, has_lead (true : seulement celles qui ont donné un prospect), updated_since (celles qui ont un message après cette date), plus la pagination. |
GET/conversations/{id} | Une conversation avec ses messages (500 au plus), dans l’ordre. |
Dans messages, role vaut visitor ou assistant ; unanswered signale une réponse de Marius qui n’avait pas l’information.
{
"id": "a3b9e1c2-5d4f-4e8a-b7c6-1f2e3d4c5b6a",
"object": "conversation",
"site": { "id": "290583f4-…", "name": "Atelier Martin", "url": "https://www.atelier-martin.fr" },
"started_at": "2026-10-15T09:11:02+00:00",
"last_message_at": "2026-10-15T09:14:37+00:00",
"messages_count": 8,
"first_message": "Bonjour, vous faites les cuisines sur mesure ?",
"lead_id": "6d1c4b0e-2f7a-4c55-9a1e-3b8d2c7f0a19",
"messages": [
{ "id": "…", "role": "visitor", "content": "Bonjour, vous faites les cuisines sur mesure ?", "unanswered": null, "created_at": "…" },
{ "id": "…", "role": "assistant", "content": "Oui : …", "unanswered": false, "created_at": "…" }
]
}Compte, sites et chiffres
GET/me | Votre entreprise, sa formule et ses sites. Pratique pour vérifier une clé. |
GET/sites | Vos sites : identifiant (pour le filtre site), nom, adresse, état. |
GET/stats | Chiffres d’un mois, heure de Paris, essais exclus. Paramètres : month (AAAA-MM, le mois en cours par défaut), site. |
{
"object": "account",
"client": { "id": "8c1f…", "name": "Atelier Martin", "plan": "pro", "live_since": "2026-10-02T08:14:00+00:00" },
"sites": [
{ "object": "site", "id": "290583f4-e9d2-4528-b615-83b8e23fc7e1", "name": "Atelier Martin",
"url": "https://www.atelier-martin.fr", "status": "active", "created_at": "2026-09-30T09:42:12+00:00" }
]
}{
"object": "stats",
"month": "2026-10",
"site_id": null,
"conversations": 412,
"visitor_messages": 1386,
"leads": 37,
"hot_leads": 9,
"done_leads": 21,
"unanswered_questions": 14
}Webhooks
Dans Mon compte → API et webhooks, le propriétaire indique une adresse en https:// et choisit les événements. Marius y envoie une requête POST en JSON à chaque événement, en moins d’une minute.
| Événement | Quand |
|---|---|
lead.created | Un visiteur vient de laisser son e-mail ou son numéro : un nouveau prospect. |
lead.qualified | Sa fiche est prête : résumé, besoin, score, valeur estimée. Peut revenir si la conversation reprend. |
lead.updated | Son statut a changé (traité, à traiter), dans l’espace ou par l’API, ou ses coordonnées ont été complétées. |
ping | Le bouton « Envoyer un test » de Mon compte. |
data contient le prospect, sous la même forme que GET /leads/{id} (sans les notes), tel qu’il était au moment de l’événement.
POST https://votre-outil.exemple.fr/marius
Content-Type: application/json; charset=utf-8
User-Agent: marius-Webhooks/1.0 (+https://marius.qd.je/developpeurs) …
Marius-Event: lead.created
Marius-Event-Id: b7e2c9d0-6a1f-4f3e-8c2d-9e0f1a2b3c4d
Marius-Delivery: 0e9d8c7b-6a5f-4e3d-2c1b-0a9f8e7d6c5b
Marius-Signature: t=1760519581,v1=5f2b0c…
{
"id": "b7e2c9d0-6a1f-4f3e-8c2d-9e0f1a2b3c4d",
"object": "event",
"type": "lead.created",
"created_at": "2026-10-15T09:13:01.482+00:00",
"data": { "id": "6d1c4b0e-…", "object": "lead", "contact": { … }, … }
}- Répondez vite par un code 2xx, en moins de 10 secondes, puis traitez l’événement. Une redirection (3xx) compte comme un échec : indiquez l’adresse finale.
- Nouveaux essais. En cas d’échec, Marius réessaie après 1 minute, 5 minutes, 30 minutes, 2 heures, 6 heures et 12 heures. Après 7 tentatives manquées (environ 21 heures), l’événement est abandonné ; si votre adresse n’a rien reçu pendant tout ce temps, le webhook est désactivé et le propriétaire reçoit un e-mail. Il le réactive dans Mon compte.
- Doublons et ordre. Un même événement peut arriver deux fois, et deux événements peuvent arriver dans le désordre : dédoublonnez avec
id(ou l’en-têteMarius-Event-Id) et fiez-vous àdata.updated_at. - Événements manqués (webhook en pause ou désactivé) : ils ne sont pas renvoyés. Récupérez-les par l’API avec
updated_since. - Adresses acceptées :
https://, un nom de domaine public (pas d’adresse IP, pas de réseau privé), port 443 ou 8443, 5 webhooks par espace au plus. - Site plus en ligne : plus aucun événement ne part, et les clés sont refusées.
Vérifier la signature
Chaque envoi est signé avec le secret du webhook (whsec_…, montré une seule fois, à l’ajout du webhook). Vérifier la signature garantit que l’envoi vient bien de Marius et n’a pas été modifié.
L’en-tête Marius-Signature vaut t=<horodatage>,v1=<signature>. La signature est le HMAC-SHA256, en hexadécimal, de la chaîne <horodatage>.<corps brut>, avec le secret entier (whsec_ compris) comme clé. Calculez-la sur le corps reçu tel quel, avant de le décoder, et refusez un horodatage de plus de 5 minutes.
Node.js
import crypto from 'node:crypto'
// rawBody : le corps reçu tel quel (texte), AVANT JSON.parse
export function mariusSignatureOk(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')))
const t = Number(parts.t)
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false // plus de 5 minutes : refusé
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const given = String(parts.v1 ?? '')
return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))
}
// Express : app.post('/marius', express.raw({ type: 'application/json' }), (req, res) => {
// if (!mariusSignatureOk(req.body.toString('utf8'), req.get('Marius-Signature') ?? '', process.env.MARIUS_WEBHOOK_SECRET)) return res.sendStatus(401)
// const event = JSON.parse(req.body)
// res.sendStatus(204) // répondre vite, traiter ensuite
// })PHP
function marius_signature_ok(string $rawBody, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
$t = (int) ($p['t'] ?? 0);
if (!$t || abs(time() - $t) > 300) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, (string) ($p['v1'] ?? ''));
}
$raw = file_get_contents('php://input');
if (!marius_signature_ok($raw, $_SERVER['HTTP_MARIUS_SIGNATURE'] ?? '', getenv('MARIUS_WEBHOOK_SECRET'))) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);Python
import hashlib, hmac, time
def marius_signature_ok(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t = int(parts.get("t", "0") or 0)
if not t or abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Zapier, Make, n8n
Pas besoin d’écrire de code pour envoyer vos prospects dans un CRM :
- Zapier : déclencheur « Webhooks by Zapier », événement « Catch Hook ». Collez l’adresse donnée par Zapier dans Mon compte, envoyez un test, puis ajoutez l’action de votre CRM (créer un contact, une affaire…).
- Make : module « Webhooks », « Custom webhook ». Même principe : l’adresse de Make dans Mon compte, un test pour que Make découvre les champs.
- n8n : nœud « Webhook » en méthode POST ; l’adresse de production dans Mon compte.
Pour lire l’API depuis ces outils, utilisez leur module HTTP avec l’en-tête Authorization: Bearer mr_live_…. Les champs utiles se trouvent dans data.contact (nom, e-mail, téléphone), data.summary.need (le besoin) et data.espace_url (lien vers la fiche).
Erreurs et limites
Une erreur renvoie un code HTTP et un objet error : code est stable (à tester dans votre programme), message est une phrase en français. Chaque réponse porte un en-tête Marius-Request-Id : donnez-le au support si besoin.
HTTP/1.1 404 Not Found
Marius-Request-Id: 1f0e2d3c-…
{
"error": { "code": "not_found", "message": "Prospect introuvable." },
"request_id": "1f0e2d3c-…"
}| HTTP | code | Cause |
|---|---|---|
| 400 | invalid_parameter, invalid_json | Paramètre inconnu ou invalide, corps qui n’est pas un objet JSON. |
| 401 | missing_key, invalid_key, key_expired | Clé absente, mal formée, inconnue, supprimée ou expirée. |
| 403 | read_only_key | Modification demandée avec une clé en lecture seule. |
| 403 | not_live | Le site n’est plus en ligne. |
| 404 | not_found | Adresse inconnue, ou élément introuvable dans votre espace. |
| 405 | method_not_allowed | Méthode non acceptée (l’en-tête Allow donne les bonnes). |
| 413 | payload_too_large | Corps de plus de 10 000 caractères. |
| 429 | rate_limited | Plus de 60 appels en une minute avec la même clé : attendez le délai de l’en-tête Retry-After. |
| 500, 502 | server_error, unavailable | Erreur de notre côté : réessayez un peu plus tard. Une autre erreur 5xx, parfois sans corps JSON, vient du réseau : même conduite. |
Limites : 60 appels par minute et par clé, 10 clés et 5 webhooks par espace. Les 15 derniers appels et envois s’affichent dans Mon compte.
Données personnelles
Les prospects sont des données personnelles (nom, e-mail, téléphone, propos tenus). Marius les traite pour votre compte, comme sous-traitant. Quand vous les envoyez vers un autre outil par l’API ou un webhook, ce transfert et ce que cet outil en fait relèvent de votre responsabilité : choisissez des outils qui respectent le RGPD, ne gardez que ce dont vous avez besoin, et supprimez une clé dès qu’elle ne sert plus.
Marius garde le journal des appels de l’API 90 jours et les événements des webhooks 30 jours. Un prospect supprimé de votre espace (ou arrivé au bout de sa durée de conservation) disparaît aussi de l’API et des envois en attente.
Historique
Version actuelle : v1. Dans une même version, on ajoute des champs et des événements, on n’en retire jamais et on n’en change jamais le sens : ignorez simplement les champs que vous ne connaissez pas. Un changement incompatible ferait l’objet d’une v2, annoncée aux propriétaires des espaces.
- 15 octobre 2026
- Première version : prospects (lecture, statut, notes), conversations, sites, chiffres du mois ; webhooks
lead.created,lead.qualified,lead.updated.