Microsoft-Ads-Conversions ohne das Developer-Token-Warten
Microsofts OfflineConversions-API sperrt produktive Conversion-Uploads hinter ein Developer-Token-Review, dieselbe mehrwöchige Reibung wie die Google-Ads-API. Beaconry leitet Microsoft über denselben zentralen Broker, du verbindest dich also per OAuth und legst noch am selben Tag mit dem Hochladen los. Das ist die Architektur: msclkid-Attribution, das Goal-Name-Adressierungsmodell, die zwei Account-IDs, die jeder Upload braucht, und warum Bing mehr B2B-Budget trägt, als sein Marktanteil vermuten lässt.
Dieselbe Sperre wie bei Google Ads, einen Anbieter weiter
Wenn du den Google-Ads-Developer-Token-Beitrag gelesen hast, kommt dir die Form dieses Problems bekannt vor. Die Authentifizierung gegen die Microsoft Advertising Conversions API braucht zwei getrennte Credentials, und sie liegen auf gegenüberliegenden Seiten der Reibungslinie.
- Ein Developer-Token. Es gehört dem, der die Integration geschrieben hat. Es identifiziert die Anwendung gegenüber Microsoft, regelt das API-Quota und ist das, was Microsoft prüft, wenn du Produktiv-Zugang beantragst. Ein Token pro Integration, egal wie viele Ad-Accounts es nutzen.
- Ein OAuth-Credential. Es gehört jedem einzelnen Ad-Account-Inhaber. Es sagt, welcher Microsoft-Account hochlädt und zu welcher Customer-ID. In Sekunden über den Standard-OAuth-Handshake ausgestellt, keine Review-Queue.
Die OAuth-Seite ist sofort da. Die Developer-Token-Seite ist ein schriftlicher Antrag an Microsoft, der deinen Use-Case, deinen Datenumgang und deine Integration beschreibt, gefolgt von Hin und Her mit den Prüfern. Das realistische Zeitfenster liegt im selben Mehrwochenbereich wie bei Google. Für einen kleinen WordPress-Shop, der einfach nur Bing-Conversions in seinen Reports will, ist das eine Pause von einem Monat plus, bevor die erste Conversion hochgeladen werden kann.
Beaconrys Antwort ist dieselbe wie bei Google Ads: ein zentraler Cloudflare Worker (der Broker) hält Beaconrys eigenes freigegebenes Developer-Token, und der Kunden-Install sieht es nie. Der Kunde durchläuft nur OAuth. Das ist die ganze Idee. Der Rest dieses Beitrags handelt von den Teilen der Microsoft-Pipeline, die keine Kopie von Google sind, denn genau dort stecken die Implementierungsdetails.
Wo Microsoft sich im Datenverkehr von Google unterscheidet
Das Broker-Pattern ist geteilt, aber die Microsoft-API hat vier konkrete Unterschiede, die der Dispatcher behandeln muss. Jeder davon ist eine Stelle, an der ein naiver "einfach den Google-Pfad klonen"-Port still brechen würde.
1. msclkid, nicht gclid
Googles Click-Identifier ist gclid. Microsofts ist msclkid (Microsoft Click ID). Er kommt auf dieselbe Weise an, als URL-Parameter auf der Landingpage, wenn ein Besucher auf eine Microsoft-Anzeige klickt, und Beaconrys nl-data-Engine erfasst ihn beim Landen und speichert ihn im First-Party-Cookie nl_ext neben jeder anderen Click-ID, die es trackt. Server-seitig ist die Lesereihenfolge zuerst der ?msclkid=-Parameter des Live-Requests, dann das nl_ext-Cookie:
public static function get_msclkid(): string {
if ( ! empty( $_GET['msclkid'] ) ) {
$id = sanitize_text_field( wp_unslash( (string) $_GET['msclkid'] ) );
if ( $id !== '' ) {
return $id;
}
}
// ... fall back to the nl_ext cookie's persisted msclkid
}Diese Reihenfolge ist wichtig: Der URL-Parameter ist das frischeste Signal (der Besucher ist gerade jetzt auf der Anzeigen-Landingpage), und das Cookie ist der dauerhafte Fallback für Conversions, die bei einem späteren Page-View in derselben Session passieren.
2. Conversions werden über den Goal-NAMEN adressiert, nicht über eine numerische ID
Das ist der Unterschied, der Microsoft tatsächlich einfacher zu konfigurieren macht als Google. Im Google-Ads-Pfad mappst du jedes Event auf eine numerische ConversionAction-ID, eine undurchsichtige Zahl, die du aus der Google-Ads-UI kopierst. Microsoft adressiert Conversions über ihren ConversionName: den menschenlesbaren Goal-Namen, den du in der Bing-Ads-UI unter "Conversion goals" eingetippt hast. Statt also 1234567890 pro Slot einzufügen, tippst du Purchase oder Newsletter signup, genau so, wie es in Microsoft Advertising erscheint. Der String reist als ConversionName über die Leitung, und Microsoft matcht ihn server-seitig.
Der Haken: Ein Tippfehler im Goal-Namen scheitert still. Es gibt keine numerische ID zum Validieren, wenn der Name in Beaconry also nicht Zeichen für Zeichen mit dem Namen in der Bing-Ads-UI übereinstimmt, akzeptiert Microsoft den Upload und ordnet ihn klammheimlich nichts zu. Der Fix ist operativ, nicht architektonisch: Kopiere den Namen, tippe ihn nicht neu.
3. Zwei Account-IDs pro Upload, nicht eine
Google braucht eine einzige Customer-ID pro Upload. Microsoft braucht zwei: eine CustomerId und eine CustomerAccountId. Die erste identifiziert den Kunden (die Manager-Ebene), die zweite identifiziert den konkreten Ad-Account darunter. Beide sind 6- bis 12-stellige Zahlen, beide werden bei jedem Upload als Header vom Broker gesendet, und ein Dispatch ist erst dann "ready", wenn beide gespeichert sind:
public static function is_dispatch_ready(): bool {
return self::is_connected()
&& (string) BCNR_Settings::get( 'microsoft_ads_customer_id' ) !== ''
&& (string) BCNR_Settings::get( 'microsoft_ads_account_id' ) !== '';
}Microsoft hat kein separates login_customer_id-Konzept, wie es Google Ads für Manager-Account-(MCC-)Szenarien hat. Die CustomerId trägt diese Rolle direkt, was ein Feld weniger zum Mitdenken ist, auch wenn es insgesamt eine ID mehr als bei Google ist.
4. Kein dokumentiertes Refresh-Token-Revoke
Google stellt einen Token-Revoke-Endpoint bereit, sodass das Trennen den Grant sofort hart killt. Microsoft veröffentlicht kein Äquivalent. Beaconrys Disconnect macht deshalb das Maximum, was es kann: Es POSTet /microsoft/disconnect, damit der Broker sein gespeichertes Refresh-Token aus dem KV löscht, und verwirft dann das lokal gespeicherte Bearer und die gesamte ID- und Goal-Konfiguration in WordPress. Microsoft selbst lässt das Refresh-Token server-seitig nach 90 Tagen Inaktivität ablaufen. Ein Kunde, der den Grant sofort weghaben will, kann Beaconrys App unter account.microsoft.com/privacy/app-access widerrufen, was die AAD-Session auf Microsofts Seite killt. Beaconrys Disconnect-Text weist genau aus diesem Grund dorthin.
Die acht Conversion-Goal-Slots
Beaconry mappt eine feste Menge kanonischer Events auf Microsoft-Conversion-Goals. Es gibt acht Slots, und jeder Slot-Wert ist der ConversionName-String, den du in der Bing-Ads-UI konfiguriert hast:
public const GOAL_SLOTS = [
'purchase',
'add_to_cart',
'begin_checkout',
'lead',
'subscribe',
'schedule',
'signup',
'contact',
];Das sind purchase, add_to_cart, begin_checkout, lead, subscribe, schedule, signup und contact. Die Commerce-Vier (purchase, add_to_cart, begin_checkout plus der implizite Funnel, in dem sie sitzen) decken einen WooCommerce- oder EDD-Store ab; die anderen vier (lead, subscribe, schedule, signup, contact) decken die B2B- und SaaS-Funnels ab, wo sich Microsoft Ads tatsächlich bezahlt macht. Du füllst nur die Slots aus, für die du Goals hast. Ein Event, dessen Slot keinen Goal-Namen gesetzt hat, wird als "auf diesem Channel nicht gewünscht" behandelt und still übersprungen, sodass eine Lead-Gen-Site, die nie ein Produkt verkauft, die Commerce-Slots einfach leer lässt.
Die Slot-Menge hat bewusst dieselbe Form wie die Google-Ads-Label-Slots, sodass der Forwarder ein einzelnes kanonisches Event aus einer Map an beide Anbieter ausfächern kann, ohne Per-Vendor-Verzweigung.
Der Attributions-Fallback: msclkid ODER gehashte PII
Hier ist der Teil des Dispatchers, der die meisten Conversions in der echten Welt einbringt. Microsofts OfflineConversions-Endpoint will eine MicrosoftClickId, um eine Conversion auf die Anzeige zurückzuführen, die sie ausgelöst hat. Aber msclkid ist ein Cookie-Wert, und Cookie-Werte überleben nicht ewig: Ein mehrtägiger Pfad, ein Browser-Storage-Cleanup oder ein anderes Gerät zwischen Klick und Kauf kann ihn verlieren. Wäre msclkid strikt verpflichtend, würde jede einzelne dieser Conversions mit längerem Zeitfenster verworfen.
Microsofts eigene OfflineConversion-Data-Object-Spec macht die MicrosoftClickId optional, wenn stattdessen eine gehashte E-Mail oder ein gehashtes Telefon geliefert wird, was ihre Enhanced-Conversions-Matching-Pipeline füttert. Beaconry implementiert genau diesen Fallback. Der Upload läuft, wenn er entweder eine Click-ID oder mindestens einen gehashten Match-Key hat, und wird nur übersprungen, wenn er keines von beiden hat:
$msclkid = (string) ( $event['msclkid'] ?? '' );
$user = (array) ( $event['user_data'] ?? [] );
$has_user_id = ! empty( $user['em'] ) || ! empty( $user['ph'] );
if ( $msclkid === '' && ! $has_user_id ) {
return; // nothing to attribute on, silent skip
}So lädt ein WooCommerce-Kauf, bei dem die msclkid des Besuchers drei Tage nach dem Anzeigenklick verloren ging, trotzdem hoch, weil der Checkout Beaconry eine gehashte E-Mail übergeben hat. Die Click-ID ist das stärkste Einzelsignal, aber sie ist nicht das einzige, und der Dispatcher weigert sich, eine perfekt attribuierbare Conversion wegzuwerfen, nur weil das Cookie zu alt geworden ist.
Was tatsächlich über die Leitung geht
Das OfflineConversion-Objekt, das Beaconry zusammenbaut, ist eine knappe Teilmenge von Microsofts Schema. Jedes Feld mappt auf einen dokumentierten Schema-Key, und Felder, die leer sind, werden vor dem Senden entfernt, sodass die API nie eine leere HashedEmailAddress oder eine leere MicrosoftClickId sieht:
$conversion = array_filter( [
'ConversionName' => $goal_name,
'ConversionTime' => gmdate( 'Y-m-d\TH:i:s\Z', $ts ),
'ConversionValue' => $value,
'ConversionCurrencyCode' => strtoupper( $currency ),
'MicrosoftClickId' => $msclkid,
'HashedEmailAddress' => $user['em'] ?? '',
'HashedPhoneNumber' => $user['ph'] ?? '',
], /* drop empty + null */ );Zwei Details, die aus dem Lesen von Microsofts Spec kamen statt aus dem Raten vom Google-Pfad:
- ConversionTime ist ISO 8601 mit einem expliziten UTC-Marker. Das Format ist
yyyy-mm-ddThh:mm:ssZ, erzeugt mitgmdate(...'Z'), sodass es eindeutig UTC ist. Ein naiver Local-Time-String würde Conversions hier ins falsche Attributionsfenster legen. - TransactionId ist NICHT im OfflineConversion-Schema. Eine frühe Version des Dispatchers trug sie aus Gewohnheit mit; Microsoft verwirft entweder unbekannte Felder oder weist sie zurück, also wurde sie entfernt. Die Order-Level-Idempotenz, die TransactionId geliefert hätte, wird stattdessen weiter oben von Beaconrys stabiler Per-Order-
event_idübernommen, bevor das Event überhaupt den Microsoft-Dispatcher erreicht.
Die Währung wird in Großbuchstaben gesetzt, der Wert ist ein Float, und das ganze Objekt läuft durch array_filter, sodass die Wire-Payload nur die Keys enthält, die echte Daten haben.
Der Upload-Pfad, Ende zu Ende
Bringt man den Broker und den Dispatcher zusammen, läuft ein einzelner Microsoft-Conversion-Upload so:
- Ein Funnel-Event feuert server-seitig (ein WooCommerce-Purchase-Hook, ein Form-Submit, ein EDD-Verkauf). Der Forwarder routet es mit einer der acht Goal-Arten zum Microsoft-Channel.
- Der Dispatcher prüft
is_dispatch_ready()(Bearer + beide IDs gespeichert), schlägt den ConversionName für diese Goal-Art nach und bricht still ab, falls eines davon fehlt. - Er prüft den Attributions-Fallback: msclkid oder gehashtes em/ph, sonst überspringen.
- Er baut das oben gezeigte OfflineConversion-Objekt und POSTet es an die
/microsoft/upload-Route des Brokers mit dem HMAC-signierten Bearer der Site im Authorization-Header. - Der Broker liest das gespeicherte Refresh-Token für diese Site, prägt ein frisches Microsoft-Access-Token, hängt das zentrale Developer-Token plus die Per-Call-Header CustomerId und CustomerAccountId an und leitet an
campaign.api.bingads.microsoft.comweiter. - Der Call ist Fire-and-Forget:
blocking => false, sodass der ursprüngliche Browser-Request (der Checkout, der Form-Submit) sofort zurückkehrt. Ergebnisse tauchen im/microsoft/quota-Counter des Brokers auf, den das Status-Dashboard liest.
Das WordPress des Kunden hält nur das Bearer-JWT, die zwei numerischen Account-IDs und die Goal-Namen-Strings. Jedes tatsächliche Secret, das Microsoft-OAuth-Client-Secret, das Developer-Token, die Refresh- und Access-Tokens, lebt im Wrangler-Secret-Store des Brokers und in seinem KV, geschlüsselt über eine undurchsichtige site_id, die ins Bearer eingebacken ist. Der OAuth-Flow selbst läuft über Microsofts /common/-v2.0-Endpoint mit dem Scope msads.manage offline_access, sodass sowohl Work-or-School-AAD-Accounts als auch persönliche Microsoft-Accounts (was die meisten Bing-Ads-Kunden nutzen) sich verbinden können, ohne durch einen bestimmten Tenant gezwungen zu werden.
Warum Bing B2B-Budget trägt, das in keinem Verhältnis zu seinem Marktanteil steht
Es ist verlockend, Microsoft Ads zu überspringen, weil Bings gesamter Such-Anteil neben Google klein aussieht. Diese Logik liest falsch, wer tatsächlich auf Bing ist. Microsoft Search ist der Default an einem sehr spezifischen und sehr wertvollen Ort: der Office-365- und Microsoft-365-Install-Base, Edge auf verwalteten Windows-11-Flotten und Firmen-Desktops, wo die IT den Default nie geändert hat. Diese Population neigt stark zu Business-Entscheidern, Beschaffung und SaaS-Käufern, die auf eine Firmenkarte ausgeben.
Die praktischen Konsequenzen für einen B2B- oder SaaS-Werbetreibenden:
- Überproportionale B2B-Reichweite. Bei Business-geführten Kampagnen fängt Microsoft Ads häufig einen Anteil am bezahlten Traffic ein, der weit über dem liegt, was der reine Such-Anteil nahelegt, weil die Audience-Zusammensetzung genau das High-Intent-Business-Segment ist.
- Weniger Konkurrenz, oft niedrigere CPAs. Weniger Werbetreibende bieten auf Bing als auf Google, also kann dasselbe Keyword zu einem niedrigeren Cost-per-Acquisition durchgehen. Das taucht in deinem Reporting nur auf, wenn die Conversions tatsächlich hochgeladen werden, was der ganze Sinn dabei ist, die API zum Laufen zu bringen.
- Search-Partner-Syndication ist gratis Reichweite. Microsoft Ads liefert über Search-Syndication-Partnerschaften auch auf Yahoo, Ecosia und DuckDuckGo aus. Conversions von diesen Partner-Klicks tragen msclkid auf dieselbe Weise, sodass im Dispatcher keine Sonderbehandlung nötig ist.
Eine LinkedIn-Notiz, weil die Eigentümerschaft Leute verwirrt: Microsoft besitzt LinkedIn, aber Microsoft Ads und die LinkedIn Conversions API sind zwei getrennte Plattformen mit zwei getrennten Slots in Beaconry. Beide zu besitzen, führt sie nicht zusammen. Wenn du auf beiden Kampagnen fährst, konfigurierst du beide; Events fließen zu dem, was verkabelt ist.
Fazit
Microsoft Ads hat dieselbe Developer-Token-Sperre wie Google Ads, und Beaconry überspringt sie auf dieselbe Weise: Ein zentraler Broker hält das freigegebene Token, du verbindest dich per OAuth, du lädst noch am selben Tag hoch. Die Teile, die zu kennen sich lohnt, sind die Unterschiede, msclkid statt gclid, Conversion-Goals über den Namen adressiert statt über eine numerische ID, zwei Account-IDs pro Upload statt einer, und ein Click-ID-oder-gehashte-PII-Attributions-Fallback, der die Conversions mit längerem Zeitfenster rettet, die eine strikte msclkid-Regel verwerfen würde. Nichts davon kompromittiert die Privacy-Trennung: Das Refresh-Token des Kunden verlässt nie WordPress, Beaconrys Developer-Token verlässt nie den Broker. Und für einen B2B- oder SaaS-Shop ist der Channel die Verkabelung wert, weil Bings Audience mehr Business-Intent trägt, als sein Marktanteil je vermuten lassen würde.