Alle Artikel
Deep dive8 Min. Lesezeit

Sowohl Apple als auch Google können dieselbe Erstattung mehr als einmal an Ihren Server liefern, und doppelte Erstattungsbenachrichtigungen kosten Sie Geld, wenn Sie auf jede einzelne reagieren

Apple wiederholt eine Erstattungsbenachrichtigung bis zu fünfmal und Google Play setzt auf Pub/Sub mit At-least-once-Zustellung, sodass dieselbe Erstattung Ihren Server mehr als einmal erreichen kann. So behandeln Sie doppelte Erstattungsbenachrichtigungen, ohne ein Guthaben doppelt abzuziehen oder API-Kontingent zweimal zu verbrennen.

Viele identische Papierumschläge auf einem dunklen Schreibtisch gestapelt, einer davon beiseitegezogen, als Sinnbild für doppelte Erstattungsbenachrichtigungen, die an Ihrem Server eintreffen

Wichtigste Erkenntnisse

  • Apple wiederholt eine App Store Server Notification V2 fünfmal, nach 1, 12, 24, 48 und 72 Stunden nach dem letzten Versuch, immer dann, wenn Ihr Server nicht mit einem HTTP-Status zwischen 200 und 206 antwortet. Zählt man den ersten Versuch mit, kann eine Erstattung bis zu sechsmal eintreffen.
  • Jeder echte Apple-Retry trägt dieselbe notificationUUID, daher ist dieses Feld, nicht die Transaktions-ID, Ihr Deduplizierungsschlüssel.
  • Die Real-time Developer Notifications von Google Play setzen auf Cloud Pub/Sub, das At-least-once-Zustellung und keine Reihenfolge garantiert, sodass dieselbe Nachricht zweimal oder in falscher Reihenfolge eintreffen kann. Google weist Sie an, die messageId auf Eindeutigkeit zu prüfen, bevor Sie irgendetwas verarbeiten.
  • Apples periodische CONSUMPTION_REQUEST-Benachrichtigungen sind keine Retries. Apple sendet über das offene Erstattungsfenster hinweg immer neue, jede mit einer anderen notificationUUID, sodass eine Deduplizierung anhand der notificationUUID jede einzelne korrekt beibehält.
  • Dieselbe Apple-transactionId kann mehr als eine Entscheidung tragen, zum Beispiel ein REFUND_DECLINED gefolgt später von einem REFUND, sodass eine Deduplizierung allein anhand der Transaktions-ID ein eigenständiges Ereignis wegwirft, das Sie gebraucht hätten.
  • Eine Duplikat-Ablehnung durch die Rückgabe eines 4xx oder 5xx sorgt nur dafür, dass der Store es erneut sendet. Deduplizieren Sie in Ihrer eigenen Datenbank und geben Sie immer einen Erfolgsstatus zurück.
  • Ein Erstattungs-Handler, der nicht idempotent ist, handelt bei der zweiten Zustellung doppelt. Er zieht ein Guthaben zweimal ab, storniert eine Auszahlung zweimal oder verbrennt abrechenbares Play Developer API- und App Store Server API-Kontingent, indem er eine Erstattung erneut prüft, die er bereits abgeschlossen hat.

Ihr Server wird dasselbe Erstattungsereignis mehr als einmal empfangen, und beide Stores haben das absichtlich so entworfen. Apple wiederholt eine App Store Server Notification bis zu fünfmal, wenn Ihr Endpunkt nicht sauber antwortet. Google Play liefert seine Real-time Developer Notifications über Cloud Pub/Sub, das At-least-once-Zustellung verspricht und nichts über die Reihenfolge. Die Frage ist also nie, ob ein Duplikat eintrifft. Sie lautet, was Ihr Code tut, wenn er dieselbe Erstattung zum zweiten Mal sieht. Machen Sie das falsch, ziehen Sie ein Guthaben zweimal ab, stornieren eine Auszahlung zweimal oder verbrennen abrechenbares API-Kontingent, indem Sie eine bereits abgeschlossene Erstattung erneut prüfen. So erreichen doppelte Erstattungsbenachrichtigungen Sie tatsächlich, welche Wiederholungen echte Duplikate sind und welche nur so aussehen, und wie Sie sie behandeln, damit die zweite Zustellung kostenlos ist.

