# Sichere APIs für KI-Agenten: Nachvollziehbare Schreibvorgänge

Ein KI-Agent macht eine API nicht automatisch gefährlich. Eine API wird gefährlich, wenn sie weit gefasste Verben, unklare Ergebnisse und Fehler anbietet, die den Aufrufer zum Raten zwingen. Menschen gleichen das mit Kontext, Zurückhaltung und einer schnellen Nachricht im Chat aus. Ein Agent reagiert dagegen mit Wiederholungen und einem weiteren Tool-Aufruf. Aus einem harmlosen Timeout werden so zwei Rückerstattungen, zwei Deployments oder ein gelöschter Datensatz, den niemand anfassen wollte.

Sichere APIs für KI-Agenten begrenzen die erlaubte Aktion, machen wiederholte Schreibvorgänge harmlos und hinterlassen eine Spur, der später ein Mensch folgen kann. Das ist API-Design, keine Prompt-Arbeit. Ein Prompt kann einem Agenten sagen, dass er vorsichtig sein soll. Der Endpunkt muss eine Aktion trotzdem ablehnen, wenn sie außerhalb seines Vertrags liegt.

Ich habe Teams gesehen, die vor einen unklaren Verwaltungsendpunkt einen Freigabebildschirm setzten und das Kontrolle nannten. Das reicht nicht. Wenn ein freigegebener Aufruf «alles in diesem Konto ändern» bedeutet, muss ein Mensch unter Zeitdruck ein Bündel verborgener Folgen prüfen. Präzision gehört zuerst in die API. Dann gibt es für die Freigabe etwas Verständliches, das genehmigt werden kann.

## Weit gefasste Verben zwingen Agenten zum Raten

Ein Agent sollte eine Operation aufrufen, deren Name, Eingaben und Nebenwirkungen in einen Satz passen. Breite Endpunkte zwingen ihn, Geschäftsregeln aus unklaren Feldern, alten Beispielen oder einer Fehlermeldung abzuleiten, die nur «bad request» sagt. Dort beginnt unsichere Improvisation.

Betrachte einen Endpunkt wie `POST /admin/execute` mit einem Payload, der `action` und beliebiges JSON enthält. Ein von Menschen geschriebener Client verwendet heute vielleicht nur fünf Aktionen. Der Endpunkt stellt jedoch jedem Aufrufer mit Zugriff alle aktuellen und künftigen Aktionen zur Verfügung. Der Server kann keine sinnvolle Berechtigungsgrenze ausdrücken, und ein Genehmiger kann ohne eine Prüfung des Payloads wie Quellcode nicht erkennen, was der Agent tun wird.

Ersetze ihn durch Operationen, die eine Zustandsänderung benennen:

- `POST /projects/{project_id}/deployments` erstellt ein Deployment aus einer angegebenen Revision.
- `POST /invoices/{invoice_id}/refunds` erstellt eine Rückerstattung mit ausdrücklichem Betrag und Grund.
- `POST /users/{user_id}/access-revocations` entzieht einem namentlich genannten Benutzer den Zugriff.
- `POST /exports` startet einen definierten Export mit ausdrücklich genannter Datenkategorie.

Auch diese Operationen können riskant sein. Entscheidend ist, dass jede dem Server einen konkreten Ort gibt, an dem er Regeln durchsetzen kann: gültige Übergänge, Betragsgrenzen, Besitz des Ziels, erforderliche Freigaben und ein Begründungsfeld an der richtigen Stelle.

Verwechsle eine generische CRUD-Schnittstelle nicht mit einer brauchbaren Agenten-Schnittstelle. `PATCH /customers/{id}` lädt dazu ein, jedes beschreibbare Feld zu ändern. Wenn `billing_email` routinemäßig geändert wird, die Änderung von `tax_status` aber einen Compliance-Prozess startet, gehören beide Änderungen nicht hinter denselben beiläufigen Patch. Erstelle für den folgenreichen Übergang eine eigene fokussierte Operation und richte ihr Eingabemodell an der Entscheidung aus.

