Guide d'intégration ERP et systèmes tiers

Cadrer une intégration avant de la construire : ce que l'API atteint, ce qu'elle n'atteint pas, et où sont les limites

9 min de lecture
Imprimer cette section

Ce guide s'adresse au développeur qui cadre une intégration entre FreeMaint et un autre système — un ERP, un historien SCADA, un logiciel de gestion d'entrepôt ou de comptabilité. Les autres articles de cette section expliquent comment appeler l'API ; celui-ci parle délibérément de frontières, parce que l'erreur coûteuse n'est jamais un en-tête erroné : c'est de découvrir à mi-parcours qu'une capacité que l'on croyait acquise n'existe pas. Chaque limite énoncée ci-dessous l'est volontairement.

Une clé, trois portes

Une clé d'API (elle commence par fmk_) se crée dans Paramètres de l'entreprise > API (/company-settings/api) par un administrateur, ou par une personne pour elle-même sous Profil > Sécurité > Clés API, et s'envoie sous la forme Authorization: Bearer fmk_... . Elle authentifie exactement trois choses : le point d'entrée GraphQL POST /api/v1/graphql, le point d'ingestion énergie, ainsi que /public-api/whoami et /public-api/ping. Elle n'authentifie PAS les routes REST sous /api/v1/{resource} : celles-ci constituent l'API interne de l'application et exigent une session utilisateur (JWT). Construisez votre intégration sur GraphQL, c'est la surface qu'une clé atteint.

Ce que la clé d'API vous donne

La clé en clair n'est affichée qu'une seule fois, à la création, et devient irrécupérable ensuite : seul un hachage SHA-256 est conservé, avec les 6 caractères qui suivent fmk_ pour que la liste puisse identifier la clé à l'écran.

Nom

Un libellé lisible — « Passerelle ERP Oracle », « Borne ligne 2 ». Créez une clé par système consommateur : vous pourrez en révoquer une sans perturber les autres.

Niveau d'accès

READ, READ_STOCK ou FULL, une hiérarchie ordonnée qui s'ajoute aux autorisations du propriétaire. READ n'exécute que des requêtes de lecture et c'est la valeur par défaut. READ_STOCK y ajoute la mutation adjustStock. FULL autorise toutes les opérations que le rôle du propriétaire permet, y compris la création, la modification et la suppression — ainsi que le point d'ingestion énergie lorsque le propriétaire peut créer des relevés d'énergie.

Expiration

Un expiresAt facultatif. Passé cet instant, la clé cesse d'authentifier. Laissez-le vide pour une intégration de service permanente ; renseignez-le pour un prestataire ou un pilote.

Révocation

isActive est un interrupteur par clé, effectif en quelques secondes. La ligne est conservée pour l'audit : une clé révoquée reste traçable au lieu de disparaître.

Dernière utilisation

lastUsedAt enregistre le dernier appel authentifié — de quoi repérer une clé que plus personne n'utilise.

Propriétaire

La personne au nom de laquelle la clé agit. Chaque appel porte ses autorisations et sa visibilité sur les données, et son nom signe ce que la clé écrit. Pour un ERP, créez un utilisateur dédié — « Intégration ERP » — avec un rôle qui couvre exactement ce dont la passerelle a besoin, et confiez-lui la clé.

Ce que la clé d'API ne vous donne pas