Eine Erstattungsbenachrichtigung wird mindestens einmal zugestellt, was nicht dasselbe ist wie genau einmal

Beide Stores behandeln eine zugestellte Benachrichtigung als ein Versprechen, das sie immer wieder zu erfüllen versuchen, nicht als einen einzigen Schuss, den sie abfeuern und vergessen. Das ist gut für die Zuverlässigkeit, denn eine Benachrichtigung, die Sie während eines Deployments verpassen, erreicht Sie später trotzdem. Es ist eine Falle für die Korrektheit, denn der Mechanismus, der garantiert, dass Sie das Ereignis irgendwann erhalten, garantiert auch, dass Sie es manchmal zweimal erhalten. Ihr Handler muss idempotent sein, das heißt, die zweite und dritte Zustellung einer Erstattung ändern nichts, was die erste nicht bereits geändert hat.

Apple wiederholt fünfmal über drei Tage

Wenn Apple eine App Store Server Notification V2 sendet, erwartet es, dass Ihr Server mit einem HTTP-Status im Bereich 200 bis 206 antwortet. Alles andere, ein 4xx oder ein 5xx, sagt Apple, dass die Zustellung fehlgeschlagen ist, und Apple wiederholt. Der Zeitplan ist fest: fünf Retries, nach 1, 12, 24, 48 und 72 Stunden nach dem vorherigen Versuch. Zählt man den ersten Versuch mit, kann ein Erstattungsereignis bis zu sechsmal eintreffen, verteilt über ungefähr eine Woche. Jeder dieser Retries trägt dieselbe notificationUUID. Dieses Feld ist Ihr Deduplizierungsschlüssel. Wenn Sie eine notificationUUID bereits aufgezeichnet haben, ist die Zustellung, die Sie gerade halten, eine Wiederholung, und die korrekte Antwort ist, nichts Neues zu speichern und trotzdem 200 zurückzugeben.

Google Play setzt auf Pub/Sub, das At-least-once verspricht und nichts über die Reihenfolge sagt

Die Real-time Developer Notifications von Google Play werden an ein Cloud Pub/Sub-Topic veröffentlicht. Die Zustellgarantie von Pub/Sub ist At-least-once, und es gibt überhaupt keine Reihenfolgegarantie. Das bedeutet, dieselbe Nachricht kann mehr als einmal an Ihren Endpunkt zugestellt werden, und zwei Nachrichten für denselben Kauf können in falscher Reihenfolge eintreffen. Googles eigene Anleitung ist eindeutig: entpacken Sie das base64-data-Feld, lesen Sie die messageId und prüfen Sie, dass Sie sie noch nicht gesehen haben, bevor Sie irgendetwas verarbeiten. Eine doppelte messageId ist eine Wiederholung, die Sie überspringen. Zwei verschiedene Benachrichtigungen über einen Kauf sollten trotzdem auf demselben Datensatz landen, also verankern Sie Ihren gespeicherten Zustand auch am purchaseToken und lassen Sie ein späteres Ereignis die Zeile aktualisieren, die ein früheres erstellt hat.

PlattformZustellmodellDeduplizieren anhandErfolgssignalWenn Sie nicht bestätigen
App Store Server Notifications V2Bis zu 6 Versuche: der erste, plus 5 Retries nach 1, 12, 24, 48, 72 StundennotificationUUIDHTTP 200 bis 206Apple wiederholt nach dem festen Zeitplan, dann hört es auf
Google Play RTDN über Pub/SubAt-least-once, keine ReihenfolgegarantiePub/Sub messageId, entitätsbezogen am purchaseTokenHTTP 200 auf den Push, oder ein explizites AckPub/Sub sendet erneut, nachdem die Ack-Frist abgelaufen ist

Die Wiederholungen, die keine Duplikate sind

