OpenAPI mit Logo

Kein Haken an Webhooks. Jetzt flexible APIs bauen mit OpenAPI 3.1 und GEFEG.FX.

Die seit OpenAPI 3.1 unterstützten Webhooks definieren klare Punkte in einem Prozess, an denen andere APIs auf klar definierte Art und Weise Operationen ausführen. Die Besonderheit bei Webhooks besteht darin, dass sie – im Gegensatz zu Callbacks – synchron zum Prozess verlaufen. Der Prozess kann die Ergebnisse des Webhooks weiter verarbeiten.

Wozu dienen Webhooks bei OpenAPI 3.1?

Webhooks sind in Internetanwendungen eigentlich nichts Besonderes. Das Geheimnis des Erfolges von WordPress liegt unter anderem in der tiefen Integration von Webhooks in der gesamten Anwendung. Erst durch diese Technik ist WordPress in der Lage, so flexibel um Plug-ins erweitert zu werden.

Prinzipielle Verwendung von Webhooks bei WordPress

Eine Webseite besteht sowohl technisch als auch optisch aus mehreren Bereichen. Sie hat zumindest einen Kopfbereich, oftmals einen Menübereich, einen Inhaltsbereich und einen Fußbereich.

Eine Webseite besteht ist in mehrere Bereiche aufgeteilt. Oben ein Bereich
Beispielhafter Aufbau einer Webseite

Die Ausgabe einer solchen Webseite ist also ein in WordPress klar definierter Prozess. Und dieser Prozess besteht aus mehreren Schritten. Vor, während oder nach jedem dieser Schritte hat WordPress Punkte definiert, in die sich Plug-ins einhaken können, die sogenannten Webhooks.

Das Bild ist in zwei Bereiche aufgeteilt: Links
Beispiel zur Verwendung von Webhooks bei Plug-ins in WordPress

In dem hier gezeigten, vereinfachten Beispiel erweitert das Plug-in das Menü der Webseite. Zusätzlich ersetzt es einen Block auf einer Seite durch die entsprechende Ausgabe. Die Besonderheit ist also, dass das Plug-in die Verarbeitung der Daten im Hauptprozess von WordPress beeinflusst.

Webhooks bei OpenAPI 3.1 sind also so etwas wie Callbacks, nur anders

Bei OpenAPI 3.1 werden natürlich weiterhin Callbacks unterstützt. Heutzutage setzt man Callbacks insbesondere bei ereignisgesteuertem Management ein. Der API Consumer kann einen Callback abonnieren (Subskription). Dabei teilt er dem Callback die Adresse für die Benachrichtigung mit. Dies ist in der Regel wieder ein API-Endpunkt. Es könnte jedoch, je nach Implementierung, auch eine E-Mail-Adresse sein. Manche Callbacks unterstützen zum Zeitpunkt der Subskription noch die Übergabe bestimmter (Filter-) Parameter.

Callback-Beispiel im Online-Handel

Ein typisches Praxisbeispiel für so eine Art von Anwendung haben wir alle schon einmal erlebt: Wir haben etwas bei einem Onlinehändler bestellt. Und dieser versendet das Paket. Wir bekommen (spätestens) am Tag der Lieferung mitgeteilt, dass das Paket bald eintreffen wird. Manche Anbieter gehen nun aber so weit, dass sie Mitteilungen versenden wie “Der Fahrer ist noch 5 Stopps entfernt”. Was ist hier passiert?

Im Prinzip wurde ein Callback eingerichtet, bei dem ein Geofence um die Zieladresse (hier 5 Stopps) mit angegeben wurde. Bei Erreichen der Ereignisbedingung führt der API-Provider den Callback aus und wir erhalten die entsprechende Nachricht. Und nun können wir selbst aktiv werden, müssen es aber nicht. Manche dieser Anwendungen erlauben nun in Echtzeit die Position des Lieferwagens auf einer Karte nachzuverfolgen. Dazu müssen wir aber aktiv diese Anwendung aufrufen. Tun wir das nicht, hat es keinerlei Einfluss auf den Lieferprozess. Sollten wir nicht anwesend sein und auch keine Ablagenehmigung erteilt haben, tritt erst dann im Lieferprozess eine Ausnahmebehandlung (Papiernachricht in Briefkasten einwerfen) ein.

Das Bild ist in zwei Bereiche

Dieses Design hat in Bezug auf die Implementierung des API Consumers einen entscheidenden Nachteil. Es bedeutet, dass hier eine zweite, vollständige API erstellt werden muss. Der Consumer benötigt zum einen den Endpunkt für den Aufruf durch den Callback. Dazu muss eine sichere Verbindung vom API Provider zum API Consumer hergestellt werden. Zum anderen benötigt er für die weiteren Aufrufe noch zusätzlich eine gesicherte Verbindung in umgekehrter Richtung. Die beiden APIs müssen sich also gegenseitig vertrauen und die jeweiligen gegenüberliegenden Schnittstellen unterstützen. Der Callback an sich verhält sich jedoch passiv.