Ce sont de vraies limites de l'implémentation actuelle, pas des oublis de cette page. Si votre revue de sécurité dépend de l'une d'elles, concevez en conséquence maintenant plutôt qu'après la construction.

  • Aucune liste de modules par clé. Une clé lit et écrit ce que le rôle de son propriétaire autorise — pour la restreindre, confiez-la à un propriétaire dont le rôle est plus restreint. Par-dessus, seuls les trois niveaux d'accès s'appliquent : vous ne pouvez pas énumérer, sur la clé elle-même, les modules qu'elle peut utiliser.
  • Aucune liste blanche d'adresses IP. Une clé est valable depuis n'importe où sur Internet. Traitez-la comme un secret : coffre-fort ou gestionnaire de secrets de votre plateforme, jamais un dépôt de code, un disque partagé ou un ticket de support.
  • Aucun journal d'usage par clé. Seul lastUsedAt est conservé. Il n'existe ni historique d'appels par clé, ni quota par clé, ni trace indiquant quelle clé a effectué quelle opération. Le compteur de quota quotidien est celui de l'ENTREPRISE et il est partagé par toutes les clés : une intégration emballée peut donc épuiser le quota de toutes les autres.
  • Aucune approbation au nom de quelqu'un. Une clé n'approuve jamais une étape du flux de travail, même quand son propriétaire le pourrait : les approbations restent un acte humain dans l'application.
  • Une seule entreprise par clé. Une clé est liée à l'entreprise où elle a été créée et ne peut pas atteindre les données d'une autre entreprise : un enregistrement d'une autre entreprise est tout simplement introuvable.

Ce que vous pouvez lire

GraphQL expose 16 requêtes de lecture : whoami, workOrders, workOrder(id), assets, asset(id), assetDowntimes, meters(assetId), parts, part (par id ou code-barres), stockMovements, locations, vendors, customers, purchaseOrders, purchaseOrder(id | reference | erpReference) et floorPlans(locationId). Chaque liste peut être parcourue jusqu'au bout : workOrders, assets, assetDowntimes, meters, parts, purchaseOrders, locations, vendors et customers acceptent tous limit (de 1 à 200) et offset, et workOrders, assets, parts, purchaseOrders, locations, vendors et customers acceptent aussi updatedSince pour les synchronisations incrémentales. La page par défaut compte 50 lignes, sauf pour locations, vendors et customers, qui conservent leur ancienne valeur par défaut de 200. Chaque liste est triée avec un critère de départage sur l'id, si bien qu'une limite de page ne saute ni ne répète jamais une ligne. stockMovements est le journal des stocks à partir duquel vous faites vos rapprochements : il se lit du plus ancien au plus récent et se pagine avec afterId — passez l'id du dernier mouvement reçu —, ce qui reste exact pendant que de nouveaux mouvements sont écrits, et since permet de lancer une reprise de l'historique à partir d'une date. Les ordres de travail placés dans la corbeille ne sont jamais renvoyés. purchaseOrders se filtre aussi par status et vendorId ; les montants sont dans la devise de l'entreprise, que chaque commande porte dans son champ currency.

Vérifier la clé et lire des bons de travail en un seul appel : curl -X POST https://freemaint.com/api/v1/graphql -H "Authorization: Bearer $FMK" -H "Content-Type: application/json" -d '{"query":"{ whoami { companyId tier } workOrders(limit: 5, status: \"OPEN\") { id reference title status priority dueDate } }"}'

Ce que vous pouvez écrire

Douze mutations : création, modification et suppression sur les bons de travail, les équipements et les pièces, plus adjustStock pour un mouvement de stock signé. Elles passent par les mêmes services que l'application elle-même : les références (WO-…, ASS-…, PART-…), les lignes d'audit et les notifications se comportent donc exactement comme dans l'interface. Pour le stock, préférez adjustStock : elle écrit une ligne d'audit StockMovement dans la même transaction et refuse de faire passer une quantité sous zéro, là où updatePart(quantity) définit une valeur absolue et enregistre l'écart comme un ajustement. Sur une pièce suivie par emplacement, passez locationId à adjustStock ; updatePart(quantity) y est refusé. La onzième, addMeterReading, enregistre un relevé de compteur par le même service que l'écran Compteurs : déclencheurs et maintenance préventive sur compteur réagissent exactement comme pour un relevé saisi dans l'application. La douzième, setPurchaseOrderErpReference, enregistre le numéro de document de votre ERP sur un bon de commande, pour rapprocher plus tard les deux enregistrements avec purchaseOrder(erpReference: …). Elle exige une clé à accès FULL et ne déclenche volontairement aucun webhook : votre ERP ne reçoit jamais sa propre écriture sous forme d'événement.