Eine eng begrenzte Operation verbessert auch die Wiederherstellung. Wenn ein Agent sagt «Die Deployment-Anfrage ist abgelaufen», kann ein Operator nach genau einer Deployment-Erstellung suchen. Wenn er sagt «Der Admin-Befehl ist abgelaufen», muss der Operator zunächst herausfinden, welchen Befehl der Agent zusammengestellt hat.

### Vorbedingungen in die Anfrage aufnehmen

Schreibvorgänge sollten die Bedingung nennen, unter der sie sinnvoll sind. Eine Anfrage zum Genehmigen einer Ausgabe kann den erwarteten Prüfstatus enthalten. Eine Anfrage zum Aktualisieren eines Dokuments kann die gelesene Version angeben. Wenn sich der Status geändert hat, sollte der Server den Schreibvorgang ablehnen, statt ihn stillschweigend auf eine andere Realität anzuwenden.

HTTP bietet dafür bereits nützliche Mechanismen. RFC 9110 definiert bedingte Anfragen über Header wie `If-Match`. Ein Server kann ein veraltetes Entity-Tag mit `412 Precondition Failed` ablehnen. Du kannst auch ein Feld `expected_version` anbieten, wenn das besser zu deiner API passt. Die konkrete Wahl ist weniger wichtig als die Disziplin: Der Client muss die Version oder den Status nennen, den er ändern will.

Akzeptiere kein Client-Feld wie `force: true` als Ausweg aus jedem Konflikt. Dieses Feld wird schnell zu einer Möglichkeit für Agenten, genau die Sicherheitsprüfung zu umgehen, die du eingebaut hast. Reserviere eine Überschreibung für eine eigene Operation, eine andere Berechtigungsstufe und einen sichtbaren Audit-Eintrag.

## Ein Schreibvorgang braucht eine eigene Identität neben dem HTTP-Versuch

Jeder nach außen sichtbare Schreibvorgang sollte eine vom Client gelieferte Idempotenz-ID haben. Der Server verwendet sie, um zu erkennen, dass mehrere Übermittlungsversuche dieselbe beabsichtigte Aktion ausdrücken.

Eine Anforderungs-ID und eine Idempotenz-ID lösen unterschiedliche Probleme. Ein Gateway oder Server erzeugt oft für jeden HTTP-Versuch eine Anforderungs-ID. Wenn die Verbindung abbricht, nachdem der Server den Schreibvorgang übernommen hat, die Antwort den Aufrufer aber noch nicht erreicht, erhält die Wiederholung eine neue Anforderungs-ID. Die Idempotenz-ID muss gleich bleiben, weil sich die beabsichtigte Schreibaktion nicht geändert hat.

Die übliche Fehlerfolge sieht so aus:

1. Der Agent sendet eine Anfrage zum Erstellen einer Auszahlung.
2. Dein Server speichert die Auszahlung und ruft einen externen Anbieter auf.
3. Die Verbindung bricht ab, bevor der Agent die Erfolgsantwort erhält.
4. Der Agent sieht ein unbekanntes Ergebnis und wiederholt die Anfrage.
5. Dein Server erstellt eine weitere Auszahlung, weil er eine neue HTTP-Anfrage erkennt.

Die Wiederholungsrichtlinie hat den Fehler nicht verursacht. Die API hat ihn verursacht, indem sie Übermittlung mit Absicht gleichsetzte.

Verwende einen Header oder ein Anfragefeld, das der Client vor dem ersten Versuch erstellt und bis zu einer endgültigen Antwort beibehält. HTTP-Header heißen häufig `Idempotency-Key`, die Kennung muss jedoch kein Geheimnis sein. Eine zufällige UUID funktioniert gut. Leite sie nicht allein aus einem Zeitstempel ab und verwende keine Kennung, die bei unabhängigen Schreibvorgängen kollidieren kann.

### Fingerabdruck und Ergebnis der Anfrage speichern

Der Server muss eine Idempotenz-ID an mehr als ein Statusflag binden. Speichere die Identität des Aufrufers, die Zielroute, einen kanonischen Fingerabdruck des semantisch relevanten Anfrage-Bodys und das vollständige Ergebnis, das für eine Wiederholung benötigt wird. Wenn derselbe Aufrufer mit derselben ID und demselben Fingerabdruck wiederholt, gib die ursprüngliche Antwort zurück. Wenn sich der Body unterscheidet, lehne die Anfrage mit einem Konflikt ab.

