Leitfaden zur ERP- und Systemintegration
Eine Integration abstecken, bevor Sie sie bauen: was die API erreicht, was nicht, und wo die Grenzen liegen
Dieser Leitfaden richtet sich an Entwicklerinnen und Entwickler, die eine Integration zwischen FreeMaint und einem anderen System abstecken — einem ERP, einem SCADA-Historian, einer Lagerverwaltung oder einer Buchhaltungssoftware. Die übrigen Artikel dieses Abschnitts erklären, wie man Aufrufe absetzt. Dieser hier handelt bewusst von Grenzen, denn der teure Fehler ist nie ein falscher Header: teuer wird es, wenn Sie mitten im Bau feststellen, dass eine vorausgesetzte Fähigkeit gar nicht existiert. Jede Grenze weiter unten steht dort mit Absicht.
Ein Schlüssel, drei Türen
Ein API-Schlüssel (er beginnt mit fmk_) wird unter Firmeneinstellungen > API (/company-settings/api) von einem Administrator erstellt oder von einer Person für sich selbst unter Profil > Sicherheit > API-Schlüssel und als Authorization: Bearer fmk_... gesendet. Er authentifiziert genau drei Dinge: den GraphQL-Endpunkt unter POST /api/v1/graphql, den Endpunkt für den Energie-Ingest sowie /public-api/whoami und /public-api/ping. Er authentifiziert NICHT die REST-Routen unter /api/v1/{resource} — das ist die anwendungseigene API, die eine Benutzersitzung (JWT) verlangt. Bauen Sie Ihre Integration auf GraphQL: Das ist die Oberfläche, die ein Schlüssel erreicht.
Was ein API-Schlüssel Ihnen gibt
Der Schlüssel im Klartext wird genau einmal angezeigt, bei der Erstellung, und ist danach nie wieder abrufbar: Gespeichert wird nur ein SHA-256-Hash, zusammen mit den 6 Zeichen, die auf fmk_ folgen, damit die Liste den Schlüssel auf dem Bildschirm identifizieren kann.
Name
Eine für Menschen lesbare Bezeichnung — „Oracle-ERP-Brücke“, „Kiosk Linie 2“. Vergeben Sie einen Schlüssel pro nutzendem System, damit Sie einen widerrufen können, ohne die anderen zu stören.
Zugriffsebene
READ, READ_STOCK oder FULL, eine geordnete Hierarchie zusätzlich zu den Berechtigungen des Eigentümers. READ führt nur Abfragen aus und ist der Standard. READ_STOCK ergänzt die Mutation adjustStock. FULL erlaubt jede Operation, die die Rolle des Eigentümers erlaubt, einschließlich Anlegen, Ändern und Löschen — und den Endpunkt für die Energie-Erfassung, wenn der Eigentümer Energiemesswerte anlegen darf.
Ablauf
Ein optionales expiresAt. Nach diesem Zeitpunkt authentifiziert der Schlüssel nicht mehr. Lassen Sie es für eine dauerhafte Dienstintegration leer; setzen Sie es für einen Dienstleister oder einen Piloten.
Widerruf
isActive ist ein Schalter pro Schlüssel, der binnen Sekunden greift. Der Datensatz bleibt zur Nachvollziehbarkeit erhalten, ein widerrufener Schlüssel bleibt also nachverfolgbar, statt zu verschwinden.
Zuletzt verwendet
lastUsedAt hält den jüngsten authentifizierten Aufruf fest — genug, um einen Schlüssel zu erkennen, den niemand mehr benutzt.
Eigentümer
Die Person, als die der Schlüssel handelt. Jeder Aufruf trägt deren Berechtigungen und deren Datensichtbarkeit, und ihr Name zeichnet, was der Schlüssel schreibt. Legen Sie für ein ERP einen eigenen Benutzer an — „ERP-Integration“ — mit einer Rolle, die genau abdeckt, was die Brücke braucht, und geben Sie ihm den Schlüssel.
Was ein API-Schlüssel Ihnen nicht gibt
Das sind echte Grenzen der aktuellen Umsetzung, keine Auslassungen dieser Seite. Wenn Ihre Sicherheitsprüfung von einer davon abhängt, planen Sie jetzt darum herum und nicht erst nach dem Bau.
- Keine Modulliste pro Schlüssel. Ein Schlüssel liest und schreibt, was die Rolle seines Eigentümers erlaubt — um ihn einzuschränken, geben Sie ihn einem Eigentümer mit einer engeren Rolle. Darüber hinaus gelten nur die drei Zugriffsstufen: Sie können nicht am Schlüssel selbst die Module festlegen, die er nutzen darf.
- Keine IP-Freigabeliste. Ein Schlüssel ist von überall im Internet gültig. Behandeln Sie ihn wie ein Geheimnis: ein Tresor oder der Secret-Store Ihrer Plattform, niemals ein Repository, ein geteiltes Laufwerk oder ein Support-Ticket.
- Kein Nutzungsprotokoll pro Schlüssel. Festgehalten wird nur lastUsedAt. Es gibt keine Aufrufhistorie pro Schlüssel, kein Kontingent pro Schlüssel und keinen Nachweis darüber, welcher Schlüssel welche Operation ausgeführt hat. Der Zähler für das Tageskontingent gilt pro UNTERNEHMEN und wird von allen Schlüsseln geteilt — eine außer Kontrolle geratene Integration kann also das Kontingent aller anderen aufbrauchen.
- Keine Genehmigung im Namen anderer. Ein Schlüssel genehmigt niemals einen Workflow-Schritt, selbst wenn sein Eigentümer es könnte: Genehmigungen bleiben ein menschlicher Akt in der App.
- Ein Unternehmen pro Schlüssel. Ein Schlüssel ist an das Unternehmen gebunden, in dem er erstellt wurde, und erreicht keine Daten eines anderen Unternehmens: Ein Datensatz eines anderen Unternehmens wird schlicht nicht gefunden.
Was Sie lesen können
GraphQL stellt 16 Lese-Abfragen bereit: whoami, workOrders, workOrder(id), assets, asset(id), assetDowntimes, meters(assetId), parts, part (per id oder Barcode), stockMovements, locations, vendors, customers, purchaseOrders, purchaseOrder(id | reference | erpReference) und floorPlans(locationId). Jede Liste lässt sich bis zum Ende durchblättern: workOrders, assets, assetDowntimes, meters, parts, purchaseOrders, locations, vendors und customers nehmen alle limit (1 bis 200) plus offset, und workOrders, assets, parts, purchaseOrders, locations, vendors und customers nehmen zusätzlich updatedSince für inkrementelle Abgleiche. Eine Seite umfasst standardmäßig 50 Zeilen, außer bei locations, vendors und customers, die ihren bisherigen Standard von 200 behalten. Jede Liste ist mit id als zusätzlichem Sortierkriterium geordnet, sodass eine Seitengrenze nie eine Zeile überspringt oder doppelt liefert. stockMovements ist das Bestandsjournal, gegen das Sie abgleichen: Es wird älteste zuerst gelesen und mit afterId geblättert — übergeben Sie die id der zuletzt erhaltenen Bewegung —, was exakt bleibt, auch während neue Bewegungen geschrieben werden, und mit since starten Sie das Nachladen der Historie ab einem Datum. Arbeitsaufträge im Papierkorb werden nie zurückgegeben. purchaseOrders filtert zudem nach status und vendorId; Beträge stehen in der Währung des Unternehmens, die jede Bestellung im Feld currency mitliefert.
Den Schlüssel prüfen und Arbeitsaufträge in einem einzigen Aufruf lesen: 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 } }"}'
Was Sie schreiben können
Zwölf Mutationen: Anlegen, Ändern und Löschen bei Arbeitsaufträgen, Anlagen und Ersatzteilen, dazu adjustStock für eine vorzeichenbehaftete Bestandsbewegung. Sie laufen über dieselben Dienste, die die Anwendung selbst nutzt, Referenzen (WO-…, ASS-…, PART-…), Prüfeinträge und Benachrichtigungen verhalten sich also genau wie in der Oberfläche. Für Bestände sollten Sie adjustStock bevorzugen: Es schreibt in derselben Transaktion einen StockMovement-Prüfeintrag und weigert sich, eine Menge unter null zu ziehen, während updatePart(quantity) einen absoluten Wert setzt und die Differenz als Bestandskorrektur erfasst. Bei einem Teil, dessen Bestand pro Lagerort geführt wird, übergeben Sie locationId an adjustStock; updatePart(quantity) wird dort abgelehnt. Die elfte, addMeterReading, erfasst einen Zählerstand über denselben Dienst wie die Zähleransicht, sodass Zählertrigger und zählerbasierte vorbeugende Wartung genau so reagieren wie bei einem in der App eingegebenen Stand. Die zwölfte, setPurchaseOrderErpReference, speichert die Belegnummer Ihres ERP an einer Bestellung, damit sich beide Datensätze später mit purchaseOrder(erpReference: …) zuordnen lassen. Sie verlangt einen Schlüssel mit FULL-Zugriff und löst bewusst keinen Webhook aus: Ihr ERP erhält seinen eigenen Schreibvorgang nie als Ereignis zurück.
Was die API überhaupt nicht abdeckt
Das GraphQL-Schema enthält Arbeitsaufträge, Anlagen, Maschinenstillstände, Ersatzteile und ihre Bestandsbewegungen, Zähler, Bestellungen, Standorte, Lieferanten, Kunden und Gebäudepläne. Sonst nichts. Die folgenden Bereiche sind voll funktionsfähige Teile von FreeMaint, die heute KEINE API-Oberfläche haben: Sie lassen sich aus einer Integration weder lesen noch schreiben noch abonnieren, und keine Kombination der vorhandenen Abfragen erreicht sie.
- Inspektionen — weder die Vorlagen noch die geplanten Durchgänge, die erfassten Ergebnisse oder die Aktionscodes. Es gibt keine Inspektions-Abfrage, keine Inspektions-Mutation und kein Inspektions-Webhook-Ereignis.
- Benutzer, Rollen und Teams — Sie können über diese API keinen Benutzer auflisten, anlegen oder deaktivieren. Die Benutzerbereitstellung erfolgt in der Anwendung; im Enterprise-Tarif lässt sie sich mit SCIM automatisieren, was ein eigener Mechanismus und nicht Teil dieser API ist.
- Projekte — keine Abfrage und keine Mutation.
- Bestellanforderungen — keine Abfrage und keine Mutation. Bestellungen sind lesbar (purchaseOrders, purchaseOrder), Ihr ERP kann seine Referenz zurückschreiben (setPurchaseOrderErpReference) und die Webhooks purchase_order.* melden sie, aber anlegen, ändern oder vereinnahmen lässt sich eine Bestellung über die API nicht: das bleibt in der App.
- Dateien, Fotos und Anhänge — Sie können weder ein Dokument oder ein Foto hochladen noch eines zurücklesen. Fotos zu Arbeitsaufträgen, Ersatzteilbilder und Inspektionsnachweise liegen sämtlich außerhalb der API. Eine URL, die in einer Antwort auftaucht, etwa die imageUrl eines Ersatzteils, ist kein Upload-Pfad.
- Präventive Wartungspläne, Zeiterfassungen, Aufgaben und Checklisten — nicht offengelegt.
Wichtig
Webhooks — der Ereigniskatalog
Webhooks sind die Push-Hälfte einer Integration: Statt zu pollen, sendet FreeMaint in dem Moment, in dem etwas passiert, ein POST an Ihre URL. Sie richten sie unter Unternehmenseinstellungen > Webhooks (/company-settings/webhooks) ein und abonnieren jeden Webhook auf die Ereignisse, die Sie brauchen. Der Katalog ist eine feste Liste.
- work_order.created — ein Arbeitsauftrag wurde erstellt
- work_order.completed — ein Arbeitsauftrag hat den Status Abgeschlossen erreicht
- work_order.status_changed — ein Arbeitsauftrag hat in einen beliebigen Status gewechselt; die Nutzlast enthält new_status. Wird zusätzlich zu work_order.completed ausgelöst, das unverändert bleibt
- request.created — eine Wartungsanfrage wurde eingereicht
- request.status_changed — eine Anfrage wurde genehmigt, abgelehnt oder storniert
- part.low_stock — ein Ersatzteil ist auf seinen Mindestbestand oder darunter gefallen
- part.out_of_stock — ein Ersatzteil hat null erreicht; ein eigenes Ereignis, keine Variante von low_stock
- part.low_stock_summary — eine tägliche Zusammenfassung je Unternehmen mit allen Ersatzteilen auf oder unter ihrem Meldebestand, einschließlich der nie bewegten
- part.out_of_stock_summary — dieselbe tägliche Zusammenfassung für Ersatzteile mit Bestand null
- part.consumed — ein Ersatzteil wurde auf einem Arbeitsauftrag verbraucht oder von ihm zurückgegeben; quantity ist vorzeichenbehaftet
- compliance.chain_divergence — die Hash-Kette des Prüfpfads ist gerissen (Enterprise)
- asset.downtime_started — ein Maschinenstillstand wurde eröffnet
- asset.downtime_ended — ein Maschinenstillstand wurde beendet; die Dauer lässt sich nun verbuchen
- report.daily_summary — die abgeschlossenen Arbeiten des Tages, einmal täglich zur von Ihnen gewählten Uhrzeit
- purchase_order.created — eine Bestellung wurde angelegt; die Nutzlast enthält die Bestellung (reference, status, total_cost, currency, expected_date, erp_reference) und ihren Lieferanten
- purchase_order.status_changed — eine Bestellung hat den Status gewechselt (genehmigt, bestellt, storniert …); die Nutzlast enthält previous_status und new_status
- purchase_order.received — eine Lieferung zu einer Bestellung wurde empfangen (wird bei jeder Lieferung gesendet, auch bei Teillieferungen); die Nutzlast listet jede Position mit quantity_ordered, quantity_received (diese Lieferung), quantity_received_total und quantity_remaining sowie receipt_id, fully_received und closed_short; new_status ist PARTIALLY_RECEIVED, solange noch Ware erwartet wird, und RECEIVED, sobald die Bestellung vollständig oder abgeschlossen ist
Warnung
Der Zustellumschlag
Jede GENERIC-Zustellung ist ein POST, dessen Rumpf denselben Umschlag aus drei Schlüsseln trägt: event, timestamp und data, wobei data nach Entität indiziert ist. Drei Header begleiten ihn. X-FreeMaint-Signature hat die Form t=<Unix-Sekunden>,v1=<hex>, wobei v1 der HMAC-SHA256 der exakten Zeichenkette "<t>.<Rohrumpf>" ist, signiert mit dem Geheimnis dieses Webhooks; X-FreeMaint-Event wiederholt den Ereignisnamen; X-FreeMaint-Delivery-Id identifiziert die Zustellung. Prüfen Sie die Signatur gegen den ROHEN Rumpf, vor dem Parsen — ein erneutes Serialisieren des JSON verändert die Bytes, und die Signatur passt dann nicht mehr.
{ "event": "work_order.created", "timestamp": "2026-09-05T08:14:02.371Z", "data": { "work_order": { "id": 5521, "title": "Leck an Pumpe Nr. 3", "status": "OPEN", "priority": "HIGH", "dueDate": "2026-09-08T00:00:00.000Z" } } }
Zuverlässigkeit und Überwachung
Eine Zustellung wird 3-mal versucht, mit exponentiellem Backoff auf einer Basis von 2 Sekunden — rund 2 s, dann 4 s, dann 8 s —, und jeder Versuch läuft nach 10 Sekunden in eine Zeitüberschreitung. Ein 2xx markiert sie als zugestellt. 5xx-Antworten und Netzwerkfehler werden erneut versucht. Ein 4xx gilt als endgültige Ablehnung und wird ÜBERHAUPT NICHT wiederholt: Antwortet Ihr Endpunkt wegen eines Fehlers auf Ihrer Seite mit 400 oder 401, endet diese Zustellung dort, und nur eine manuelle Wiederholung holt sie zurück.
- Jede Zustellung wird gespeichert: das Ereignis, die exakt gesendete Nutzlast, der Status, der HTTP-Code, den Sie zurückgegeben haben, die ersten rund 500 Zeichen Ihres Antwortrumpfs, die Anzahl der Versuche und die Fehlermeldung.
- Die Status sind PENDING, DELIVERED, FAILED, RATE_LIMITED (die Starter-Obergrenze von 100 Zustellungen pro Tag wurde erreicht) und SKIPPED_TIER (der Tarif berechtigt nicht mehr zu diesem Ziel).
- Eine gespeicherte Zustellung lässt sich aus der Historie des Webhooks erneut abspielen. Dabei wird die gesicherte Nutzlast unverändert erneut gesendet, die fachlichen Schlüssel darin bleiben also identisch, und ein Empfänger, der sauber dedupliziert, bucht nicht zweimal.
- Jeder Webhook führt lastTriggeredAt, lastSuccessAt, lastFailureAt und einen failureCount mit — genug für eine Zustandsprüfung, ohne die vollständige Historie zu öffnen.
- Die Webhook-Liste und ihr Zustellprotokoll sind in jedem Tarif lesbar, eine durch eine Herabstufung gestoppte Integration ist also sichtbar statt still tot.
- Nichts alarmiert Sie, wenn Zustellungen zu scheitern beginnen — keine E-Mail, keine Benachrichtigung. Wenn ein stiller Ausfall für Sie ins Gewicht fiele, fragen Sie das Zustellprotokoll aus Ihrer eigenen Überwachung heraus ab.
Sicherheit
URLs werden beim Speichern des Webhooks geprüft, und die Regeln sind mit Absicht streng: Ein Webhook ist eine ausgehende Anfrage unserer Server, eine großzügige Regel wäre also ein SSRF-Vektor.
- Schlüssel werden als SHA-256-Hashes gespeichert — der Klartextwert lässt sich nach der Erstellung nicht mehr aus FreeMaint zurückgewinnen.
- Webhook-URLs müssen https verwenden. Einfaches http und jedes andere Schema werden abgelehnt.
- Nur die Ports 443 und 8443 werden akzeptiert.
- Hosts aus Loopback-, Link-Local- und privaten Bereichen (127.x, 10.x, 192.168.x, 172.16–31.x, ::1 und dergleichen) werden abgelehnt: Der Empfänger muss aus dem öffentlichen Internet erreichbar sein. Ein System, das nur innerhalb Ihres Werks existiert, braucht ein öffentliches Relais oder einen Tunnel.
- Zugangsdaten in der URL (https://user:pass@host) werden abgelehnt.
- Weiterleitungen werden nicht verfolgt — der Endpunkt, den Sie eintragen, ist der Endpunkt, der das POST erhält.
- Jeder Webhook hat sein eigenes Signaturgeheimnis, und es gibt eine Aktion zum Rotieren des Geheimnisses. Die Rotation greift sofort, bringen Sie das neue Geheimnis auf Ihrer Seite also im selben Zeitfenster aus.
- Ziele vom Typ SLACK und TEAMS erhalten eine plattformgerecht formatierte Nachricht und KEINEN FreeMaint-HMAC-Header: Für sie ist die Geheimhaltung der URL die einzige Authentifizierung. Verwenden Sie GENERIC für alles, was Sie kryptografisch prüfen müssen.
Muster, die heute gut funktionieren
Das sind die Integrationen, die die aktuelle Oberfläche sauber trägt, samt dem Detail, das jede davon im Produktivbetrieb sicher macht.
Verbrauch in ein ERP spiegeln
Abonnieren Sie part.consumed. quantity ist vorzeichenbehaftet — negativ bei der Ausgabe an den Auftrag, positiv bei der Rückgabe — und die Nutzlast trägt eine stock_movement_id, die über Wiederholungen und erneute Abspielungen hinweg stabil bleibt; verwenden Sie sie als Idempotenzschlüssel. Die Mutation adjustStock löst dieses Ereignis bewusst NICHT aus, und genau das verhindert, dass Ihre eigenen Schreibvorgänge als Echo zu Ihnen zurücklaufen und eine Schleife bilden.
Bestandskorrekturen einspielen
Rufen Sie adjustStock mit einem vorzeichenbehafteten Delta von Ihrer Seite aus auf. Ein READ_STOCK-Schlüssel kann genau das, ohne irgendetwas anlegen, ändern oder löschen zu können — passgenau für eine Einweg-Bestandsbrücke.
Arbeitsaufträge aus einem anderen System erzeugen
createWorkOrder aus einem SCADA-Alarm, einem Helpdesk-Ticket oder einer ERP-Störmeldung. Der Arbeitsauftrag erhält seine normale Referenz und löst work_order.created aus wie jeder andere auch.
Einen Ersatzteilkatalog im Gleichschritt halten
parts(updatedSince:) liefert nur zurück, was sich seit einem von Ihnen gewählten Zeitpunkt geändert hat. createPart und updatePart nehmen customReference entgegen — Ihren eigenen Code für das Ersatzteil — und part(barcode:) gleicht anschließend darauf ab, Sie können ein Ersatzteil also über die Kennung nachschlagen, die Ihr ERP ohnehin benutzt. Eindeutigkeit wird nicht erzwungen; dafür zu sorgen ist Ihre Aufgabe.
Den Kreis beim Abschluss schließen
work_order.completed trägt den Arbeitsauftrag und dessen vorherigen Status, was genügt, um auf der Gegenseite eine Wartungsmeldung zu schließen.
Welchen Tarif Sie brauchen
Zwei unabhängige Schranken: eine für die API, eine für Webhooks. Ein Unternehmen im Starter-Tarif kann Webhooks empfangen, ohne die API zu haben.
GraphQL-API und API-Schlüssel
Business und höher. Core und Starter können überhaupt keinen Schlüssel anlegen.
Tageskontingent
5.000 Aufrufe pro Tag bei Business, 100.000 bei Enterprise. Gezählt wird pro Unternehmen und pro Operation, mit Rücksetzung um 00:00 UTC — mehrere Operationen in einer HTTP-Anfrage zu bündeln spart Roundtrips, niemals Kontingent.
Webhooks
Starter und höher, gedeckelt bei 100 Zustellungen pro Tag; unbegrenzt ab Business.
Slack- und Teams-Ziele
Enterprise. Ein generischer Webhook ist ab Starter verfügbar.
Tipp
Verwandte Artikel
War diese Seite hilfreich?