Eine Reporting-Währung aus vielen Shop-Währungen
Ein Shop, der seine Preise in die lokale Währung des Besuchers umschaltet, ist gut für die Conversion und furchtbar fürs Reporting. Dasselbe Produkt verkauft sich an drei Stellen für 99 und landet in deinen Ad-Plattformen als drei verschiedene Zahlen. Beaconry schreibt value und jeden items[].price in eine Zielwährung um, bevor das Event ausfächert, nutzt dafür die Tageskurse der Europäischen Zentralbank, behält die Originalwährung am Event und fällt auf einen robusten Cache zurück, wenn der EZB-Feed ausfällt.
Das Problem: ein Produkt, drei Zahlen
Multi-Currency ist heute Pflichtprogramm für jeden Shop, der über Grenzen hinweg verkauft. Ein WooCommerce-Store mit Währungsumschalter-Plugin oder ein SureCart-Storefront, das den Besucher geolokalisiert, zeigt einem deutschen Käufer EUR, einem britischen Käufer GBP und einem US-Käufer USD. Jeder Checkout feuert ein purchase-Event mit dem Preis in der Währung, in der dieser Käufer bezahlt hat.
Das ist richtig für den Kunden und kaputt fürs Reporting. Drei abgeschlossene Käufe desselben 99-Produkts erreichen deine Ad-Plattformen als:
- EUR-Käufer:
value: 99, currency: "EUR" - GBP-Käufer:
value: 99, currency: "GBP" - USD-Käufer:
value: 99, currency: "USD"
Manche Plattformen rechnen intern für die Anzeige um, manche nicht, und die, die es tun, nutzen ihren eigenen Kurs zu ihrer eigenen Zeit, den du weder sehen noch prüfen kannst. Schlimmer: Wenn eine einzelne Kampagne Käufer über alle drei Währungen hinweg bringt, wird dem Optimizer der Plattform gesagt, dass eine Conversion jeweils "99" wert ist, obwohl 99 GBP rund das 1,18-fache des Werts von 99 EUR sind und 99 USD rund das 0,93-fache. Deine ROAS-Spalte mischt drei Wert-Skalen. Value-based Bidding optimiert gegen Rauschen.
Die Lösung ist, eine Reporting-Währung zu wählen und jedes Geldfeld in sie umzurechnen, bevor das Event deinen Server verlässt. Genau das macht Beaconrys Multi-Currency-Feature, und weil Beaconry server-seitig versendet, passiert die Umrechnung an einer Stelle, die jeden Kanal identisch speist.
Wo es einhakt: ein Filter, vor jedem Kanal
Beaconry fächert ein einziges kanonisches Event an bis zu zehn server-seitige Kanäle aus (GA4, Meta, TikTok, LinkedIn, Google Ads, Microsoft Ads, Pinterest, X Ads, Snapchat, Reddit). Das Fan-out lebt in BCNR_Forwarder::dispatch(), und direkt bevor es das Event pro Kanal aufteilt, läuft ein Filter:
apply_filters( 'bcnr_pre_dispatch_event', $event )Die Multi-Currency-Klasse registriert einen einzigen Listener auf diesem Filter:
// class-bcnr-currency.php
add_filter( 'bcnr_pre_dispatch_event', [ self::class, 'normalize_event' ], 10, 1 );Diese Platzierung ist der ganze Sinn. Die Währungsumrechnung passiert einmal, bevor das Event in zehn Per-Kanal-Payloads kopiert wird. Es gibt keinen Per-Kanal-Währungscode, kein Risiko, dass Meta EUR bekommt, während Google Ads das originale USD bekommt. Jede dispatch_*()-Methode weiter unten liest denselben bereits normalisierten value, currency und items[].price. Füge morgen einen elften Kanal hinzu, und er erbt den normalisierten Wert gratis, ohne einen Währungscode schreiben zu müssen.
Was umgeschrieben wird und was unangetastet bleibt
Der Listener liest die currency_target-Einstellung des Kunden (ein 3-stelliger ISO-Code, oder das BCNR_CURRENCY_TARGET-Konstanten-Override) und schreibt drei Dinge an einem Geld-Event um:
$event['value'], multipliziert mit dem Quelle-zu-Ziel-Kurs.- Jeden
$event['items'][n]['price'], dieselbe Multiplikation pro Line Item. Die Funnel-Events tragen vollständigeitems[]-Arrays (item_id, item_name, price, quantity), also muss der Per-Item-Preis ebenfalls mit umziehen, sonst würde dein GA4-Umsatz auf Item-Ebene mit der Event-Summe nicht übereinstimmen. $event['currency'], auf das Ziel gesetzt, damit jeder nachgelagerte Kanal denselben Code meldet.
Es ist bewusst zurückhaltend dabei, wann es nichts tut. Das Event geht in all diesen Fällen vollkommen unverändert durch:
- Das Feature ist aus (
currency_targetleer). Standardzustand. - Das Event hat keinen
value(einpage_viewoderview_item_listohne Preis ist kein Geld und wird nicht angefasst). - Die Quellwährung fehlt oder entspricht bereits dem Ziel. No-op, außer dass ein wertbehaftetes Event mit leerer Währung den Zielcode aufgestempelt bekommt, damit ein nachgelagerter Kanal nicht auf seinen eigenen "EUR"-Default zurückfällt.
- Die Quellwährung steht nicht in der EZB-Tabelle (ein exotischer Code), in welchem Fall Beaconry den Wert in Ruhe lässt, statt einen Kurs zu raten.
- Die FX-Tabelle ist nach einem fehlgeschlagenen Pull ohne gecachten Fallback leer. Durchreichen plus ein Admin-Hinweis, niemals ein Crash.
"In Ruhe lassen statt raten" ist die leitende Regel. Ein falscher Kurs verfälscht das Umsatz-Reporting still und auf eine Weise, die sehr schwer zu bemerken ist; ein nicht umgerechneter Wert ist zumindest offensichtlich in seiner Originalwährung und erklärt sich selbst.
Woher die Kurse kommen: EZB, EUR-basiert
Die Kursquelle ist der tägliche Referenz-Feed der Europäischen Zentralbank, dasselbe XML, auf dem die halbe europäische Finanzwelt klammheimlich läuft:
https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xmlEr ist öffentlich, kostenlos, braucht keinen API-Key und veröffentlicht rund 30 Hauptwährungen einmal pro Geschäftstag gegen 16:00 CET. Beaconry parst die <Cube currency="USD" rate="1.08"/>-Knoten in eine flache Tabelle von [ 'USD' => 1.08, 'GBP' => 0.84, ... ].
Ein Detail ist für die Korrektheit entscheidend: Die EZB veröffentlicht nur EUR-zu-X-Kurse. Es gibt keine direkte USD-zu-GBP-Zahl im Feed. Also überbrückt eine USD-zu-GBP-Umrechnung über EUR:
// USD -> EUR is 1 / rate[USD], then EUR -> GBP is x rate[GBP]
$to_eur = ( $from === 'EUR' ) ? 1.0 : 1.0 / $rates[ $from ];
$from_eur = ( $to === 'EUR' ) ? 1.0 : $rates[ $to ];
return $to_eur * $from_eur;Wenn eine der beiden Seiten unbekannt ist, gibt die Funktion 0.0 zurück, was der Aufrufer als "nicht umrechnen" liest, und das Event geht unangetastet durch. Umgerechnete Beträge werden mit PHP_ROUND_HALF_UP auf zwei Dezimalstellen gerundet, damit ein Wert wie 1.005 vorhersehbar rundet, statt am Gleitkomma-Glück zu hängen.
Die Zielwährung selbst wird gegen die Live-Tabelle validiert, bevor irgendetwas umgerechnet wird. EUR wird immer akzeptiert (es ist die EZB-Basis). Alles andere muss tatsächlich im abgerufenen Kurs-Set existieren, sodass ein vertipptes currency_target von "USB" zu leer aufgelöst wird und das ganze Feature ein No-op macht, statt jedes Purchase-Event zu zerbrechen.
Der robuste Fallback: EZB-Ausfälle stoppen das Dispatching nicht
Conversion-Tracking, das zusammenbricht, wenn ein vorgelagerter Feed einen schlechten Nachmittag hat, ist schlimmer als gar keine Normalisierung, weil es das ganze Purchase-Event mit herunterreißt. Also hat die Kurs-Tabelle zwei Cache-Schichten und einen harten Durchreich-Boden.
- Transient-Cache (24 h TTL). Der Happy Path. Der EZB-Feed wird höchstens einmal am Tag vom
bcnr_currency_refresh-Cron abgerufen, geparst und imbcnr_fx_rates-Transient gespeichert. Jedes Event während des Tages liest das mit null Netzwerkkosten. - Robuster Option-Fallback. Dieselbe geparste Tabelle wird auch in eine
autoload=false-WP-Option geschrieben,bcnr_fx_rates_fallback. Transients können von einem aggressiven Object-Cache früh verdrängt werden; diese Option kann das nicht. Wenn der Transient weg ist und der EZB-Pull fehlschlägt, liefert Beaconry die letzten guten Kurse aus dieser Option und primt den Transient für eine Stunde neu, damit es während eines Ausfalls nicht bei jedem Event auf die EZB hämmert. - Leere-Tabelle-Boden. Nur wenn es keinen Transient, keinen robusten Fallback und der Live-Pull fehlschlägt (eine brandneue Installation, die noch nie auch nur einmal die EZB erreicht hat), gibt das Feature auf. Es vermerkt eine menschenlesbare Nachricht in
bcnr_fx_last_error, blendet sie als Admin-Hinweis ein und reicht jedes Event in seiner Originalwährung durch. Das Dispatching läuft weiter. Niemand verliert eine Conversion.
Der HTTP-Pull selbst ist defensiv: ein 5-Sekunden-Timeout, SSL-Verifizierung an, eine Non-200 oder ein leerer Body als Fehler behandelt, und das XML mit LIBXML_NONET geparst, sodass selbst die vertrauenswürdige EZB-Quelle keine externe Entität hereinziehen kann. Jeder Parse-Fehler gibt eine leere Tabelle zurück, die direkt in die obige Fallback-Logik führt.
Die Originalwährung wird nie weggeworfen
An Ort und Stelle zu normalisieren würde Information zerstören. Wenn ein deutscher Käufer 99 EUR bezahlt hat und du nur jemals "92,59 USD" speicherst, hast du die Tatsache verloren, dass die echte Transaktion in EUR war. Beaconry behält beides. Nach einer Umrechnung annotiert es das Event mit zwei internen Feldern:
$event['_bcnr_fx_source'] = $source; // e.g. 'USD'
$event['_bcnr_fx_rate'] = $rate; // e.g. 0.9259Diese zwei Felder sind der Audit-Trail. Die Recent-Events-Tabelle des Live-Dashboards kann "92,59 EUR (USD)" statt einer nackten Zahl rendern, und das Betriebs-Log kann den exakten Kurs zeigen, der auf ein gegebenes Event angewendet wurde, sodass eine Umsatz-Diskrepanz im Nachhinein debugbar ist statt eine Blackbox.
Entscheidend: Das _bcnr_-Präfix bedeutet, dass dies nur interne Felder sind. Jede dispatch_*()-Methode leitet nur die dokumentierten Keys weiter, die ihr Kanal erwartet (das Meta-content_*-Set, das GA4-items[]-Set und so weiter). Die _bcnr_fx_source- und _bcnr_fx_rate-Annotationen werden innerhalb von WordPress für Anzeige und Logging gelesen und vor dem Übertragen entfernt. Meta sieht sie nie, GA4 sieht sie nie, sie existieren rein, damit ein Mensch später beantworten kann "welchen Kurs haben wir auf Bestellung 4815 angewendet?".
Was es über Meta, Google und GA4 hinweg tatsächlich aufräumt
Konkret, mit currency_target auf EUR gesetzt und einer Kampagne, die alle drei Käufer vom Anfang dieses Artikels gebracht hat:
- Meta empfängt drei
Purchase-Events, die allevalue: ~92.59, currency: "EUR"lesen (das GBP-Event zu seinem eigenen EUR-Äquivalent, das EUR-Event unverändert). Diecontent_*-Per-Item-Werte stimmen mit der Event-Summe überein, weilitems[].pricemit umgezogen ist. Value-based Bidding und ROAS im Ads Manager vergleichen jetzt Gleiches mit Gleichem. - Google Ads bekommt denselben normalisierten Wert über die Conversions API, sodass die Conversion-Value-Spalte in Google Ads in einer Währung ist, statt still drei zu mischen, die Google dann nach seinem eigenen Zeitplan neu umrechnet.
- GA4 erfasst jeden
purchasein EUR, was bedeutet, dass die Monetization-Reports korrekt summieren, ohne dass du eine Währungsumrechnung in GA4s eigenen Einstellungen einrichtest (und ohne die Rundung, die GA4 anwendet, wenn es für dich umrechnet). Der Umsatz auf Item-Ebene in GA4 stimmt mit dem Event-Umsatz überein, weil beide vom selben Kurs im selben Moment umgerechnet wurden.
Die gemeinsame Eigenschaft über alle drei hinweg ist, dass der Wert einmal, server-seitig, mit einem Kurs, den du sehen kannst, umgerechnet wurde, bevor irgendeine Plattform ins Spiel kam. Du vertraust nicht länger drei verschiedenen Blackbox-Umrechnungen; du schickst drei Plattformen eine konsistente Zahl und behältst den Beleg.
Einschalten
Standardmäßig aus. Aktiviere es über die Multi-Currency-Karte im Advanced-Tab, oder setze die Konstante in wp-config.php für ein Power-User-Override:
define( 'BCNR_CURRENCY_TARGET', 'EUR' );Wähle die Währung, in der du tatsächlich reportest und bietest, was üblicherweise deine Buchhaltungswährung ist, nicht notwendigerweise deine häufigste Checkout-Währung. Danach gibt es nichts Per-Kanal zu konfigurieren. Der tägliche Cron hält die Kurs-Tabelle warm, der robuste Fallback deckt die schlechten Tage der EZB ab, und jedes Geld-Event von WooCommerce, EDD oder SureCart landet in einer Währung in deinen Ad-Plattformen.
Zum Verifizieren: Schließe einen Test-Kauf in einer Nicht-Zielwährung ab und prüfe die Recent-Events-Tabelle im Live-Dashboard. Ein normalisiertes Event rendert den Zielwert mit der Quellwährung in Klammern, zum Beispiel "92,59 EUR (USD)". Wenn du die Originalwährung unverändert siehst, prüfe den Admin-Hinweis-Bereich auf die EZB-Fallback-Nachricht und bestätige, dass dein currency_target ein Code ist, der tatsächlich im EZB-Feed auftaucht.
Fazit
Multi-Currency-Shops haben kein Preis-Problem, sie haben ein Reporting-Problem: Dasselbe Produkt erreicht deine Ad-Plattformen als mehrere inkompatible Zahlen. Beaconry löst es an der einen Stelle, die jeden Kanal speist, dem bcnr_pre_dispatch_event-Filter, indem es value und jeden items[].price mit EZB-Tageskursen in eine Zielwährung umrechnet, vor dem Fan-out. Es behält die Originalwährung und den angewendeten Kurs am Event, damit die Rechnung prüfbar ist, es überbrückt Cross-Currency-Paare über EUR, weil die EZB nur das veröffentlicht, und es hat einen Zwei-Schichten-Cache mit hartem Durchreich-Boden, sodass ein EZB-Ausfall dich für einen Tag Genauigkeit kostet, niemals eine einzige getrackte Conversion. Eine Reporting-Währung, ein Kurs, den du sehen kannst, keine Blackbox-Umrechnungen in drei verschiedenen Plattformen.