Der IETF-Entwurf «The Idempotency-Key HTTP Header Field» beschreibt diesen Header als Möglichkeit für Clients, nicht idempotente HTTP-Methoden fehlertolerant zu machen. Seine Warnung zur Eindeutigkeit ist wichtig: Der Client sollte einen Wert nicht für eine andere Anfrage wiederverwenden. Ich würde bei der Implementierung noch weiter gehen. Setze diese Regel serverseitig durch, weil Agenten Anfragen wiederholen, neu starten und gelegentlich einen Zustand wiederverwenden, den ein menschlicher Client verworfen hätte.

Ein kompakter Vertrag kann so aussehen:

```http
POST /v1/projects/prj_48/deployments
Idempotency-Key: 8c8d77c1-4ef9-4fae-b0ba-5480f686ce4c
Content-Type: application/json

{
  "revision": "a1b2c3d4",
  "environment": "staging",
  "expected_project_version": 17
}
```

Gib beim ersten akzeptierten Aufruf eine Ressource und beide Kennungen zurück:

```json
{
  "request_id": "req_01J8X7QK3JZ6",
  "deployment": {
    "id": "dep_01J8X7R5G2",
    "state": "queued",
    "revision": "a1b2c3d4",
    "environment": "staging"
  }
}
```

Wenn der Agent nach einem Timeout dieselbe Anfrage wiederholt, gib dieselbe `dep_01J8X7R5G2` zurück, kein zweites Deployment. Wenn er `environment` in `production` ändert, aber die Kennung beibehält, gib einen Konflikt zurück, der die Behebung klar macht:

```json
{
  "error": {
    "code": "idempotency_payload_mismatch",
    "message": "This idempotency identifier belongs to a deployment request with different parameters.",
    "request_id": "req_01J8X84S9P2V"
  }
}
```

Bewahre Idempotenzdatensätze mindestens so lange auf, wie realistische Client-Wiederholungen und die Wiederherstellung von Jobs möglich sind. Eine sehr kurze Aufbewahrungsdauer erzeugt eine verspätete Dublette, die in der Produktion wie ein sporadischer Fehler aussieht. Wenn Speicherdruck ein Ablaufen erzwingt, dokumentiere das Zeitfenster klar und sorge dafür, dass Verbraucher ein dazu passendes Wiederholungsverhalten wählen.

## Wiederholungen sind nur sinnvoll, wenn das Ergebnis ausreichend bekannt ist

Ein Agent sollte Transportfehler und ausgewählte vorübergehende Antworten wiederholen, aber niemals eine neue Aktion erfinden, um Unsicherheit zu umgehen. Deine Antwortklassen müssen diese Entscheidung ermöglichen.

RFC 9110 definiert `429 Too Many Requests` und erlaubt `Retry-After`. Wenn du diesen Mechanismus sendest, halte dich daran. Ein Aufrufer kann die angegebene Zeit warten, seine Idempotenz-ID beibehalten und dieselbe Anfrage senden. Bei einem vorübergehenden Serverfehler solltest du eine 5xx-Antwort mit Anforderungs-ID zurückgeben und angeben, ob der Server die Operation angenommen hat. Verwende keinen unklaren 500-Fehler für eine Validierungsabweichung oder eine verweigerte Autorisierung. Damit bringst du Clients das falsche Wiederholungsverhalten bei.

Bei asynchronen Schreibvorgängen sind Annahme und Abschluss zwei verschiedene Tatsachen. Eine `202 Accepted`-Antwort sollte eine Operationsressource zurückgeben, die die Arbeit und ihren Status nennt. Nach einem Timeout kann der Agent diese Ressource abfragen, statt einen Seiteneffekt erneut zu übermitteln.

```json
{
  "request_id": "req_01J8X9FW7GH2",
  "operation": {
    "id": "op_01J8X9FTVX",
    "state": "running",
    "status_url": "/v1/operations/op_01J8X9FTVX"
  }
}
```

