APIs and webhooks
Dernière mise à jour : 15 octobre 2026
Translation provided for convenience; the French version prevails.
OpenAPI specification: openapi.json
A question: bonjour@marius.qd.je
To start up
Marius qualifies visitors to your site and turns them into prospects. The API and webhooks send them to your tools: your CRM (HubSpot, Pipedrive, Salesforce, Axonaut, etc.), a spreadsheet, Zapier, Make, n8n or your own software.
- Webhooks send each new lead to an address of your choice, as soon as it arrives. It's the simplest way to populate a CRM.
- The API allows your tool to reread everything whenever it wants: prospects, conversations, sites, figures of the month. It also allows you to mark a prospect as processed and add notes, which appear in your Marius space.
The API is open to customers whose site is online, all plans combined. THE owner of the space creates the keys and webhooks in its space: My account → API and webhooks.
All addresses start with https://marius.qd.je/api/v1. They respond in JSON (UTF-8). Dates follow ISO 8601, in UTC.
curl https://marius.qd.je/api/v1/me \
-H "Authorization: Bearer mr_live_votre_cle"Keys and authentication
Each call has a key in the header Authorization: Bearer mr_live_…. A key starts with mr_live_ and is displayed only once, at its creation: Marius only keeps a print, no one can find it. If it is lost, delete it and create another one.
- Scope. A key opens all the data in your space (all your sites), never that of another client.
- Read-only or edits. By default, a key only reads. Check "Allow changes" when it is created so that it can mark a prospect as processed and add notes.
- Expiry. Each key has an expiration date, chosen when it is created (two years at most). The owner receives an email seven days in advance.
- Deletion. A deleted key stops working immediately. It belongs to your company, not to a person: it remains valid if the owner of the space changes.
- Server side only. A key is kept like a password. Never put it in the code of a web page or a mobile application: the API also refuses calls made from a browser.
Lists and synchronization
Lists (/leads, /conversations) return one page at a time:
{
"object": "list",
"data": [ … ],
"has_more": true,
"next_cursor": "MjAyNi0xMC0xNVQwOToxMzowMS40ODIrMDA6MDB8NmQxYzRiMGUt…"
}| Setting | Role |
|---|---|
limit | Elements per page, from 1 to 100 (default 25). |
cursor | Next page: value next_cursor from the previous page, as is. |
order | desc (default): most recent first. asc : the oldest first, for synchronization. |
updated_since | Only what changed after this date (ISO 8601 with time zone, for example 2026-10-15T09:00:00Z). |
To keep a CRM up to date without a webhook, keep the largest updated_at received and go from there to the following passage:
# 1st time: all the history, from oldest to most recent
curl "https://marius.qd.je/api/v1/leads?order=asc&limit=100" -H "Authorization: Bearer $MARIUS_KEY"
# next page: resume next_cursor as long as has_more is true
curl "https://marius.qd.je/api/v1/leads?order=asc&limit=100&cursor=MjAyNi0x…" -H "Authorization: Bearer $MARIUS_KEY"
# then, every 15 minutes: only what has changed since then
# largest updated_at already received
curl "https://marius.qd.je/api/v1/leads?order=asc&updated_since=2026-10-15T09:13:01.482Z" -H "Authorization: Bearer $MARIUS_KEY"An unknown parameter is refused (error 400): a typing error never goes unnoticed.
Prospects
A prospect is a visitor who has left their email or number. Tests made with "Tester Marius" never appear.
GET/leads | List. Filters: site (site identifier), status (new, done), score (hot, warm, cold), plus those of the paging. Sorted by updated_at. |
GET/leads/{id} | A prospect, with your team's notes (notes). |
PATCH/leads/{id} | Change the status: {"status": "done"} (processed) or {"status": "new"} (to be processed). Key with modifications. |
GET/leads/{id}/notes | Team notes on this prospect. |
POST/leads/{id}/notes | Add a note ({"text": "..."}, 2,000 characters at most), signed "API · key name". Key with 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@example.fr", "phone": "06 12 34 56 78" },
"answers": [
{ "key": "project", "label": "What is your project?", "value": "Renovate the kitchen", "answered_at": "2026-10-15T09:12:44Z" },
{ "key": "delay", "label": "When?", "value": "Before Christmas", "answered_at": "2026-10-15T09:13:01Z" }
],
"score": {
"level": "hot",
"points": 80,
"criteria": [ { "id": "budget", "label": "Indicated budget", "points": 25 } ]
},
"estimated_value_eur": 8500,
"summary": {
"text": "Claire wants to renovate her kitchen before Christmas and has a set budget.",
"need": "Complete kitchen renovation",
"next_step": "Call her back this week to schedule a visit.",
"score_reason": "Contact details left, budget and deadline given."
},
"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(to be processed) ordone(processed, withdone_atAnddone_by).contact- Name, email and telephone number left by the visitor;
nullfor what he did not give. answers- Answers to the questions in your questionnaire, in their order.
score- Level (
hot,warm,cold), points out of 100 and criteria met.nulluntil the form is completed. estimated_value_eur- Estimated value, if you have provided your prices; Otherwise
null. summary- Summary, need, recommended next step and reason for score. Done once the conversation has died down (30 minutes without a message), done again if it resumes.
space_url- The file in your Marius space, to be linked in your 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": "Quote sent on 10/16."}'
# answer (201)
{ "id": "f0c1…", "object": "note", "author": "API · HubSpot", "text": "Quote sent on 10/16.",
"created_at": "2026-10-16T08:02:11+00:00", "edited_at": null }Conversations
GET/conversations | List, sorted by date of last message. Filters: site, has_lead (true : only those who gave a lead), updated_since (those who have a message after this date), plus the paging. |
GET/conversations/{id} | A conversation with its messages (500 at most), in order. |
In messages, role worth visitor Or assistant ; unanswered points out a response from Marius who did not have the 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": "Hello, do you make custom kitchens?",
"lead_id": "6d1c4b0e-2f7a-4c55-9a1e-3b8d2c7f0a19",
"messages": [
{ "id": "…", "role": "visitor", "content": "Hello, do you do custom kitchens?", "unanswered": null, "created_at": "…" },
{ "id": "…", "role": "assistant", "content": "Yes:…", "unanswered": false, "created_at": "…" }
]
}Account, sites and figures
GET/me | Your company, its formula and its sites. Convenient for checking a key. |
GET/sites | Your sites: identifier (for the filter site), name, address, state. |
GET/stats | Figures for one month, Paris time, tests excluded. Settings: month (YYYY-MM, the current month by default), 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
In My account → API and webhooks, the owner indicates an address in https:// and chooses the events. Marius sends a request there POST in JSON for each event, in less than a minute.
| Event | When |
|---|---|
lead.created | A visitor has just left their email or number: a new prospect. |
lead.qualified | His form is ready: summary, need, score, estimated value. May return if the conversation resumes. |
lead.updated | Its status has changed (processed, to be processed), in space or by the API, or its contact details have been completed. |
ping | The "Send a test" button in My Account. |
data contains the prospect, in the same form as GET /leads/{id} (without notes), as it was at the time of the event.
POST https://votre-outil.agence.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": { … }, … }
}- Respond quickly with a 2xx code, in less than 10 seconds, then process the event. A redirection (3xx) counts as a failure: indicate the final address.
- New tests. If unsuccessful, Marius tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. After 7 failed attempts (around 21 hours), the event is abandoned; If your address has not received anything during this time, the webhook is disabled and the owner receives an email. He reactivates it in My Account.
- Duplicates and order. The same event can happen twice, and two events can happen out of order: duplicate with
id(or the headerMarius-Event-Id) and rely ondata.updated_at. - Missed events (webhook paused or disabled): they are not returned. Retrieve them through the API with
updated_since. - Addresses accepted :
https://, a public domain name (no IP address, no private network), port 443 or 8443, 5 webhooks per space at most. - Site no longer online : no more events are sent, and the keys are refused.
Verify signature
Each shipment is signed with the webhook secret (whsec_…, shown only once, when adding the webhook). Verifying the signature ensures that the submission comes from Marius and has not been modified.
The header Marius-Signature worth t=<timestamp>,v1=<signature>. The signature is HMAC-SHA256, in hexadecimal, of the string <timestamp>.<raw body>, with the entire secret (whsec_ included) as a key. Calculate it on the body received as is, before decoding it, and refuse a timestamp of more than 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) // respond quickly, process later
// })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
No need to write code to send your prospects to a CRM:
- Zapier : "Webhooks by Zapier" trigger, "Catch Hook" event. Paste the address given by Zapier into My account, send a test, then add the action of your CRM (create a contact, a deal, etc.).
- Make : "Webhooks", "Custom webhook" module. Same principle: Make's address in My account, a test so that Make discovers the fields.
- n8n : "Webhook" node in POST method; the production address in My Account.
To read the API from these tools, use their HTTP module with the header Authorization: Bearer mr_live_…. Useful fields can be found in data.contact (name, email, telephone), data.summary.need (the need) and data.espace_url (link to the file).
Errors and limitations
An error returns an HTTP code and an object error : code is stable (to be tested in your program), message is a phrase in French. Each response has a header Marius-Request-Id : give it to support if necessary.
HTTP/1.1 404 Not Found
Marius-Request-Id: 1f0e2d3c-…
{
"error": { "code": "not_found", "message": "Lead not found." },
"request_id": "1f0e2d3c-…"
}| HTTP | code | Cause |
|---|---|---|
| 400 | invalid_parameter, invalid_json | Unknown or invalid parameter, body that is not a JSON object. |
| 401 | missing_key, invalid_key, key_expired | Key missing, malformed, unknown, deleted or expired. |
| 403 | read_only_key | Change requested with a read-only key. |
| 403 | not_live | The site is no longer online. |
| 404 | not_found | Unknown address, or item not found in your space. |
| 405 | method_not_allowed | Method not accepted (the header Allow give the correct ones). |
| 413 | payload_too_large | Body of more than 10,000 characters. |
| 429 | rate_limited | More than 60 calls in a minute with the same key: wait for header delay Retry-After. |
| 500, 502 | server_error, unavailable | Error on our side: try again a little later. Another 5xx error, sometimes without a JSON body, comes from the network: same behavior. |
Limits: 60 calls per minute per key, 10 keys and 5 webhooks per space. The last 15 calls and sendings are displayed in My Account.
Personal data
Prospects are personal data (name, email, telephone number, comments made). Marius processes them on your behalf, as a subcontractor. When you send them to another tool via the API or a webhook, this transfer and what this tool does with it are your responsibility: choose tools that respect the GDPR, only keep what you need, and delete a key as soon as it is no longer needed.
Marius keeps the API call log for 90 days and webhooks events for 30 days. A prospect deleted from your space (or at the end of its retention period) also disappears from the API and pending shipments.
Historical
Current version: v1. In the same version, we add fields and events, we never remove them and we never change their meaning: simply ignore the fields that you do not know. An incompatible change would be the subject of a v2, announced to the space owners.
- October 15, 2026
- First version: prospects (reading, status, notes), conversations, sites, figures of the month; webhooks
lead.created,lead.qualified,lead.updated.