Aktive Ereignissteuerung über Webhooks ab OpenAPI 3.1

Setzt man statt Callbacks nun Webhooks ein, könnte das Szenario etwas anders aussehen. Gerade bei Just-In-Time Lieferungen kommt es darauf an, den richtigen Artikel zum richtigen Zeitpunkt am richtigen Ort zu haben. Oftmals sind aber auf einem Lkw mehrere Artikel geladen, die zu zeitlich nahen Punkten der Fertigung gebraucht werden. Ein Transportunternehmer hat ein recht enges Zeitfenster, in dem er an der jeweiligen Rampe seine Ware anzuliefern hat.

In der Praxis ist dies oftmals schwierig – trotz der langen Erfahrung und der hohen Automatisierung. An welche Rampe fährt der Fahrer denn nun zuerst, wenn er an der Schranke zum Betriebsgelände steht? Hier ist in dem Prozess “Anlieferung” also eine synchrone Entscheidung gefragt. Der Prozess muss unterbrochen werden und kann erst fortgesetzt werden, wenn die erforderlichen Daten vorliegen.

Das Bild ist vertikal in zwei Bereiche
Webhooks mit OpenAPI 3.1 unterbrechen in der Regel einen Prozess

Der Serviceprozess des API Providers wird durch den Webhook unterbrochen. Der Webhook führt eine Aktion beim API Consumer durch. Die Ergebnisse dieser Aktion können dann im Service des API Providers weiterverarbeitet werden.

Diese Rückgabe ist eine schöne Sache. Aber nicht zwingend erforderlich. Es ist ebenfalls denkbar Webhooks zu definieren, die lediglich eine positive Rückmeldung erwarten, z. B.:200 OK.

Nichts als Webhooks in der API

Eine wesentliche Neuerung bei Webhooks mit OpenAPI 3.1 besteht nun darin, dass APIs spezifiziert werden können, die ausschließlich aus Webhooks bestehen. Als ich das erste Mal davon gehört habe, empfand ich das als merkwürdig. Beim weiteren Nachdenken macht das aber durchaus Sinn.

So lassen sich zum Beispiel APIs spezifizieren, die rein der Prozessüberwachung dienen. Dies ist in etwa vergleichbar mit der Steuerzentrale eines Kraftwerks. Der eigentliche Betrieb erfolgt vollautomatisch. Klar definierte Ereignisse werden auf den Instrumenten der Steuerzentrale angezeigt. Die Bediener haben die Möglichkeit steuernd auf die einzelnen Ereignisse zu reagieren.

Ein weiterer Ansatz wäre die Definition von APIs, die ein ähnliches Konzept wie WordPress verfolgen. Der API Provider führt einen oder mehrere klar definierte Prozesse aus. Zum Beispiel die Kalkulation einer Rechnung, das Buchen eines Tickets oder die Produktion von Waren. In diesem Prozess werden Erweiterungspunkte definiert, vergleichbar mit Extension-Points in XML-Nachrichten. Und an diese Erweiterungspunkte können sich andere APIs einhaken, um die Basisfunktion dynamisch und flexibel zu erweitern.

Nehmen wir einmal an, wir haben eine API geschrieben, die sehr einfache Rechnungen kalkulieren und erstellen kann. Sehr einfach bedeutet hier, dass sie nur einen Verkäufer, einen Käufer und einfache Artikelpositionen unterstützt. Wenn in diesem Prozess an den richtigen Schritten Webhooks definiert sind, kann dieser Prozess einfach erweitert werden. Ein Plug-in könnte zum Beispiel die Berücksichtigung von Rabatten hinzufügen. Ein weiteres Plug-in die Unterstützung von Rechnungen in einer Fremdwährung.

Der geschickte Einsatz von Webhooks macht’s

Und genau hier liegt auch wieder die Schwierigkeit, aber zugleich eine mächtige Chance. Wenn ich auch die Plug-in API so spezifiziert habe, dass sie mit zusätzlichen APIs umgehen kann oder sogar selbst wieder erweitert werden kann, werden aus Webhooks sehr mächtige Werkzeuge.

Genau hier kommt aber der vielleicht entscheidendste Punkt beim Design einer API mit Webhooks. Ich muss den (fremden) APIs vertrauen. Ich muss ihnen zutrauen die Daten für den eigenen Prozess so zu verändern, wie es grundsätzlich vorgesehen ist. Bzw. ich muss auf der API-Provider-Seite berücksichtigen, dass die Daten auf eine Art manipuliert werden, mit der ich ggf. nichts anfangen kann.