Die Statusressource braucht mehr als `running` und `failed`. Füge einen Endstatus, bei Erfolg einen Verweis auf das Ergebnis und einen öffentlichen Fehlercode hinzu, wenn der Worker den Auftrag nicht abschließen kann. Ein Deployment, das beispielsweise Gesundheitsprüfungen nicht besteht, sollte nicht wie ein API-Transportfehler aussehen. Der Agent muss einen Deployment-Fehler melden oder beheben. Einen Verbindungsfehler sollte er nur dann wiederholen, wenn der Server die Anfrage nie angenommen hat.

Vermeide automatische Wiederholungen für Aktionen, die E-Mails versenden, Geld abbuchen, Zugangsdaten austauschen oder ein externes System aufrufen, sofern dein Server die Deduplizierung nicht bis zur tatsächlichen Wirkung kontrolliert. Idempotenz in deiner Datenbank verhindert nicht zwei E-Mails, wenn ein Worker abstürzt, nachdem der E-Mail-Anbieter die Nachricht angenommen hat, aber bevor der Worker den Abschluss speichert. Verwende einen Outbox-Eintrag mit einer stabilen Deduplizierungsreferenz beim Anbieter, sofern dieser das unterstützt. Wenn das externe System nicht deduplizieren kann, mache die Operation beobachtbar und verlange nach einem unbekannten Ergebnis eine menschliche Entscheidung.

## Fehler müssen dem Aufrufer sagen, wie er die Anfrage repariert

Nützliche Fehlermeldungen beschreiben den verletzten Vertrag, nicht die Verlegenheit des Servers. Ein Agent kann mit einem präzisen Fehler arbeiten. Mit einer HTML-Fehlerseite, einem Stacktrace oder «invalid input» nach einer Anfrage mit zehn Feldern kann er nicht sicher arbeiten.

Gib für jeden erwarteten Fehler ein einheitliches JSON-Format zurück. Enthalten sein sollten ein stabiler `code` für Programme, eine kurze `message` für Protokolle und Menschen, eine Anforderungs-ID sowie Details auf Feldebene, sofern diese sicher offengelegt werden können. RFC 9457, «Problem Details for HTTP APIs», bietet mit Feldern wie `type`, `title`, `status`, `detail` und `instance` eine Standardstruktur. Du musst nicht jedes Feld übernehmen, um die zentrale Aussage zu verstehen: Fehler sind Teil des API-Vertrags und kein beiläufiger Text.

Diese Antwort sagt einem Agenten genau, was er ändern muss:

```json
{
  "error": {
    "code": "invalid_state_transition",
    "message": "A refund can be created only for a paid invoice.",
    "request_id": "req_01J8XAS2D8M4",
    "details": {
      "invoice_id": "inv_204",
      "current_state": "draft",
      "allowed_states": ["paid", "partially_paid"]
    }
  }
}
```

Diese Antwort führt zum Raten:

```json
{
  "error": "Request failed"
}
```

Die zweite Antwort schickt einen Agenten zurück zur Dokumentation, zum Quellcode oder zu Erkundungsaufrufen. Erkundungsaufrufe gegen eine Schreib-API sind der Weg, auf dem aus einem kleinen Fehler ein lauter Zwischenfall wird.

Platziere keine Geheimnisse in Fehlern. Spiegle keine Autorisierungs-Header, Zugriffstoken, signierten URLs, rohen Datenbankabfragen oder Antworten externer Dienste zurück, die Daten eines anderen Kunden enthalten könnten. Ein häufiges schlechtes Muster besteht darin, jede abgefangene Ausnahme zu verpacken und ihre Meldung an den Aufrufer zurückzugeben. Das erleichtert einen Tag lang die Fehlersuche und schafft jahrelang einen Offenlegungskanal.

Trenne ungültige Eingaben von unzureichender Berechtigung. `422 Unprocessable Content` kann einen korrekt aufgebauten Payload beschreiben, der gegen eine Geschäftsregel verstößt. `403 Forbidden` sollte sagen, dass die angeforderte Operation eine Berechtigung oder Freigabe erfordert, ohne Ressourcen offenzulegen, die der Aufrufer nicht prüfen darf. `404 Not Found` kann sinnvoll sein, wenn du die Existenz einer Ressource absichtlich verbirgst. Lege die Semantik fest, dokumentiere sie und wende sie einheitlich an.

