Passer au contenu
MenuiserieAi

API v1

Branchez la simulation
sur votre logiciel

Lancez des simulations de menuiserie depuis votre CRM, votre ERP ou votre application, et récupérez les rendus. Odoo est notre premier intégrateur — l'API, elle, ne connaît aucun logiciel en particulier.

Démarrer en cinq minutes

Une simulation se lance en un appel. La réponse est un 202 : le rendu n'est pas prêt, il arrivera dans la minute.

curl -X POST https://menuiserieai.fr/api/v1/simulations \
  -H "Authorization: Bearer mai_live_…" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "category": "portail",
    "site_photo_url": "https://votre-domaine.fr/photo.jpg",
    "material": "aluminium",
    "color": "gris-anthracite",
    "numero_devis": "DEV-2026-118"
  }'

# → 202 { "id": "job_cbae…", "status": "pending" }

Puis suivez l'avancement, toutes les 5 à 10 secondes :

curl https://menuiserieai.fr/api/v1/jobs/job_cbae… \
  -H "Authorization: Bearer mai_live_…"

# → { "status": "succeeded", "simulation_id": "sim_…" }
# Lisez ensuite GET /v1/simulations/sim_… pour l'adresse du rendu.
Préférez les notifications au sondage. Nous poussons un événement simulation.succeeded vers l'adresse de votre choix, signé et rejoué jusqu'à huit fois. Demandez-le en même temps que votre clé.

Le parcours complet

Ce qu'un devis produit, du clic de votre commercial au rendu affiché chez vous. En Node ; la transposition dans un autre langage est directe.

const MAI = 'https://menuiserieai.fr/api/v1';
const H = {
  Authorization: `Bearer ${process.env.MAI_KEY}`,
  'Content-Type': 'application/json',
  // Le commercial à qui la simulation sera imputée.
  'X-MAI-On-Behalf-Of': 'jean.martin@votre-reseau.fr',
};

// ① Lancer. La clé d'idempotence protège d'un double débit sur relance.
const job = await fetch(`${MAI}/simulations`, {
  method: 'POST',
  headers: { ...H, 'Idempotency-Key': crypto.randomUUID() },
  body: JSON.stringify({
    category: 'portail',
    site_photo_url: 'https://votre-domaine.fr/chantier.jpg',
    material: 'aluminium',
    color: 'gris-anthracite',
    numero_devis: 'DEV-2026-118',
    external_ref: 'crm:devis:118',   // votre identifiant, republié tel quel
  }),
}).then((r) => r.json());

// ② Attendre. Le webhook est préférable ; le sondage reste le repli.
let etat = job;
while (etat.status === 'pending' || etat.status === 'processing') {
  await new Promise((r) => setTimeout(r, 5000));   // 5 s, pas moins
  etat = await fetch(`${MAI}/jobs/${job.id}`, { headers: H })
    .then((r) => r.json());
}

if (etat.status === 'failed') {
  // Le crédit n'a PAS été débité : une génération échouée ne coûte rien.
  throw new Error(etat.error?.code ?? 'echec');
}

// ③ Lire le rendu. Le détail publie des champs absents de la liste.
const simulation = await fetch(`${MAI}/simulations/${etat.simulation_id}`, {
  headers: H,
}).then((r) => r.json());

console.log(simulation.generated_image_url);   // à afficher dans votre devis
Ne sondez pas plus vite que toutes les 5 secondes. Un rendu prend entre 30 et 90 secondes : interroger chaque seconde ne le rendra pas plus rapide, mais vous rapprochera de la limite de cadence.

Authentification

Une clé, un en-tête. Rien d'autre.

Authorization: Bearer mai_live_78ac4b4c755c_…

La clé porte une portée — ce qu'elle peut faire — et un périmètre — ce qu'elle peut voir. Une clé d'agence ne verra jamais les dossiers d'une autre agence, quoi qu'elle demande. Ce n'est pas un filtre que vous appliquez : c'est une contrainte que nous appliquons.

Imputer au bon commercial

Une clé appartient à un compte, pas à une personne. Pour qu'une simulation soit attribuée au commercial qui l'a lancée — et non au siège — indiquez son adresse :

X-MAI-On-Behalf-Of: jean.martin@votre-reseau.fr

L'adresse doit correspondre à celle d'un membre de l'organisation. Elle n'est jamais déclarée : elle est vérifiée. Une adresse inconnue reçoit un ACTOR_NOT_IN_ORGANIZATION, jamais un rattachement approximatif.

Cet en-tête exige la portée act_on_behalf et une autorisation explicite sur la clé — deux verrous, parce qu'imputer une consommation à quelqu'un d'autre n'est pas anodin.

Recevoir les rendus (webhooks)

