Leitfaden zur ERP- und Systemintegration

Eine Integration abstecken, bevor Sie sie bauen: was die API erreicht, was nicht, und wo die Grenzen liegen

9 Min. Lesezeit
Diesen Abschnitt drucken

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

Stecken Sie Ihr Vorhaben anhand dieser Liste ab, nicht anhand dessen, was das Produkt im Browser leistet. Die eigenen Web- und Mobil-Apps von FreeMaint laufen auf einer internen REST-API, die ein API-Schlüssel nicht erreicht — dass eine Funktion auf dem Bildschirm sichtbar ist, belegt also nicht, dass eine Integration sie steuern kann.

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

Es gibt kein Inspektionsereignis und kein Ereignis für Mängel oder Feststellungen. Wenn Ihr Prozess lautet „eine Inspektion fällt durch, mein ERP reagiert“, wird dafür heute nichts ausgelöst. Planen Sie diese Lücke ausdrücklich ein, statt anzunehmen, es gebe ein hinreichend ähnliches Ereignis.

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

Planen Sie etwas, das an eine der obigen Lücken stößt? Schreiben Sie uns an contact@freemaint.com, was Sie bauen. Welche Fähigkeit als Nächstes ausgeliefert wird, entscheidet sich daran, woran Kunden tatsächlich hängen bleiben.

War diese Seite hilfreich?