Ein guter Fehler sagt auch, wann eine Wiederholung sinnlos ist. `invalid_state_transition`, `idempotency_payload_mismatch` und `approval_required` sollten blinde Wiederholungen stoppen. `rate_limited` mit einer Wartezeit und `upstream_temporarily_unavailable` können zu einer kontrollierten Wiederholung einladen. Diese Unterscheidung verhindert mehr Schaden als ein ausgeklügelter Agent-Prompt.

## Anforderungs-IDs machen aus einer strittigen Aktion eine Untersuchung

Gib jeder eingehenden Anfrage eine Anforderungs-ID zurück, sende sie im Antwort-Body oder Header mit und führe sie durch jeden internen Aufruf, jede Warteschlangennachricht, jeden Worker-Job und jeden externen Anbieteraufruf. Wenn ein Agent sagt, er habe keine Antwort erhalten, musst du zwei getrennte Fragen beantworten: Hat deine API die Aktion angenommen und was hat jede Komponente danach getan?

Erzeuge die Anforderungs-ID an der Vertrauensgrenze, wenn der Aufrufer keine liefert. Du kannst eine Korrelations-ID des Aufrufers für dessen eigene Ablage akzeptieren, darfst aber nicht zulassen, dass ein nicht vertrauenswürdiger Aufrufer deine serverseitig vergebene Kennung überschreibt. Bewahre beide auf, wenn das nützlich ist. Die Server-ID verankert deine Protokolle, die Aufrufer-ID verbindet eine Folge von Agentenentscheidungen.

Protokolliere strukturierte Ereignisse, statt Textzeilen zusammenzusetzen, die Operatoren später mit regulären Ausdrücken auswerten müssen. Halte mindestens Anforderungs-ID, authentifizierte Identität, Operationsnamen, Zielressource, gegebenenfalls die Idempotenz-ID, Autorisierungsentscheidung, Ergebnisstatus und Verweise auf erstellte Ressourcen fest. Redigiere Anfragefelder anhand eines Schemas, nicht durch einen nachträglichen Zeichenkettenfilter. Ein Feld namens `token` lässt sich leicht redigieren. Ein in beliebigem Text eingebettetes Geheimnis nicht.

Die Spur muss die Reihenfolge bewahren, ohne mehr zu behaupten, als sie beweist. Eine Anforderungs-ID kann zeigen, dass deine API einen Auftrag angenommen hat und ein Worker einen Anbieteraufruf gesendet hat. Sie kann nicht beweisen, dass ein Mensch die Aktion beabsichtigt hat, sofern dein System diese Entscheidung nicht separat festhält. Halte die Unterscheidung klar:

- Ein Korrelationsdatensatz verbindet Ereignisse, die zu einer Anfrage gehören.
- Ein Auditdatensatz sagt, wer oder was eine Aktion autorisiert hat und was das System getan hat.
- Ein Idempotenzdatensatz verhindert einen doppelten logischen Schreibvorgang.

Teams fassen diese Informationen oft in einer Datenbankzeile zusammen. Dann muss die Zeile Wiederholungen, Fehlersuche, Compliance-Prüfung und nutzerseitige Historie bedienen und erfüllt keine dieser Aufgaben sauber. Du kannst verwandte Verweise gemeinsam speichern, solltest aber die unterschiedlichen Bedeutungen im Datenmodell erhalten.

Bei risikoreicheren Aktionen solltest du die normalisierte Anfrage, den Autorisierungskontext, das Ergebnis der Richtlinien- oder Freigabeprüfung und einen Ergebnis-Digest in einem unveränderbaren Audit-Stream festhalten. Schütze diesen Stream vor dem normalen Anwendungskonto. Andernfalls kann ein kompromittierter Dienst genau die Historie umschreiben, die ihn entlarven würde.