Nicht jede Benachrichtigung, die wie eine bereits gesehene aussieht, ist ein Retry. Zwei Apple-Verhaltensweisen senden echt neue Ereignisse, die einen Kauf teilen, aber jeweils verarbeitet werden müssen, und sie mit einer naiven Deduplizierung zusammenzufassen wirft Informationen weg, die Sie gebraucht hätten.

Apple sendet frische CONSUMPTION_REQUESTs, keine Retries

Während einer offenen Erstattungsanfrage für ein Verbrauchsgut sendet Apple nicht eine einzige CONSUMPTION_REQUEST und wartet. Es sendet über das gesamte offene Erstattungsfenster hinweg periodisch neue, bis die Erstattung abgeschlossen ist. Apple-Mitarbeiter haben bestätigt, dass dies keine Retries sind, und das verräterische Zeichen ist das Feld, anhand dessen Sie deduplizieren: jede frische CONSUMPTION_REQUEST trägt eine andere notificationUUID. Eine Deduplizierung, die an der notificationUUID verankert ist, tut also automatisch das Richtige. Sie fasst echte Retries zusammen und behält jede eigenständige Aufforderung. Was Sie nicht tun dürfen, ist eine Deduplizierung anhand von Transaktions-ID und Benachrichtigungstyp, denn das würde jede CONSUMPTION_REQUEST nach der ersten zum Schweigen bringen und Sie das 12-Stunden-Beweisfenster bei den fallengelassenen kosten.

Eine Transaktion kann mehr als eine Entscheidung tragen

Eine einzelne transactionId kann über ihre Lebensdauer mehr als ein Erstattungsergebnis erzeugen. Apple kann ein REFUND_DECLINED senden und dann, später, ein REFUND für dieselbe Transaktion, und Entwickler berichten, drei oder mehr erstattungsbezogene Benachrichtigungen für eine Transaktions-ID zu erhalten. Jede ist ein eigenständiges Ereignis mit ihrer eigenen notificationUUID. Wenn Ihr Deduplizierungsschlüssel die Transaktions-ID ist, sieht die zweite Entscheidung wie ein Duplikat der ersten aus, und Sie erfahren nie, dass die Erstattung letztlich gewährt wurde. Die Transaktions-ID gruppiert Ereignisse. Sie identifiziert sie nicht.

Ein Robotergreifer, der ein doppeltes Paket von einer Förderlinie in einen Seitenbehälter hebt, ein Bild als Sinnbild für das Deduplizieren wiederholter Erstattungsbenachrichtigungen

Was ein Duplikat Sie tatsächlich kostet

Eine Erstattungsbenachrichtigung ist keine Statusanzeige. Sie löst echte Aktionen aus: Sie entziehen Zugriff, Sie ziehen ein Verbrauchsgut-Guthaben ab, Sie stornieren eine Creator-Auszahlung, Sie rufen die App Store Server API oder die Play Developer API auf, um den Zustand zu bestätigen. Führen Sie eine dieser Aktionen bei einem Duplikat ein zweites Mal aus, und die Kosten sind real.

Folgen Sie dem Geld. Zweimal Zugriff zu entziehen ist harmlos, denn der Zugriff ist bereits weg. Zweimal ein Guthaben abzuziehen ist es nicht: ein Nutzer, der ein Münzpaket gekauft und erstattet hat, kann auf ein negatives Guthaben getrieben werden, das Ihr Support-Team dann von Hand auflösen muss. Zweimal eine Auszahlung zu stornieren zieht Geld zurück, das Sie bereits einmal zurückgegeben haben, und nun schulden Sie einem Creator eine Entschuldigung und eine Korrektur. Und jedes Duplikat, das Sie erneut gegen eine Store-API verarbeiten, verbraucht Kontingent, das Google Sie ausdrücklich zu schützen mahnt, sodass ein Schwall von Pub/Sub-Neuzustellungen während eines Ausfalls Sie an genau dem Tag ins Rate-Limiting treiben kann, an dem Sie es sich am wenigsten leisten können.