Plutôt que d'interroger en boucle, laissez-nous vous prévenir. Nous poussons un événement vers l'adresse HTTPS de votre choix, dès que le rendu est prêt.

Les événements

ÉvénementQuand
simulation.succeededLe rendu est prêt. La charge utile porte son adresse.
simulation.failedLa génération a échoué. Aucun crédit débité.
video.succeededUne vidéo est prête.
video.failedLa vidéo a échoué.
lead.createdUn prospect est arrivé par le simulateur public.
lead.routedUn prospect a été attribué à une agence.
lead.updatedLe statut d’un prospect a changé.

Ce que vous recevez

POST https://votre-domaine.fr/hooks/menuiserieai
X-MAI-Event-Type: simulation.succeeded
X-MAI-Event-Id: evt_9f2a…              ← pour dédupliquer
X-MAI-Signature: t=1785312045,v1=8c1f…  ← à vérifier, voir plus bas
X-MAI-Delivery-Attempt: 1

{
  "id": "evt_9f2a…",
  "type": "simulation.succeeded",
  "created_at": "2026-09-02T09:14:05Z",
  "data": {
    "simulation_id": "sim_…",
    "job_id": "job_…",
    "external_ref": "crm:devis:118",
    "numero_devis": "DEV-2026-118",
    "generated_image_url": "https://…"
  }
}

Vérifier la signature

Obligatoire. Sans cette vérification, n'importe qui connaissant votre adresse peut vous injecter de faux rendus. Le format est celui de Stripe — vos bibliothèques existantes s'appliquent.

import { createHmac, timingSafeEqual } from 'crypto';

function verifier(rawBody, entete, secret) {
  const parts = Object.fromEntries(
    entete.split(',').map((p) => p.split('=')),
  );
  const t = parts.t;

  // ① Rejouer une requête vieille de plusieurs heures ne doit pas passer.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  // ② La signature porte sur « horodatage.corps », corps BRUT — pas
  //    re-sérialisé : un JSON.parse suivi d'un JSON.stringify change les
  //    octets, et la signature ne correspondra plus.
  const attendu = createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  // ③ Comparaison en temps constant : une comparaison naïve laisse
  //    deviner la signature octet par octet.
  const a = Buffer.from(attendu);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}
Répondez 2xx rapidement. Mettez l'événement en file et traitez-le après. Nous abandonnons la tentative au-delà de 8 secondes et la comptons comme un échec — un traitement synchrone un peu lent suffit à déclencher des renvois inutiles.
Nous réessayons huit fois, en espaçant progressivement. Un incident de votre côté ne perd donc pas l'événement — mais vous le recevrez plusieurs fois.
Dédupliquez sur X-MAI-Event-Id. C'est la conséquence directe du point précédent : sans cette garde, un rendu pourrait être attaché huit fois au même devis.
Pendant une rotation de secret, l'en-tête porte deux signatures séparées par une virgule. Acceptez l'événement si l'une des deux correspond — sinon toute rotation provoquerait une coupure.

L'adresse de réception et le secret se configurent avec votre clé : demandez-les en même temps.

Les routes

12 opérations, engendrées depuis le code. Si une route figure ici, elle existe.

MéthodeRouteRôle
POST/v1/simulationsLancer une simulation
GET/v1/simulationsLister les simulations
GET/v1/simulations/{id}Lire une simulation
GET/v1/jobs/{id}Suivre un rendu
POST/v1/embed_sessionsOuvrir un écran MenuiserieAI
POST/v1/uploadsObtenir une adresse de dépôt
GET/v1/metaCatégories, matériaux, styles, coloris
GET/v1/productsLe catalogue de modèles
GET/v1/leadsLister les prospects
POST/v1/leadsTransmettre un prospect
GET/v1/usageConsommation et quota
GET/v1/agenciesLister les agences

Ouvrir nos écrans chez vous

Plutôt que de reproduire notre interface, ouvrez-la. Un appel renvoie une adresse : votre utilisateur y voit l'écran réel de MenuiserieAI, sans se reconnecter, avec son devis déjà rempli.

curl -X POST https://menuiserieai.fr/api/v1/embed_sessions \
  -H "Authorization: Bearer mai_live_…" \
  -H "X-MAI-On-Behalf-Of: jean.martin@votre-reseau.fr" \
  -H "Content-Type: application/json" \
  -d '{ "destination": "simulate", "quote_ref": "DEV-2026-118" }'

# → 201 { "url": "https://menuiserieai.fr/embed/simulate#…", "expires_in": 1800 }
# Ouvrez cette adresse dans un onglet. Usage unique.

Quinze écrans sont ouvrables : l'assistant de simulation, le studio conversationnel, la bibliothèque, les vidéos, le catalogue, les outils de retouche. La liste est fermée — un chemin libre serait refusé, faute de quoi cette adresse deviendrait une redirection ouverte vers n'importe quelle page authentifiée.