Ce que l'API ne couvre pas du tout

Le schéma GraphQL contient les ordres de travail, les équipements, les arrêts machine, les pièces et leurs mouvements de stock, les compteurs, les bons de commande, les emplacements, les fournisseurs, les clients et les plans. Rien d'autre. Les éléments suivants sont des parties pleinement fonctionnelles de FreeMaint qui n'ont AUCUNE surface API aujourd'hui : ils ne peuvent être ni lus, ni écrits, ni abonnés depuis une intégration, et aucune combinaison des requêtes existantes ne les atteint.

  • Les inspections — ni les modèles, ni les campagnes planifiées, ni les résultats enregistrés, ni les codes d'action. Il n'existe aucune requête, aucune mutation et aucun événement webhook d'inspection.
  • Les utilisateurs, rôles et équipes — vous ne pouvez ni lister, ni créer, ni désactiver un utilisateur via cette API. Le provisionnement se fait dans l'application ; avec l'offre Enterprise, il peut être automatisé via SCIM, qui est un mécanisme distinct et ne fait pas partie de cette API.
  • Les projets — aucune requête, aucune mutation.
  • Les demandes d'achat — aucune requête, aucune mutation. Les bons de commande sont lisibles (purchaseOrders, purchaseOrder), votre ERP peut y réécrire sa référence (setPurchaseOrderErpReference) et les webhooks purchase_order.* les annoncent, mais une commande ne peut être ni créée, ni modifiée, ni réceptionnée par l'API : cela reste dans l'application.
  • Les fichiers, photos et pièces jointes — vous ne pouvez ni téléverser un document ou une photo, ni en relire un. Les photos de bons de travail, les images de pièces et les preuves d'inspection sont hors API. Une URL présente dans une réponse, comme l'imageUrl d'une pièce, n'est pas un chemin de téléversement.
  • Les plans de maintenance préventive, les pointages, les tâches et les listes de contrôle — non exposés.

Important

Cadrez votre développement sur cette liste, pas sur ce que le produit sait faire dans un navigateur. Les applications web et mobile de FreeMaint s'appuient sur une API REST interne qu'une clé d'API ne peut pas atteindre : qu'une fonctionnalité soit visible à l'écran ne prouve donc pas qu'une intégration puisse la piloter.

Webhooks — le catalogue d'événements

Les webhooks sont la moitié « push » d'une intégration : au lieu d'interroger l'API, FreeMaint appelle votre URL en POST dès qu'un événement survient. Vous les configurez dans Paramètres de l'entreprise > Webhooks (/company-settings/webhooks) et vous abonnez chaque webhook aux événements voulus. Le catalogue est une liste figée.

  • work_order.created — un bon de travail a été créé
  • work_order.completed — un bon de travail est passé à Terminé
  • work_order.status_changed — un ordre de travail a changé de statut, quel qu'il soit ; la charge utile porte new_status. Se déclenche en plus de work_order.completed, qui reste inchangé
  • request.created — une demande d'intervention a été soumise
  • request.status_changed — une demande a été approuvée, refusée ou annulée
  • part.low_stock — une pièce est descendue à son minimum ou en dessous
  • part.out_of_stock — une pièce est tombée à zéro ; c'est un événement distinct, pas une variante de low_stock
  • part.low_stock_summary — un récapitulatif quotidien par entreprise listant les pièces à leur point de commande ou en dessous, y compris celles qui n'ont jamais bougé
  • part.out_of_stock_summary — le même récapitulatif quotidien, pour les pièces à zéro
  • part.consumed — une pièce a été consommée sur un bon de travail, ou rendue ; quantity est signé
  • compliance.chain_divergence — la chaîne de hachage d'audit est rompue (Enterprise)
  • asset.downtime_started — un arrêt machine a été ouvert
  • asset.downtime_ended — un arrêt machine a été clos ; la durée peut être imputée
  • report.daily_summary — les travaux terminés de la journée, une fois par jour à l'heure que vous choisissez
  • purchase_order.created — un bon de commande a été créé ; la charge utile porte la commande (reference, status, total_cost, currency, expected_date, erp_reference) et son fournisseur
  • purchase_order.status_changed — un bon de commande a changé de statut (approuvé, commandé, annulé…) ; la charge utile porte previous_status et new_status
  • purchase_order.received — une livraison a été réceptionnée sur un bon de commande (envoyé à chaque livraison, y compris partielle) ; la charge utile liste chaque ligne avec quantity_ordered, quantity_received (cette livraison), quantity_received_total et quantity_remaining, ainsi que receipt_id, fully_received et closed_short ; new_status vaut PARTIALLY_RECEIVED tant que des marchandises sont attendues, RECEIVED une fois la commande complète ou clôturée