Doch dem wirkt andererseits die OpenAPI 3.1 Spezifikation auch wieder entgegen. Denn der API-Provider kann auch die Datenstruktur des Rückgabeformats spezifizieren. Eine Prüfung auf inhaltliche Korrektheit oder Sinnhaftigkeit ist jedoch ggf. noch zusätzlich vorzunehmen. Berücksichtigt der Provider diese Situation nicht oder hat das Plug-in mit der Rabattfunktion einen Fehler, könnte die gesamte Rechnung falsch werden. Zum Beispiel dann, wenn anschließend (scheinbar) auf der Positionsebene steht, dass für einen Artikel, der mit Menge 5 und einem Stückpreis von EUR 4 insgesamt nur EUR 17 zu zahlen sind.

Ein Webhook-Beispiel, erstellt mit GEFEG.FX

Da die offiziellen Webhook Beispiele der OpenAPI Initiative nur sehr dürftig sind, möchte ich abschließend noch ein einfaches Beispiel im YAML-Format aufzeigen.

openapi: 3.1.0
info:
  title: GEFEG CrossIndustryInvoice Webhook example
  version: 1.0.0
webhooks:
  createLineItem:
    post:
      summary: Inform external API that a new LineItem is created
      requestBody:
        description: Information about a new line item in the invoice
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LineItem'
        required: true
      responses:
        200:
          description: Return a 200 status to indicate that the data was processed successfully. The response body may contain the extended line item.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LineItem'
  validateLineItem:
    get:
      summary: Validate the LineItem
      requestBody:
        description: Information about the LineItem to be validated
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LineItem'
        required: true
      responses:
        406:
          description: Not Acceptable, The validation went wrong.
        200:
          description: OK, the line item is valid

components:
  schemas:
    LineItem:
      type: object
      properties:
        AssociatedDocumentLineDocument:
          $ref: '#/components/schemas/DocumentLineDocumentType'
        SpecifiedTradeProduct:
          $ref: '#/components/schemas/TradeProductType'
        SpecifiedLineTradeAgreement:
          $ref: '#/components/schemas/LineTradeAgreementType'
        SpecifiedLineTradeDelivery:
          $ref: '#/components/schemas/LineTradeDeliveryType'
        SpecifiedLineTradeSettlement:
          $ref: '#/components/schemas/LineTradeSettlementType'
      required:
        - AssociatedDocumentLineDocument
        - SpecifiedTradeProduct
        - SpecifiedLineTradeAgreement
        - SpecifiedLineTradeDelivery
        - SpecifiedLineTradeSettlement
    DocumentLineDocumentType:
      type: object
      properties:
        LineID:
          $ref: '#/components/schemas/IDType'
    LineTradeAgreementType:
      type: object
      properties:
        NetPriceProductTradePrice:
          $ref: '#/components/schemas/TradePriceType'
      required:
        - NetPriceProductTradePrice
    LineTradeDeliveryType:
      type: object
      properties:
        BilledQuantity:
          $ref: '#/components/schemas/QuantityType'
      required:
        - BilledQuantity
    LineTradeSettlementType:
      type: object
      properties:
        SpecifiedTradeSettlementLineMonetarySummation:
          $ref: '#/components/schemas/TradeSettlementLineMonetarySummationType'
      required:
        - SpecifiedTradeSettlementLineMonetarySummation
    TradeProductType:
      type: object
      properties:
        Name:
          $ref: '#/components/schemas/TextType'
    IDType:
      type: string
    QuantityType:
      type: number
    TextType:
      type: string
    TradePriceType:
      type: object
      properties:
        ChargeAmount:
          $ref: '#/components/schemas/AmountType'
      required:
        - ChargeAmount
    TradeSettlementLineMonetarySummationType:
      type: object
      properties:
        LineTotalAmount:
          $ref: '#/components/schemas/AmountType'
      required:
        - LineTotalAmount
    AmountType:
      type: number
Code-Sprache: YAML (yaml)

In diesem Beispiel werden zwei Webhooks definiert. Der erste createLineItem wird aufgerufen, wenn eine neue Position eingefügt wird. Der Webhook führt also in der externen API die POST – Operation durch und übergibt dabei die Informationen der aktuellen Position. Als Rückgabe wird wiederum die (ggf. durch Extension) erweiterte Position als Rückgabewert erwartet.

Der zweite Webhook validateLineItem dient dazu, die Validierung der Position erweitern zu können. Die externe API wäre somit in der Lage, zum Beispiel die Rabattberechnung zu prüfen. Stimmt diese, liefert sie den Code200. Ist etwas schiefgegangen, liefert sie den Code406.

Dieses Beispiel ist eventuell noch nicht in allen Belangen ausgereift, soll aber die Möglichkeit zur Anwendung von Webhooks mit OpenAPI 3.1 zeigen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert