Guida all'integrazione con ERP e altri sistemi
Delimitare un'integrazione prima di costruirla: cosa raggiunge l'API, cosa non raggiunge e dove sono i confini
Questa guida è per lo sviluppatore che sta delimitando un'integrazione tra FreeMaint e un altro sistema: un ERP, uno storico SCADA, un gestionale di magazzino o di contabilità. Gli altri articoli di questa sezione spiegano come chiamare l'API; questo parla deliberatamente di confini, perché l'errore costoso non è mai un header sbagliato: è scoprire a metà dello sviluppo che una capacità che davi per scontata non esiste. Ogni limite indicato qui sotto è dichiarato di proposito.
Una chiave, tre porte
La chiave API (inizia con fmk_) viene creata in Impostazioni Azienda > API (/company-settings/api) da un amministratore, oppure da una persona per sé stessa in Profilo > Sicurezza > Chiavi API, e si invia come Authorization: Bearer fmk_... . Autentica esattamente tre cose: l'endpoint GraphQL su POST /api/v1/graphql, l'endpoint di ingestione energia, e /public-api/whoami e /public-api/ping. NON autentica le rotte REST sotto /api/v1/{resource}: quelle sono l'API interna dell'applicazione e richiedono una sessione utente (JWT). Costruisci la tua integrazione su GraphQL: è quella la superficie che una chiave raggiunge.
Cosa ti dà una chiave API
La chiave in chiaro viene mostrata una sola volta, alla creazione, e dopo non è più recuperabile: viene conservato solo un hash SHA-256, insieme ai 6 caratteri che seguono fmk_ perché l'elenco possa identificare la chiave a schermo.
Nome
Un'etichetta leggibile: «Ponte ERP Oracle», «Chiosco linea 2». Emetti una chiave per ogni sistema che consuma l'API, così puoi revocarne una senza disturbare le altre.
Livello di accesso
READ, READ_STOCK o FULL, una gerarchia ordinata che si aggiunge ai permessi del proprietario. READ esegue soltanto query ed è il valore predefinito. READ_STOCK aggiunge la mutation adjustStock. FULL consente ogni operazione consentita dal ruolo del proprietario, comprese creazione, modifica ed eliminazione — e l'endpoint di ingestione energia quando il proprietario può creare letture di energia.
Scadenza
Un expiresAt facoltativo. Superato quel momento la chiave smette di autenticare. Lascialo vuoto per un'integrazione di servizio permanente; valorizzalo per un fornitore esterno o un pilota.
Revoca
isActive è un interruttore per singola chiave che ha effetto nel giro di pochi secondi. La riga resta per l'audit, così una chiave revocata rimane tracciabile invece di sparire.
Ultimo utilizzo
lastUsedAt registra l'ultima chiamata autenticata: quanto basta per individuare una chiave che non usa più nessuno.
Proprietario
La persona per conto della quale agisce la chiave. Ogni chiamata porta i suoi permessi e la sua visibilità sui dati, e il suo nome firma ciò che la chiave scrive. Per un ERP, crea un utente dedicato — «Integrazione ERP» — con un ruolo che copra esattamente ciò di cui il ponte ha bisogno, e affidagli la chiave.
Cosa non ti dà una chiave API
Sono limiti reali dell'implementazione attuale, non dimenticanze di questa pagina. Se la tua revisione di sicurezza dipende da uno di essi, progetta di conseguenza adesso e non a costruzione finita.
- Nessun elenco di moduli per chiave. Una chiave legge e scrive ciò che il ruolo del suo proprietario consente — per restringerla, affidala a un proprietario con un ruolo più ristretto. Oltre a questo si applicano solo i tre livelli di accesso: non puoi elencare, sulla chiave stessa, i moduli che può usare.
- Nessuna lista di indirizzi IP autorizzati. Una chiave è valida da qualunque punto di Internet. Trattala come un segreto: un vault o il gestore di segreti della tua piattaforma, mai un repository, un disco condiviso o un ticket di assistenza.
- Nessun registro d'uso per chiave. Viene conservato solo lastUsedAt. Non esiste uno storico delle chiamate per chiave, né una quota per chiave, né traccia di quale chiave abbia eseguito quale operazione. Il contatore della quota giornaliera è per AZIENDA ed è condiviso da tutte le chiavi, quindi un'integrazione impazzita può esaurire la quota di tutte le altre.
- Nessuna approvazione per conto di qualcuno. Una chiave non approva mai un passaggio del flusso di lavoro, anche quando il suo proprietario potrebbe farlo: le approvazioni restano un atto umano nell'app.
- Un'azienda per chiave. Una chiave è legata all'azienda in cui è stata creata e non può raggiungere i dati di un'altra azienda: un record di un'altra azienda semplicemente non viene trovato.
Cosa puoi leggere
GraphQL espone 16 query di lettura: whoami, workOrders, workOrder(id), assets, asset(id), assetDowntimes, meters(assetId), parts, part (per id o codice a barre), stockMovements, locations, vendors, customers, purchaseOrders, purchaseOrder(id | reference | erpReference) e floorPlans(locationId). Ogni lista può essere scorsa fino in fondo: workOrders, assets, assetDowntimes, meters, parts, purchaseOrders, locations, vendors e customers accettano tutte limit (da 1 a 200) più offset, e workOrders, assets, parts, purchaseOrders, locations, vendors e customers accettano anche updatedSince per le sincronizzazioni incrementali. La pagina predefinita è di 50 righe, tranne per locations, vendors e customers, che mantengono il precedente valore predefinito di 200. Ogni lista è ordinata con un criterio di spareggio sull'id, così il confine tra due pagine non salta né ripete mai una riga. stockMovements è il registro di magazzino su cui basare la riconciliazione: si legge dal più vecchio e si pagina con afterId — passa l'id dell'ultimo movimento ricevuto —, il che resta esatto anche mentre vengono scritti nuovi movimenti, e since consente di avviare un recupero dello storico a partire da una data. Gli ordini di lavoro nel cestino non vengono mai restituiti. purchaseOrders si filtra anche per status e vendorId; gli importi sono nella valuta dell'azienda, che ogni ordine riporta nel campo currency.
Verificare la chiave e leggere gli ordini di lavoro in un'unica chiamata: 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 } }"}'
Cosa puoi scrivere
Dodici mutation: creazione, modifica ed eliminazione su ordini di lavoro, asset e ricambi, più adjustStock per un movimento di magazzino con segno. Passano dagli stessi servizi che usa l'applicazione stessa, quindi i riferimenti (WO-…, ASS-…, PART-…), le righe di audit e le notifiche si comportano esattamente come nell'interfaccia. Per il magazzino preferisci adjustStock: scrive una riga di audit StockMovement nella stessa transazione e si rifiuta di portare una quantity sotto zero, mentre updatePart(quantity) imposta un valore assoluto e registra la differenza come rettifica. Su un ricambio gestito per ubicazione, passa locationId ad adjustStock; lì updatePart(quantity) viene rifiutato. L'undicesima, addMeterReading, registra una lettura di contatore tramite lo stesso servizio della schermata contatori, quindi trigger e manutenzione preventiva a contatore reagiscono esattamente come per una lettura inserita nell'app. La dodicesima, setPurchaseOrderErpReference, salva il numero di documento del tuo ERP su un ordine di acquisto, così i due record si possono abbinare in seguito con purchaseOrder(erpReference: …). Richiede una chiave ad accesso FULL e di proposito non genera alcun webhook: il tuo ERP non riceve mai la propria scrittura come evento.
Cosa l'API non copre affatto
Lo schema GraphQL contiene ordini di lavoro, asset, fermi macchina, ricambi e relativi movimenti di magazzino, contatori, ordini di acquisto, ubicazioni, fornitori, clienti e planimetrie. Nient'altro. Quelli che seguono sono componenti pienamente funzionanti di FreeMaint che oggi NON hanno alcuna superficie API: non possono essere letti, scritti né sottoscritti da un'integrazione, e nessuna combinazione delle query esistenti li raggiunge.
- Le ispezioni: né i modelli, né le esecuzioni pianificate, né i risultati registrati, né i codici azione. Non esiste alcuna query di ispezione, alcuna mutation di ispezione né alcun evento webhook di ispezione.
- Utenti, ruoli e squadre: non puoi elencare, creare o disattivare un utente tramite questa API. Il provisioning degli utenti avviene nell'applicazione; con il piano Enterprise può essere automatizzato con SCIM, che è un meccanismo separato e non fa parte di questa API.
- Progetti: nessuna query e nessuna mutation.
- Richieste di acquisto: nessuna query e nessuna mutation. Gli ordini di acquisto sono leggibili (purchaseOrders, purchaseOrder), il tuo ERP può riscriverci il proprio riferimento (setPurchaseOrderErpReference) e i webhook purchase_order.* li annunciano, ma un ordine non si può creare, modificare né ricevere tramite l'API: questo resta nell'app.
- File, foto e allegati: non puoi caricare un documento o una foto, e non puoi rileggerli. Le foto degli ordini di lavoro, le immagini dei ricambi e le evidenze delle ispezioni stanno tutte fuori dall'API. Un URL che compare in una risposta, come l'imageUrl di un ricambio, non è un percorso di caricamento.
- I piani di manutenzione preventiva, i rapportini di tempo, i task e le checklist: non sono esposti.
Importante
Webhook — il catalogo degli eventi
I webhook sono la metà «push» di un'integrazione: invece di interrogare l'API, è FreeMaint a fare una POST verso il tuo URL nel momento in cui accade qualcosa. Li configuri in Impostazioni azienda > Webhook (/company-settings/webhooks) e iscrivi ogni webhook agli eventi che ti servono. Il catalogo è un elenco fisso.
- work_order.created — è stato creato un ordine di lavoro
- work_order.completed — un ordine di lavoro è passato a Completato
- work_order.status_changed — un ordine di lavoro è passato a un qualsiasi stato; il payload porta new_status. Si attiva oltre a work_order.completed, che resta invariato
- request.created — è stata inviata una richiesta di manutenzione
- request.status_changed — una richiesta è stata approvata, respinta o annullata
- part.low_stock — un ricambio è sceso al suo minimo o sotto
- part.out_of_stock — un ricambio è arrivato a zero; è un evento a sé, non una variante di low_stock
- part.low_stock_summary — un riepilogo giornaliero per azienda con i ricambi al punto di riordino o sotto, compresi quelli mai movimentati
- part.out_of_stock_summary — lo stesso riepilogo giornaliero, per i ricambi a zero
- part.consumed — un ricambio è stato consumato su un ordine di lavoro, o da esso reso; quantity ha segno
- compliance.chain_divergence — la catena di hash di audit si è interrotta (Enterprise)
- asset.downtime_started — è stato aperto un fermo macchina
- asset.downtime_ended — è stato chiuso un fermo macchina; la durata è ora imputabile
- report.daily_summary — i lavori completati della giornata, una volta al giorno all'ora che scegli
- purchase_order.created — è stato creato un ordine di acquisto; il payload contiene l'ordine (reference, status, total_cost, currency, expected_date, erp_reference) e il fornitore
- purchase_order.status_changed — un ordine di acquisto ha cambiato stato (approvato, ordinato, annullato…); il payload contiene previous_status e new_status
- purchase_order.received — è stata ricevuta una consegna su un ordine d'acquisto (inviato a ogni consegna, parziali comprese); il payload elenca ogni riga con quantity_ordered, quantity_received (questa consegna), quantity_received_total e quantity_remaining, oltre a receipt_id, fully_received e closed_short; new_status è PARTIALLY_RECEIVED finché si attende merce e RECEIVED quando l'ordine è completo o chiuso
Attenzione
La busta di consegna
Ogni consegna GENERIC è una POST il cui corpo è sempre la stessa busta a tre chiavi: event, timestamp e data, dove data è indicizzato per entità. La accompagnano tre header. X-FreeMaint-Signature ha la forma t=<secondi unix>,v1=<hex>, dove v1 è l'HMAC-SHA256 della stringa esatta «<t>.<corpo grezzo>» firmata con il segreto di quel webhook; X-FreeMaint-Event ripete il nome dell'evento; X-FreeMaint-Delivery-Id identifica la consegna. Verifica la firma sul corpo GREZZO, prima del parsing: ri-serializzare il JSON cambia i byte e la firma non corrisponderà più.
{ "event": "work_order.created", "timestamp": "2026-09-05T08:14:02.371Z", "data": { "work_order": { "id": 5521, "title": "Perdita pompa n. 3", "status": "OPEN", "priority": "HIGH", "dueDate": "2026-09-08T00:00:00.000Z" } } }
Affidabilità e monitoraggio
Una consegna viene tentata 3 volte con backoff esponenziale su base 2 secondi — circa 2 s, poi 4 s, poi 8 s — e ogni tentativo scade dopo 10 secondi. Un 2xx la segna come consegnata. Le risposte 5xx e gli errori di rete vengono ritentati. Un 4xx è considerato un rifiuto definitivo e NON viene ritentato affatto: se il tuo endpoint risponde 400 o 401 per un bug dalla tua parte, quella consegna si ferma lì e solo un rinvio manuale la recupera.
- Ogni consegna viene memorizzata: l'evento, il payload esatto inviato, lo stato, il codice HTTP che hai restituito, i primi ~500 caratteri del corpo della tua risposta, il numero di tentativi e il messaggio di errore.
- Gli stati sono PENDING, DELIVERED, FAILED, RATE_LIMITED (è stato raggiunto il tetto Starter di 100 consegne al giorno) e SKIPPED_TIER (il piano non dà più diritto a quella destinazione).
- Una consegna memorizzata può essere rinviata dallo storico del webhook. Rispedisce il payload salvato senza modifiche, quindi le chiavi di business che contiene restano identiche e un ricevente che deduplica correttamente non registrerà due volte.
- Ogni webhook porta lastTriggeredAt, lastSuccessAt, lastFailureAt e un failureCount: abbastanza per un controllo di salute senza aprire lo storico completo.
- L'elenco dei webhook e il suo registro delle consegne sono consultabili su tutti i piani, così un'integrazione fermata da un downgrade resta visibile invece di morire in silenzio.
- Nulla ti avvisa quando le consegne iniziano a fallire: né e-mail, né notifica. Se un'interruzione silenziosa fosse un problema, interroga il registro delle consegne dal tuo monitoraggio.
Sicurezza
Gli URL vengono validati al salvataggio del webhook, e le regole sono volutamente severe: un webhook è una richiesta in uscita fatta dai nostri server, quindi una regola permissiva sarebbe un vettore SSRF.
- Le chiavi sono conservate come hash SHA-256: il valore in chiaro non è recuperabile da FreeMaint dopo la creazione.
- Gli URL dei webhook devono usare https. Il semplice http e ogni altro schema vengono rifiutati.
- Sono accettate solo le porte 443 e 8443.
- Gli host di loopback, link-local e degli intervalli privati (127.x, 10.x, 192.168.x, 172.16–31.x, ::1 e simili) vengono rifiutati: il ricevente deve essere raggiungibile da Internet pubblica. Un sistema che esiste solo dentro il tuo stabilimento ha bisogno di un relay pubblico o di un tunnel.
- Le credenziali nell'URL (https://user:pass@host) vengono rifiutate.
- I redirect non vengono seguiti: l'endpoint che registri è l'endpoint che riceve la POST.
- Ogni webhook ha il proprio segreto di firma ed esiste un'azione di rotazione del segreto. La rotazione ha effetto immediato, quindi distribuisci il nuovo segreto dalla tua parte nella stessa finestra.
- Le destinazioni SLACK e TEAMS ricevono un messaggio formattato per la piattaforma e NESSUN header HMAC di FreeMaint: per loro la segretezza dell'URL è l'unica autenticazione. Usa GENERIC per tutto ciò che devi verificare crittograficamente.
Schemi che funzionano bene oggi
Sono le integrazioni che la superficie attuale supporta in modo pulito, con il dettaglio che rende ciascuna sicura in produzione.
Rispecchiare i consumi in un ERP
Iscriviti a part.consumed. quantity ha segno — negativo quando il ricambio esce verso il lavoro, positivo quando rientra — e il payload porta uno stock_movement_id che resta stabile tra ritentativi e rinvii: usalo come chiave di idempotenza. La mutation adjustStock deliberatamente NON genera questo evento, ed è ciò che impedisce alle tue stesse scritture di tornarti indietro in eco, creando un ciclo.
Inviare correzioni di giacenza
Chiamate adjustStock con un delta con segno dal vostro lato. Una chiave READ_STOCK può farlo senza poter creare, modificare o eliminare alcunché, il che la rende una scelta calzante per un ponte di magazzino a senso unico.
Creare ordini di lavoro da un altro sistema
createWorkOrder a partire da un allarme SCADA, da un ticket di helpdesk o da una segnalazione di guasto dell'ERP. L'ordine di lavoro riceve il suo riferimento normale e genera work_order.created come qualsiasi altro.
Tenere allineato un catalogo ricambi
parts(updatedSince:) restituisce solo ciò che è cambiato dal momento che scegli. createPart e updatePart accettano customReference — il tuo codice per il ricambio — e part(barcode:) fa poi corrispondenza su di esso, così puoi ritrovare un ricambio con l'identificativo che il tuo ERP già usa. L'unicità non è imposta: garantirla spetta a te.
Chiudere il cerchio al completamento
work_order.completed porta con sé l'ordine di lavoro e il suo stato precedente, il che basta a chiudere una segnalazione di manutenzione dall'altra parte.
Cosa devi avere
Due condizioni indipendenti: una per l'API, una per i webhook. Un'azienda su Starter può ricevere i webhook senza avere l'API.
API GraphQL e chiavi API
Business e superiori. Core e Starter non possono creare alcuna chiave.
Quota giornaliera
5.000 chiamate al giorno su Business, 100.000 su Enterprise. Contate per azienda e per operazione, con azzeramento alle 00:00 UTC — raggruppare più operazioni in un'unica richiesta HTTP fa risparmiare round-trip, mai quota.
Webhook
Starter e superiori, con un tetto di 100 consegne al giorno; illimitate da Business.
Destinazioni Slack e Teams
Enterprise. Un webhook generico è disponibile già da Starter.
Suggerimento
Articoli correlati
Questa pagina è stata utile?