Avertissement

Il n'existe aucun événement d'inspection, ni d'anomalie ou de constat. Si votre processus est « une inspection échoue, mon ERP réagit », rien ne se déclenche aujourd'hui. Prévoyez ce manque explicitement plutôt que de supposer qu'un événement approchant fera l'affaire.

L'enveloppe de livraison

Chaque livraison GENERIC est un POST dont le corps suit la même enveloppe à trois clés : event, timestamp et data, où data est indexé par entité. Trois en-têtes l'accompagnent. X-FreeMaint-Signature prend la forme t=<secondes unix>,v1=<hex>, où v1 est le HMAC-SHA256 de la chaîne exacte "<t>.<corps brut>" signée avec le secret de ce webhook ; X-FreeMaint-Event répète le nom de l'événement ; X-FreeMaint-Delivery-Id identifie la livraison. Vérifiez la signature sur le corps BRUT, avant tout parsing : re-sérialiser le JSON change les octets et la signature ne correspondra plus.

{ "event": "work_order.created", "timestamp": "2026-09-05T08:14:02.371Z", "data": { "work_order": { "id": 5521, "title": "Fuite pompe n° 3", "status": "OPEN", "priority": "HIGH", "dueDate": "2026-09-08T00:00:00.000Z" } } }

Fiabilité et supervision