Sallyport verfolgt für Agentenaktionen einen nützlichen Ansatz: Es speichert Agent-Sitzungen und einzelne Aufrufe in einem schreibblinden, verschlüsselten und hashverketteten Audit-Protokoll. `sp audit verify` prüft die Kette offline, ohne einen Tresorschlüssel zu benötigen. Deine API braucht trotzdem eigene Datensätze, weil ein Gateway zwar zeigen kann, dass es einen Aufruf weitergeleitet hat, aber nur dein Dienst die von ihm festgeschriebene Zustandsänderung kennt.

## Der Authentifizierungsumfang macht eine unsichere Operation nicht sicher

Kurzlebige Zugangsdaten und begrenzte Berechtigungsumfänge verringern den möglichen Schaden, machen einen breiten Endpunkt aber nicht sicher. Ein auf ein Projekt beschränktes Token kann trotzdem jedes Deployment zerstören, alle erlaubten Daten exportieren oder jede verfügbare Verwaltungsaktion in diesem Projekt auslösen.

Binde die Autorisierung an Operation und Ziel. Wer ein Deployment erstellen darf, sollte nicht automatisch eines in die Produktion überführen dürfen. Wer den Zugriff eines Benutzers entziehen darf, sollte nicht die Berechtigung erhalten, dessen Abrechnungsprofil zu ändern, nur weil beides unter `/users/{id}` liegt.

Halte Zugangsdaten möglichst vom Agenten fern. Ein Agent, der ein Bearer-Token erhält, kann es in ein Protokoll, eine Debug-Datei, die Shell-Historie oder einen Aufruf eines externen Dienstes kopieren. Lege die Nutzung stattdessen hinter ein lokales Aktions-Gateway oder einen serverseitigen Broker, der die Zugangsdaten für eine freigegebene Operation auswählt. Der Agent übermittelt Absicht und Parameter. Die vertrauenswürdige Komponente fügt das Geheimnis erst beim externen Aufruf ein.

Dieses Design ersetzt nicht die Parameterprüfung. Wenn ein Agent `url: https://anything.example` angeben kann, kann ein HTTP-Helfer mit eingefügten Zugangsdaten zu einem Werkzeug für Geheimnisabfluss werden. Binde Zugangsdaten an benannte externe Systeme und Methoden. Prüfe Hosts auch nach Weiterleitungen, nicht nur davor. Bei SSH solltest du Zugangsdaten möglichst an bekannte Hosts und eine eingeschränkte Befehlsschnittstelle binden, statt beliebigen Fernzugriff auf eine Shell anzubieten.

Menschliche Freigaben haben ihren Platz, sollten aber eine kleine Aktion mit sichtbarem Ziel und sichtbaren Folgen abdecken. Eine Freigabe pro Sitzung beantwortet die Frage «Darf dieser Agent-Prozess überhaupt arbeiten?» Eine Freigabe pro Aufruf beantwortet die Frage «Darf er diese konkrete sensible Aktion jetzt ausführen?» Keine von beiden rettet einen Endpunkt, dessen Payload alles bedeuten kann.

## Nebenläufigkeit braucht einen eindeutigen Verlierer

Idempotenz verhindert die doppelte Übermittlung einer Absicht. Sie löst nicht zwei unterschiedliche Absichten, die miteinander konkurrieren. Wenn zwei Agenten eine Rechnung im Status `paid` lesen und beide mit unterschiedlichen Idempotenz-IDs eine vollständige Rückerstattung senden, muss dein Server entscheiden, welche Anfrage gewinnt.

Verwende eine transaktionale Zustandsänderung, sofern dein Speichersystem das unterstützt. Die Aktualisierung sollte den erwarteten Status enthalten, und der Server sollte den Konflikt melden, wenn ein anderer Schreiber ihn zuerst geändert hat. Ein Versionsfeld, Entity-Tag oder bedingtes Update gibt der API die Möglichkeit, veraltete Absichten abzulehnen, statt sie nach einer Zustandsänderung anzuwenden.

