Das Problem
Eine fehlgeschlagene Zahlung ist die unangenehmste Sorte Bestellung: Sie existiert, sie blockiert Bestand, sie taucht in Auswertungen auf — aber es ist kein Geld geflossen. Und in den meisten Shops passiert danach: nichts. Die Bestellung liegt auf Zahlungsstatus „Fehlgeschlagen", bis jemand sie zufällig sieht oder ein Cronjob sie irgendwann storniert.
In einem Shop, den wir betreuen, waren das in zwölf Monaten 24 Bestellungen. Neun davon haben sich von selbst erledigt — der Kunde hat es nochmal versucht und bezahlt. Der große Rest wurde irgendwann storniert. Wie viele davon nach einer freundlichen Erinnerung doch noch bezahlt worden wären, weiß niemand, weil nie eine rausging.
Seit Shopware 6.7.14 geht zumindest die erste Mail automatisch raus. Das ist der Anfang eines Prozesses, nicht der Prozess.
Was 6.7.14 tatsächlich mitbringt
Die Release Notes formulieren es knapp: Shopware liefert jetzt einen Standard-Flow, ein Flow-Template und ein Mail-Template für das Event state_enter.order_transaction.state.failed.
Wenn du nach dem Update in den Flow Builder schaust (der übrigens mit 6.7.14 aus den Einstellungen in ein eigenes Hauptmenü Automatisierung umgezogen ist), findest du einen Flow namens „Payment enters status failed". Ausgepackt besteht er aus genau einer Sequenz:
{
"name": "Payment enters status failed",
"eventName": "state_enter.order_transaction.state.failed",
"active": true,
"sequences": [
{
"actionName": "action.mail.send",
"config": {
"recipient": { "type": "default" },
"mailTemplateId": "…"
}
}
]
}
Das dazugehörige Mail-Template hat den Typ order_transaction.state.failed, den Betreff „Die Zahlung für Ihre Bestellung ist fehlgeschlagen" und einen Zweizeiler als Inhalt: Zahlung fehlgeschlagen, bitte Zahlungsdaten prüfen oder im Kundenkonto eine andere Zahlungsart wählen.
Zwei Dinge dazu, die man wissen sollte:
Der Flow ist nach dem Update aktiv. Niemand fragt dich, ob du das willst. Wenn du eine eigene Lösung für fehlgeschlagene Zahlungen hast, schickst du ab dem Update zwei Mails. Erster Handgriff nach dem Update: Flow-Liste durchsehen.
Als Variablen stehen im Template nur order, salesChannel, previousState und newState zur Verfügung. Das ist der Umfang, den der Mail-Template-Typ deklariert — kein Transaktions-Objekt, kein Fehlergrund vom Zahlungsanbieter. Für „Ihre Kreditkarte wurde abgelehnt, Grund X" reicht das nicht.
Ebenfalls neu in 6.7.14 und für alles Folgende wichtig: Der Statusverlauf einer Bestellung zeigt jetzt, wer einen Statuswechsel ausgelöst hat. Ändert ein Kunde den Status über eine Aktion in der Storefront, steht dort „Customer" statt pauschal „System". Änderungen eines Administrators werden dem konkreten Benutzer zugeordnet.
Wo der Flow endet
Der Flow ist ereignisbasiert. Er feuert in dem Moment, in dem die Transaktion auf failed springt, genau einmal, und dann ist er fertig. Was er strukturell nicht kann:
- Nachfassen. „Wenn nach drei Tagen immer noch nicht bezahlt ist, erinnere nochmal." Zeitverzögerte Flow-Aktionen gibt es nur in den kommerziellen Plänen oder per Drittanbieter-Plugin. In der Open-Source-Version endet die Automatisierung beim Event.
- Den Zustand nachprüfen. Der Flow weiß nicht, ob der Kunde zehn Minuten später erfolgreich bezahlt hat.
- Alles, was nicht Mail ist. Lagerreservierung auflösen, Lieferantenbestellung stoppen, ab Warenkorbwert X eine Aufgabe ins Team-Postfach legen, den Fall nach sieben Tagen stornieren.
Genau da fängt n8n an. Der Flow macht die Sofortreaktion, n8n macht den Prozess drumherum. Der Rest dieses Beitrags ist der zweite Teil: eine geplante Zahlungserinnerung, die nicht doppelt rausgeht.
Der Zugang: eine Integration, kein Admin-Benutzer
n8n spricht mit der Admin API über eine Integration (Einstellungen → System → Integrationen), nicht über einen Benutzer-Login. Client-ID und Secret gegen /api/oauth/token tauschen:
POST /api/oauth/token
Content-Type: application/json
{
"grant_type": "client_credentials",
"client_id": "SWIA…",
"client_secret": "…"
}
Die Rolle der Integration braucht order:read, order:update und — wenn Shopware die Mail verschicken soll — das Recht api_send_email. Mehr nicht.
Der schöne Nebeneffekt hängt direkt an der neuen 6.7.14-Funktion: Jede Statusänderung, die n8n über diese Integration auslöst, landet in state_machine_history mit gesetzter integrationId. Ändert ein Kollege den Status im Admin, steht dort eine userId. Ist beides leer, war es der Kunde oder das System. Zum ersten Mal ist damit sauber auswertbar, welcher Anteil eurer Statuswechsel automatisiert läuft.
Die Abfrage — und der Denkfehler darin
Naheliegend ist eine Suche über POST /api/search/order mit einem Filter auf den Zahlungsstatus:
{
"limit": 100,
"filter": [
{
"type": "equals",
"field": "transactions.stateMachineState.technicalName",
"value": "failed"
},
{
"type": "not",
"operator": "and",
"queries": [
{
"type": "equals",
"field": "stateMachineState.technicalName",
"value": "cancelled"
}
]
},
{
"type": "range",
"field": "orderDateTime",
"parameters": { "gte": "2026-09-02T00:00:00+00:00", "lte": "2026-09-13T00:00:00+00:00" }
}
],
"associations": {
"transactions": {
"sort": [{ "field": "createdAt", "order": "ASC" }],
"associations": { "stateMachineState": {} }
},
"stateMachineState": {},
"orderCustomer": {}
}
}
Kleine Korrektur vorweg, über die viele stolpern: Der Bestellstatus kennt gar kein „Fehlgeschlagen". Die Statusmaschine order.state hat exakt vier Zustände — open, in_progress, completed, cancelled. „Fehlgeschlagen" existiert nur in der Zahlungsstatus-Maschine order_transaction.state. Der Ausschluss, den du willst, heißt also cancelled.
Und jetzt der Punkt, an dem die Abfrage kippt: transactions.stateMachineState.technicalName filtert über eine Assoziation. Getroffen wird jede Bestellung, die irgendeine Transaktion mit diesem Status hat — nicht die, deren aktuelle Transaktion fehlgeschlagen ist.
Jeder neue Zahlungsversuch legt in Shopware eine neue order_transaction an. Die alte, fehlgeschlagene bleibt stehen. Eine Bestellung, bei der der Kunde beim zweiten Anlauf erfolgreich per PayPal bezahlt hat, sieht in der Datenbank so aus:
order B-10xxx
├── transaction 2026-01-12 12:29 → failed
└── transaction 2026-01-12 12:32 → paid
Und sie steht in deinem Suchergebnis. Für immer.
In dem Shop aus dem Beispiel oben liefert die Abfrage ohne Zeitfenster 24 Bestellungen. Davon:
- bei 13 ist die jüngste Transaktion gar nicht mehr
failed— neun davon sind schlicht bezahlt, - von den verbleibenden 11 sind 10 längst storniert,
- übrig bleibt genau eine Bestellung, bei der eine Zahlungserinnerung fachlich richtig wäre.
24 Treffer, 1 echter Fall. Wer diese Liste ungefiltert in einen Mail-Knoten kippt, schreibt neun Kunden an, die bezahlt haben.
Ein DAL-Filter auf „die letzte Transaktion" existiert nicht. Deshalb: Assoziation sortiert mitladen (siehe associations oben) und in n8n nachfiltern.
// Code-Node: nur Bestellungen, deren jüngste Transaktion fehlgeschlagen ist
return items.filter(({ json: order }) => {
const latest = [...(order.transactions ?? [])]
.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
.at(-1)
return latest?.stateMachineState?.technicalName === 'failed'
})
Das ist dieselbe Konvention, die Shopware intern verwendet: Die zuletzt angelegte Transaktion ist die gültige.
Der Merker: warum es ohne nicht geht
Der Workflow läuft nach Zeitplan — sagen wir stündlich. Ohne Gedächtnis schreibt er denselben Kunden bei jedem Lauf an, solange die Bestellung im Zeitfenster liegt. Es braucht also eine Notiz „diesem Kunden haben wir am … geschrieben".
Variante A: Custom Field an der Bestellung
Ein Custom-Field-Set auf der Entität order, zwei Felder:
| Technischer Name | Typ | Zweck |
|---|---|---|
nvx_dunning_reminded_at | Datum/Uhrzeit | Zeitpunkt der letzten Erinnerung |
nvx_dunning_reminder_count | Zahl | Anzahl bisheriger Erinnerungen |
Geschrieben wird mit einem simplen PATCH:
PATCH /api/order/{orderId}
Content-Type: application/json
{
"customFields": {
"nvx_dunning_reminded_at": "2026-09-16T09:00:00+00:00",
"nvx_dunning_reminder_count": 1
}
}
Wichtig und oft falsch verstanden: Shopware ersetzt das customFields-Objekt bei einer Aktualisierung nicht, sondern schreibt die übergebenen Schlüssel einzeln per JSON-Update in das bestehende Objekt. Custom Fields anderer Plugins an derselben Bestellung bleiben also unangetastet. Nur "customFields": null löscht alles.
Vorteil dieser Variante: Der Merker ist im Admin sichtbar, überlebt jeden n8n-Umzug und lässt sich direkt in die Suchabfrage aufnehmen:
{
"type": "equals",
"field": "customFields.nvx_dunning_reminded_at",
"value": null
}
Variante B: Data Table in n8n
n8n bringt mit den Data Tables eine eigene Tabelle mit. Eine Tabelle payment_reminders mit den Spalten order_id, order_number, reminded_at, count, geschrieben über die Operation Upsert mit order_id als Schlüssel — fertig.
Vorteil: kein Eingriff in den Shop, keine Schema-Änderung, funktioniert auch, wenn die Integration nur Leserechte hat.
Wann welche? Wenn du den Shop betreust: Custom Field. Der Zustand gehört fachlich zur Bestellung, und der Kundenservice sieht im Admin, dass bereits erinnert wurde, statt hinterher zu raten. Wenn du nur einen API-Zugang von außen hast oder mehrere Shops in einem Workflow bedienst: Data Table.
Reihenfolge und Fehlerfall
Den Merker nach dem erfolgreichen Versand schreiben, nicht davor — sonst verlierst du die Erinnerung stillschweigend, wenn der Mailversand scheitert. Dafür brauchst du zwei Absicherungen:
- In n8n ein Error Workflow hinterlegen, damit ein abgebrochener Lauf nicht unbemerkt bleibt.
- Den Zeitplan länger takten als die Laufzeit des Workflows. Zwei parallel laufende Instanzen sehen beide „noch nicht erinnert" und schreiben beide.
Die Mail: n8n oder Shopware?
Der SMTP-Knoten in n8n ist schneller gebaut. Er hat aber einen Nachteil: Der Text liegt dann in n8n, und wer im Shop den Ton der Kundenkommunikation pflegt, kommt nicht mehr dran.
Die sauberere Variante lässt Shopware senden — mit einem echten Mail-Template, das im Admin bearbeitbar ist:
POST /api/_action/mail-template/get-data-and-send
Content-Type: application/json
{
"mailTemplateId": "…",
"salesChannelId": "{{ $json.salesChannelId }}",
"recipients": {
"{{ $json.orderCustomer.email }}": "{{ $json.orderCustomer.firstName }} {{ $json.orderCustomer.lastName }}"
},
"entities": {
"order": "{{ $json.id }}"
}
}
Diese Route lädt die Entitäten anhand der übergebenen IDs selbst, rendert das Template damit und verschickt es. Die erlaubten Schlüssel unter entities bestimmt der Typ des Mail-Templates — beim Typ order_transaction.state.failed sind das order, salesChannel, previousState und newState. Für eine eigene Erinnerungsmail legst du am besten ein eigenes Template desselben Typs an; dann hast du {{ order.orderNumber }}, {{ order.amountTotal }} und die Kundendaten im Zugriff, ohne etwas selbst zusammenbauen zu müssen.
Über salesChannelId bekommt die Mail außerdem Absendername und Branding des richtigen Verkaufskanals — bei Multi-Channel-Setups kein Detail.
Stolperfalle: „Erinnert" ist kein gangbarer Weg
Naheliegender Gedanke: Shopware hat doch den Zahlungsstatus „Erinnert" (reminded) — warum nicht den als Merker setzen? Das wäre im Admin sichtbar und käme ganz ohne Custom Field aus.
Weil es die Statusmaschine nicht erlaubt. Die Transition remind existiert nur aus zwei Zuständen heraus:
| Von | Aktion | Nach |
|---|---|---|
open | remind | reminded |
paid_partially | remind | reminded |
Aus failed heraus gibt es sie nicht. Von failed kommst du nur nach open (reopen), in_progress (do_pay, process), paid, paid_partially, unconfirmed — oder wieder nach failed. Der Status „Erinnert" ist für den Vorkasse-/Rechnungsfall gedacht, nicht für gescheiterte Transaktionen.
Wer es trotzdem probiert, bekommt einen Fehler der Statusmaschine — was ehrlich gesagt besser ist als ein stiller Fehlschlag.
Was die neue Auslöser-Info im Alltag bringt
Zurück zum zweiten Teil des 6.7.14-Updates, denn er ist mehr als Kosmetik. Die Tabelle state_machine_history hält pro Statuswechsel fest:
userId— ein Administrator hat den Status geändert,integrationId— eine Integration oder App war es, also zum Beispiel euer n8n,- beides leer — Kunde oder System,
internalComment— der interne Kommentar aus dem Status-Dialog, der seit 6.7.8 im Admin erfasst werden kann und für Kunden unsichtbar bleibt,- dazu
transitionActionName,fromStateId,toStateIdundcreatedAt.
Damit lässt sich die Frage beantworten, an der solche Automatisierungen sonst scheitern: Hat sie etwas gebracht? Eine Abfrage auf state_machine_history mit entityName = order_transaction beantwortet, wie viele fehlgeschlagene Zahlungen der Kunde selbst geheilt hat, wie viele ein Mitarbeiter von Hand nachgezogen hat und wie viele auf das Konto der Automatisierung gehen. Vor 6.7.14 war das eine einzige graue Masse namens „System".
Fazit
Shopware 6.7.14 nimmt dir den einfachen Teil ab: die Sofort-Mail bei fehlgeschlagener Zahlung, ohne Konfiguration, direkt nach dem Update. Prüf nach dem Update, ob der Flow aktiv sein soll, und schau dir den Standardtext an — er ist bewusst nüchtern.
Der Rest ist ein Prozess, kein Ereignis, und der gehört in ein Werkzeug, das Zeit, Zustand und Gedächtnis kann. Die drei Stellen, an denen selbstgebaute Lösungen erfahrungsgemäß kaputtgehen, sind:
- Der Assoziationsfilter, der jede jemals fehlgeschlagene Transaktion trifft statt der aktuellen.
- Der fehlende Merker, der aus einer Erinnerung eine Mailserie macht.
- Die Annahme, der Zahlungsstatus „Erinnert" ließe sich als Merker missbrauchen.
Alle drei kosten wenig Zeit, wenn man sie vorher kennt — und Kundenvertrauen, wenn nicht.
Wenn ihr so etwas für euren Shop aufsetzen wollt und lieber jemanden dabeihaben möchtet, der die Stolperfallen schon gefunden hat: Meldet euch.