Die Erstattungs-Beweisfenster erhöhen den Einsatz auf der Apple-Seite. Wenn eine naive Deduplizierung die wiederholten CONSUMPTION_REQUESTs zum Schweigen bringt, die Apple über das offene Erstattungsfenster sendet, können Sie diejenige verpassen, die Sie beantworten mussten, und eine CONSUMPTION_REQUEST, die Sie nicht innerhalb von 12 Stunden beantworten, ist eine Erstattung, die Apple oft standardmäßig gewährt. Das ist keine Doppelbelastung. Es ist ein verlorener Verkauf plus die Rechenleistung, API-Aufrufe, Speicherung und Auszahlungen, die Sie bereits für die Lieferung des Kaufs ausgegeben haben, von denen die Erstattung keine zurückbringt.

FehlermodusWas schiefgehtWas es kostet
Ein Guthaben bei einem doppelten REFUND erneut abziehenDas Verbrauchsgut-Guthaben des Nutzers wird negativManuelle Support-Zeit zum Abgleich und eine schlechte Kundenerfahrung
Eine Auszahlung zweimal stornierenSie ziehen Geld zurück, das Sie bereits einmal zurückgegeben habenEine Creator-Korrektur und eine Buchhaltungsbereinigung
Erneut gegen eine Store-API verarbeitenDoppelte Aufrufe verbrennen Play Developer- oder App Store Server API-KontingentRate-Limiting während des Ausfalls, der die Neuzustellung verursacht hat
CONSUMPTION_REQUESTs über-deduplizierenSie lassen eine eigenständige Erstattungsaufforderung als falsches Duplikat fallenEin verpasstes 12-Stunden-Fenster, sodass Apple die Erstattung standardmäßig gewährt

Wie Sie doppelte Erstattungsbenachrichtigungen behandeln, ohne doppelt zu handeln

Das Muster ist auf beiden Stores dasselbe, mit einem anderen Schlüssel. Zeichnen Sie die Zustellung auf, prüfen Sie den Schlüssel, bevor Sie handeln, handeln Sie einmal, und sagen Sie dem Store immer, dass Sie sie empfangen haben.

  • Deduplizieren Sie anhand der Zustell-ID des Stores, nicht der Transaktion. Verwenden Sie notificationUUID für Apple und die Pub/Sub-messageId für Google Play. Speichern Sie sie mit einer Unique-Constraint, sodass ein gleichzeitiges Duplikat das Rennen verliert, statt doppelt zu handeln.
  • Machen Sie die nachgelagerte Aktion nach ihren eigenen Maßstäben idempotent. Die Verankerung an der Zustell-ID stoppt die erneute Verarbeitung, aber schreiben Sie auch den Effekt so, dass das Entziehen, Abziehen oder Stornieren zuerst den aktuellen Zustand prüft und sicher zweimal ausgeführt werden kann.
  • Erst persistieren, dann bestätigen. Schreiben Sie das Ereignis in Ihre Datenbank, bevor Sie 200 zurückgeben oder die Pub/Sub-Nachricht bestätigen. Wenn Sie zuerst bestätigen und das Schreiben fehlschlägt, betrachtet der Store die Nachricht als zugestellt und sendet sie nie wieder, und nun haben Sie sie endgültig verloren.
  • Geben Sie immer einen Erfolgsstatus zurück, auch für ein Duplikat. Ein 200 bis 206 für Apple, ein 200 auf den Pub/Sub-Push für Google. Eine Wiederholung mit einem Fehler abzulehnen sorgt nur dafür, dass der Store sie erneut sendet.
  • Gruppieren Sie an der Entität, identifizieren Sie am Ereignis. Verankern Sie Ihren gespeicherten Kaufzustand am purchaseToken oder originalTransactionId, sodass Zustellungen in falscher Reihenfolge eine Zeile aktualisieren, aber behandeln Sie jede notificationUUID oder messageId als eigenes Ereignis, denn ein Kauf erzeugt berechtigterweise mehrere.