L'adresse vaut une session. Ne la journalisez pas, ne la mettez pas en cache, ouvrez-la immédiatement. Elle expire en 30 minutes et ne sert qu'une fois.

Codes d’erreur

Branchez-vous sur le code, jamais sur le message : le premier est stable, le second peut être reformulé. La colonne Rejouable dit si réessayer a une chance d'aboutir — sur un QUOTA_EXCEEDED, une boucle de reprise ne fera qu'user votre journal.

HTTPCodeRejouableSignification
400API_VERSION_UNSUPPORTEDnonVersion d'API inconnue ou retirée. Mettez à jour le connecteur ; voir l'en-tête Sunset.
400IDEMPOTENCY_KEY_REQUIREDnonL'en-tête Idempotency-Key est obligatoire sur cet endpoint. Générez un UUID v4 par tentative métier et réutilisez-le sur les rejeux.
400IDEMPOTENCY_KEY_TOO_LONGnonLa clé d'idempotence dépasse 255 caractères.
400INVALID_CURSORnonLe curseur de pagination est invalide ou ne correspond plus aux filtres. Relancez la pagination depuis le début, sans curseur.
400MALFORMED_JSONnonLe corps de la requête n'est pas un JSON valide. Corrigez la sérialisation.
401INVALID_API_KEYnonClé d'API absente ou invalide. Vérifiez l'en-tête « Authorization: Bearer mai_live_… ». Ne réessayez pas : prévenez l'administrateur du compte.
401KEY_EXPIREDnonCette clé d'API a expiré. Faites-la renouveler avant sa date d'expiration.
401KEY_REVOKEDnonCette clé d'API a été révoquée. Arrêtez les tâches planifiées et demandez une nouvelle clé au siège.
402MEMBER_ALLOCATION_EXCEEDEDnonLe plafond individuel de ce commercial est atteint. Son responsable d'agence peut le réajuster.
402PLAN_REQUIREDnonCette fonctionnalité n'est pas incluse dans l'offre du compte.
402QUOTA_EXCEEDEDnonQuota de simulations épuisé pour la période en cours. Ne réessayez pas avant le renouvellement (voir « resets_at » de GET /v1/usage).
402SUBSCRIPTION_INACTIVEnonL'abonnement du compte n'est pas actif. Prévenez la comptabilité ; ne réessayez pas avant régularisation.
402VIDEO_QUOTA_EXCEEDEDnonQuota de vidéos épuisé pour la période en cours.
403ACTOR_OUT_OF_AGENCY_SCOPEnonLe commercial indiqué n'appartient pas à l'agence de cette clé. Corrigez la correspondance entre son compte et son membre MenuiserieAI.
403AGENCY_OUT_OF_SCOPEnonLe paramètre « agency_id » est hors du périmètre de cette clé. Retirez le filtre, ou utilisez la clé de siège.
403BROWSER_ORIGIN_FORBIDDENnonCette API est serveur-à-serveur. Une clé « mai_live_ » exposée dans un navigateur doit être considérée comme compromise : retirez l'appel du front et faites tourner la clé.
403ON_BEHALF_NOT_ALLOWEDnonCette clé n'est pas autorisée à agir au nom d'un commercial (en-tête X-MAI-On-Behalf-Of). Demandez l'activation au siège.
403SCOPE_MISSINGnonCette clé d'API ne porte pas le droit requis pour cette opération. Demandez au siège d'ajouter le scope indiqué dans « details ».
404NOT_FOUNDnonRessource introuvable.
405METHOD_NOT_ALLOWEDnonMéthode HTTP non autorisée sur cette ressource. Voir l'en-tête Allow.
409IDEMPOTENCY_CONFLICTnonCette clé d'idempotence a déjà été utilisée avec un corps différent. Une clé = une tentative métier : générez-en une neuve.
409IDEMPOTENCY_IN_PROGRESSouiUne requête portant cette clé est en cours de traitement.
409JOB_NOT_CANCELABLEnonCe job n'est plus annulable : il est déjà terminé.
409WEBHOOK_URL_ALREADY_REGISTEREDnonCette URL est déjà enregistrée pour cette organisation.
413PAYLOAD_TOO_LARGEnonLe corps de la requête dépasse la limite acceptée. Utilisez le flux d'upload en deux temps (POST /v1/uploads).
415UNSUPPORTED_MEDIA_TYPEnonType de contenu non supporté. « application/json » est attendu.
422ACTOR_NOT_ACTIVEnonLe commercial indiqué n'a pas d'accès actif (invitation en attente, refusée ou révoquée). Faites accepter l'invitation, ou réassignez.
422ACTOR_NOT_IN_ORGANIZATIONnonLe commercial indiqué n'appartient pas à cette organisation. Corrigez le mapping ; voir GET /v1/members.
422ACTOR_REQUIREDnonAucun commercial n'a été indiqué et cette clé n'a pas d'acteur par défaut. Renseignez l'en-tête X-MAI-On-Behalf-Of.
422IMAGE_UNREACHABLEnonL'URL de la photo de chantier n'est pas accessible depuis nos serveurs. Passez par POST /v1/uploads.
422INVALID_CATEGORYnonCatégorie de menuiserie inconnue. Utilisez un slug de GET /v1/meta/categories.
422INVALID_MODEnonLe mode doit valoir « add » ou « replacement ».
422INVALID_STATUSnonStatut de lead invalide. Valeurs : nouveau, contacte, devis, signe, pose.
422NO_IMAGEnonCette simulation n'a pas de rendu : la vidéo est impossible. Attendez « status: succeeded ».
422VALIDATION_ERRORnonLe corps de la requête est invalide. Chaque entrée de « details » porte le champ concerné, le motif et la valeur reçue.
422WEBHOOK_URL_FORBIDDENnonURL de webhook refusée : HTTPS sur le port 443 obligatoire, adresses privées interdites.
429CONCURRENCY_LIMIT_REACHEDouiTrop de générations simultanées pour cette clé ou cette organisation. Réessayez après le délai indiqué, sans augmenter le parallélisme.
429DAILY_CAP_REACHEDnonLe plafond quotidien de générations de ce compte est atteint. Reprenez demain, ou demandez un relèvement au siège.
429RATE_LIMITEDouiTrop de requêtes. Réessayez après le délai indiqué par l'en-tête Retry-After.
500INTERNALouiErreur interne. Réessayez une fois avec la même Idempotency-Key, puis transmettez la référence « request_id » au support.
503DEPENDENCY_UNAVAILABLEouiUn service dont dépend l'API est momentanément indisponible. Réessayez après le délai indiqué, avec la même Idempotency-Key.
503PROVIDER_UNAVAILABLEouiLe moteur de rendu est indisponible. Aucun crédit n'a été débité ; réessayez après le délai indiqué.
503SCHEMA_UNAVAILABLEnonIncident de configuration côté MenuiserieAI. Ne réessayez pas en boucle : alertez le support avec la référence « request_id ».