Modelliere eine Rückerstattung beispielsweise als Operation auf dem verbleibenden erstattungsfähigen Betrag und nicht als blinden Befehl, der einem vom Client gelieferten Betrag vertraut. Prüfe in einer Transaktion den aktuell bezahlten Betrag, ziehe frühere Rückerstattungen ab, validiere den angeforderten Betrag, reserviere die neue Rückerstattung und erstelle den Rückerstattungsdatensatz. Ein separater asynchroner Worker kann den Zahlungsanbieter nach dieser Reservierung aufrufen. Bei einer Wiederholung setzt der Worker denselben Rückerstattungsdatensatz fort, statt einen neuen zu erstellen.

Sag Agenten nicht «erst prüfen, dann handeln», wenn das deine einzige Nebenläufigkeitskontrolle ist. Ein vorbereitender `GET` hilft dem Agenten, eine sinnvolle Anfrage zu bilden. Ein anderer Aufrufer kann den Status jedoch zwischen Lesen und Schreiben ändern. Der Schreibendpunkt ist für die Korrektheit zuständig, weil er den tatsächlichen Status beim Festschreiben sieht.

Gestalte auch die Stornierung mit derselben Sorgfalt. `DELETE /operations/{id}` sollte nicht versprechen, dass eine externe Aktion nie stattgefunden hat. Die Antwort sollte den tatsächlichen Stornostatus zurückgeben: Stornierung angefordert, vor der Weiterleitung storniert, vor der Stornierung abgeschlossen oder nach der Weiterleitung nicht mehr stornierbar. Agenten und Menschen brauchen eine Sprache, die die Grenze zwischen deinem System und dem externen Anbieter abbildet.

## Teste unbekannte Ergebnisse, bevor Agenten sie in der Produktion entdecken

Eine Testsuite, die nur eine 200-Antwort prüft, bringt alle dazu, den schwierigsten Teil von Aktions-APIs zu ignorieren. Nimm Fehlerfälle in Vertragstests auf und führe sie an einer echten Dienstgrenze aus, nicht nur an einem simulierten Handler.

Teste für jede Schreiboperation eine Folge, bei der der Server die Wirkung festschreibt und der Client die Antwort verliert. Sende danach dieselbe Idempotenz-ID erneut und prüfe, ob der Server die ursprüngliche Ressource zurückgibt. Sende anschließend dieselbe ID mit geändertem Body und prüfe, ob der Server einen Konflikt zurückgibt, ohne eine weitere Ressource zu erstellen.

Teste parallele Anfragen mit unterschiedlichen Idempotenz-IDs gegen dieselbe Zustandsänderung. Prüfe, ob eine erfolgreich ist und die andere einen konkreten Fehler wegen eines veralteten Status oder einer Geschäftsregel erhält. Wenn beide in einer Testdatenbank erfolgreich sind, weil jeder Test allein läuft, hast du die entscheidende Eigenschaft nicht getestet.

Behandle den Fehlervertrag als Daten. Prüfe Statuscodes, stabile Fehlercodes, Feldnamen und das Vorhandensein einer Anforderungs-ID. Erstelle keine Snapshots nur für die englische Meldung. Du wirst die Formulierungen mit der Zeit verbessern. Clients sollten anhand von `code` und nicht anhand von Text verzweigen.

Führe zuletzt eine Übung für Operatoren durch. Wähle eine abgeschlossene Aktion, eine abgelehnte Aktion, einen Timeout mit erfolgreicher serverseitiger Festschreibung und einen asynchronen Fehler. Gib einem Entwickler nur die Anforderungs-IDs und bitte ihn, den Ablauf zu rekonstruieren. Wenn er über unabhängige Protokolle hinweg suchen, ein Agentenprotokoll prüfen und raten muss, welche Wiederholung welchen Datensatz erstellt hat, korrigiere die Instrumentierung, bevor du unbeaufsichtigte Schreibvorgänge zulässt.

Die erste zu reparierende Aktion ist meistens der weiteste Schreibendpunkt. Teile ihn in benannte Zustandsänderungen auf, verlange eine Idempotenz-ID und sorge dafür, dass die Antwort die resultierende Ressource identifiziert. Sobald dieser Vertrag existiert, können Agenten schnell handeln, ohne jedes Netzwerkproblem als Erlaubnis zu verstehen, etwas anderes zu versuchen.