Eine kurze Checkliste, bevor Sie Ihrem Erstattungs-Webhook vertrauen

  • Apple-Zustellungen werden anhand der notificationUUID dedupliziert, und eine Wiederholung schreibt nichts Neues, gibt aber trotzdem 200 zurück.
  • Google Play-Zustellungen werden anhand der Pub/Sub-messageId dedupliziert, geprüft vor jeder Verarbeitung.
  • Der Kaufzustand ist am purchaseToken oder originalTransactionId verankert, sodass Ereignisse in falscher Reihenfolge auf einem Datensatz landen.
  • Jeder Erstattungs-Nebeneffekt, Entziehen, Abziehen oder Stornieren, ist sicher mehr als einmal ausführbar.
  • Ihr Handler schreibt das Ereignis, bevor er bestätigt, niemals danach.
  • Wiederholte CONSUMPTION_REQUESTs werden als eigenständige Aufforderungen behandelt, nicht als Duplikate, sodass kein offenes Erstattungsfenster fallengelassen wird.

Schleusen Sie absichtlich ein Duplikat durch Ihren eigenen Webhook und beobachten Sie, wie es beim zweiten Mal nichts ändert. Das ist der ganze Test. Ein Erstattungs-Handler, der sicher zweimal getroffen werden kann, ist einer, um den Sie sich nicht mehr sorgen müssen, sobald ein Store beschließt, ihn sechsmal zu treffen.

Häufig gestellte Fragen

Warum empfängt mein Server dieselbe App Store-Erstattungsbenachrichtigung mehr als einmal?
Weil Apple eine App Store Server Notification V2 bis zu fünfmal wiederholt, nach 1, 12, 24, 48 und 72 Stunden nach dem letzten Versuch, immer dann, wenn Ihr Server nicht mit einem HTTP-Status zwischen 200 und 206 antwortet. Jeder Retry trägt dieselbe notificationUUID, sodass Sie ihn erkennen und überspringen können.
Welches Feld sollte ich verwenden, um App Store Server Notifications zu deduplizieren?
Verwenden Sie die notificationUUID. Ein echter Retry wiederholt immer dieselbe notificationUUID, während jedes echt neue Ereignis, einschließlich jeder frischen CONSUMPTION_REQUEST, eine andere erhält, sodass eine Deduplizierung anhand der notificationUUID Wiederholungen überspringt, ohne eigenständige Ereignisse fallenzulassen.
Sind wiederholte CONSUMPTION_REQUEST-Benachrichtigungen Duplikate, die ich ignorieren sollte?
Nein. Apple sendet periodisch neue CONSUMPTION_REQUEST-Benachrichtigungen über das offene Erstattungsfenster, und Apple-Mitarbeiter bestätigen, dass dies keine Retries sind. Jede hat ihre eigene notificationUUID, verarbeiten Sie also jede einzelne. Sie fallenzulassen riskiert, das 12-Stunden-Fenster zu verpassen, das Apple Ihnen zur Antwort gibt.
Wie dedupliziere ich Google Play Real-time Developer Notifications?
Lesen Sie die Pub/Sub-messageId aus jeder Benachrichtigung und prüfen Sie sie gegen die bereits verarbeiteten, bevor Sie handeln, denn Pub/Sub liefert At-least-once und kann dieselbe Nachricht mehr als einmal senden. Google empfiehlt dies ausdrücklich, um doppelte Verarbeitung und verschwendetes API-Kontingent zu vermeiden.
Sollte ich einen Fehler zurückgeben, um eine doppelte Erstattungsbenachrichtigung abzulehnen?
Nein. Die Rückgabe eines 4xx oder 5xx sagt dem Store, dass die Zustellung fehlgeschlagen ist, sodass er trotzdem wiederholt. Deduplizieren Sie in Ihrer eigenen Datenbank und geben Sie immer einen Erfolgsstatus zurück, HTTP 200 bis 206 für Apple oder eine 200-Bestätigung für den Google Play-Push.

Quellen und weiterführende Informationen

RefundHalt

Der Rückerstattungs-Autopilot für App Store und Google Play

Weiterlesen

Die nächste Rückerstattungsanfrage ist bereits unterwegs.

Richten Sie RefundHalt in der Zeit ein, die Sie brauchen, um eine weitere Support-E-Mail über eine Rückerstattung zu lesen, die Sie nicht anfechten konnten.