Une livraison est tentée 3 fois, avec un repli exponentiel sur une base de 2 secondes — environ 2 s, puis 4 s, puis 8 s — et chaque tentative expire au bout de 10 secondes. Un 2xx la marque comme livrée. Les réponses 5xx et les erreurs réseau sont réessayées. Un 4xx est traité comme un refus définitif et n'est PAS réessayé : si votre point d'entrée répond 400 ou 401 à cause d'un bug de votre côté, la livraison s'arrête là et seule une relecture manuelle la récupérera.

  • Chaque livraison est conservée : l'événement, la charge utile exacte envoyée, le statut, le code HTTP que vous avez renvoyé, les ~500 premiers caractères de votre corps de réponse, le nombre de tentatives et le message d'erreur.
  • Les statuts sont PENDING, DELIVERED, FAILED, RATE_LIMITED (le plafond Starter de 100 livraisons par jour a été atteint) et SKIPPED_TIER (l'abonnement ne donne plus droit à cette destination).
  • Une livraison enregistrée peut être rejouée depuis l'historique du webhook. Elle renvoie la charge utile telle quelle : les clés métier qu'elle contient restent identiques et un récepteur qui déduplique correctement ne comptabilisera pas deux fois.
  • Chaque webhook porte lastTriggeredAt, lastSuccessAt, lastFailureAt et un failureCount, ce qui suffit à un contrôle de santé sans ouvrir l'historique complet.
  • La liste des webhooks et son journal de livraison sont consultables sur tous les abonnements : une intégration arrêtée par un déclassement reste visible au lieu de mourir en silence.
  • Rien ne vous alerte quand les livraisons commencent à échouer — ni e-mail, ni notification. Si une panne silencieuse serait problématique, interrogez le journal de livraison depuis votre propre supervision.

Sécurité

Les URL sont validées à l'enregistrement du webhook, et les règles sont volontairement strictes : un webhook est une requête sortante émise par nos serveurs, donc une règle permissive constituerait un vecteur SSRF.

  • Les clés sont stockées sous forme de hachages SHA-256 — la valeur en clair ne peut pas être récupérée depuis FreeMaint après la création.
  • Les URL de webhook doivent utiliser https. Le http simple et tout autre schéma sont refusés.
  • Seuls les ports 443 et 8443 sont acceptés.
  • Les hôtes en boucle locale, en lien-local ou en plage privée (127.x, 10.x, 192.168.x, 172.16–31.x, ::1 et assimilés) sont refusés : le récepteur doit être joignable depuis l'Internet public. Un système qui n'existe qu'à l'intérieur de votre usine nécessite un relais public ou un tunnel.
  • Les identifiants dans l'URL (https://user:pass@host) sont refusés.
  • Les redirections ne sont pas suivies — le point d'entrée que vous enregistrez est celui qui reçoit le POST.
  • Chaque webhook possède son propre secret de signature et une action de rotation existe. La rotation prend effet immédiatement : déployez le nouveau secret de votre côté dans la même fenêtre.
  • Les destinations SLACK et TEAMS reçoivent un message formaté pour la plateforme et AUCUN en-tête HMAC FreeMaint : pour elles, le secret de l'URL est la seule authentification. Utilisez GENERIC dès que vous devez vérifier cryptographiquement.

Les schémas qui fonctionnent bien aujourd'hui

Voici les intégrations que la surface actuelle prend en charge proprement, et le détail qui rend chacune sûre en production.

Répercuter la consommation dans un ERP

Abonnez-vous à part.consumed. quantity est signé — négatif lors de la sortie vers le chantier, positif au retour — et la charge utile porte un stock_movement_id stable à travers les réessais et les rejeux : utilisez-le comme clé d'idempotence. La mutation adjustStock n'émet délibérément PAS cet événement, ce qui empêche vos propres écritures de vous revenir en écho et de boucler.

Pousser les corrections de stock

Appelez adjustStock avec un delta signé depuis votre côté. Une clé READ_STOCK peut le faire sans pouvoir créer, modifier ni supprimer quoi que ce soit, ce qui en fait un choix serré pour une passerelle de stock à sens unique.

Créer des bons de travail depuis un autre système

createWorkOrder à partir d'une alarme SCADA, d'un ticket de support ou d'un avis de panne ERP. Le bon de travail reçoit sa référence normale et déclenche work_order.created comme n'importe quel autre.

Garder un catalogue de pièces synchronisé

parts(updatedSince:) ne renvoie que ce qui a changé depuis l'instant choisi. createPart et updatePart acceptent customReference — votre propre code pour la pièce — et part(barcode:) fait ensuite correspondre dessus : vous pouvez donc retrouver une pièce par l'identifiant que votre ERP utilise déjà. L'unicité n'est pas imposée ; c'est à vous de la garantir.

Boucler la boucle à la clôture

work_order.completed transporte le bon de travail et son statut précédent, ce qui suffit à clôturer un avis de maintenance de l'autre côté.

Les prérequis d'abonnement

Deux verrous distincts : un pour l'API, un pour les webhooks. Une entreprise en Starter peut recevoir des webhooks sans avoir l'API.

API GraphQL et clés d'API

Business et au-delà. Core et Starter ne peuvent pas créer de clé du tout.

Quota quotidien

5 000 appels par jour en Business, 100 000 en Enterprise. Comptés par entreprise et par opération, remis à zéro à 00:00 UTC — regrouper des opérations dans une seule requête HTTP économise des allers-retours, jamais du quota.

Webhooks

Starter et au-delà, plafonnés à 100 livraisons par jour ; illimités à partir de Business.

Destinations Slack et Teams

Enterprise. Un webhook générique est disponible dès Starter.

Astuce

Vous préparez quelque chose qui bute sur l'une des limites ci-dessus ? Dites-nous ce que vous construisez à contact@freemaint.com. Ce qui sort ensuite est décidé par ce qui bloque réellement les clients.

Cette page vous a-t-elle été utile ?