Portées

Demandez la portée minimale. Une clé qui ne fait que lire ne pourra jamais consommer un crédit, même exploitée.

PortéeCe qu’elle autorise
simulations:readLire GET /v1/simulations et GET /v1/simulations/{id}.
simulations:writeCréer POST /v1/simulations (202) et supprimer DELETE /v1/simulations/{id}.
leads:readLire GET /v1/leads.
leads:writeCréer POST /v1/leads.
uploads:writeObtenir une URL d'upload signée via POST /v1/uploads.
agencies:readLire GET /v1/agencies (le réseau vu par la clé).
usage:readLire GET /v1/usage (quotas, consommation, date de renouvellement).
act_on_behalfEmployer l'en-tête X-MAI-On-Behalf-Of pour imputer une action à un commercial précis (et donc à son plafond individuel `TeamMember.allocated_credits`).

Les pièges à éviter

Ne recopiez pas les catégories

Rapatriez-les depuis GET /v1/meta, une fois par jour. Une valeur écrite en dur sera refusée le jour où le référentiel évolue — et votre commercial ne comprendra pas pourquoi.

Envoyez toujours une Idempotency-Key

Sans elle, une coupure réseau pendant un POST /v1/simulations peut consommer deux crédits pour une seule demande. Avec elle, rejouer le même appel renvoie le même travail.

C'est l'image du modèle qui pilote le rendu

product_reference_url est la référence technique à reproduire. Les libellés — matériau, style, coloris — n'alimentent que le devis. Une photo de modèle vaut mieux que dix champs bien remplis.

La photo du chantier doit être en HTTPS

Une adresse en clair est refusée. Si votre photo n'est pas publiquement accessible, passez par POST /v1/uploads, qui vous donne une adresse de dépôt signée.

Paginez par curseur, jamais par décalage

Utilisez next_cursor. Un décalage numérique décale toutes vos pages dès qu'une simulation est créée pendant votre parcours.

N'exposez jamais votre clé côté navigateur

Elle donne accès à l'ensemble de votre périmètre. Les appels partent de votre serveur ; pour l'interface, utilisez POST /v1/embed_sessions, dont l'adresse est à usage unique et expire en 30 minutes.

Une question ?

Chaque réponse d'erreur porte un request_id. Citez-le : il nous permet de retrouver l'appel exact, sans avoir à vous demander de reproduire le problème.

Nous écrire
MenuiserieAi

L'Intelligence Artificielle au service de la menuiserie

Besoin d'aide ?

Réponse sous 24h