# Tracking-Referenz für Entwickler: window.RTR_HOOKS

Wie die Rentware-Widgets Buchungsereignisse an den dataLayer, gtag, das Meta-Pixel, Umami und das OpenAI-Pixel senden: Optionen, Payloads, Callbacks, GTM-Einrichtung und Einwilligung.

Diese Seite richtet sich an Entwickler und Agenturen, die die Rentware-Buchungswidgets mit ihren eigenen Tags verbinden. Sie beschreibt die **Browser**-Seite. Serverseitiges Tracking über Rentware (GA4, Google Ads und Meta von unserem Server) braucht keinen Code: siehe [Conversion-Tracking: was du wo einrichtest](https://rentware.com/help/conversion-tracking/).

## Überblick

- Das Widget sendet im Browser **nichts** an Tags, solange die Seite `window.RTR_HOOKS` nicht definiert. Der ältere Name `window.RENTWARE_HOOKS` funktioniert auch; gibt es beide, gilt `RTR_HOOKS`.
- `RTR_HOOKS` wird bei jedem Ereignis neu gelesen. Es kann also spät gesetzt werden, zum Beispiel von einem GTM-Tag, solange es vor dem Ereignis existiert.
- Das Widget lädt nie selbst ein Tag. `gtag`, `fbq`, `umami` und `oaiq` müssen schon auf der Seite sein; fehlt eine Funktion, wird dieser Kanal stillschweigend übersprungen. Nur der `dataLayer` wird angelegt, wenn er nicht existiert.
- Das Widget prüft keine Einwilligung für deine Tags. Lade sie erst nach der Einwilligung (Aufgabe deines Cookie-Banners).
- Alle Werte sind **netto**: Rabatte abgezogen, Kautionen weggelassen, MwSt. in `tax`. Beträge sind Zahlen in der Hauptwährungseinheit (`49.5`), sofern nicht anders angegeben.

## Snippet

```html
<script>
  window.RTR_HOOKS = Object.assign(window.RTR_HOOKS || {}, {
    // E-Commerce-Ereignisse im GA4-Format in window.dataLayer (Google Tag Manager; funktioniert auch ohne GTM)
    enableDataLayer: true,

    // gtag('event', …) direkt
    // enableGtagTracking: true,
    // zusätzlich gtag('event', 'conversion', …) beim Kauf
    // gtagAdsSendTo: 'AW-123456789/AbCdEfGhIj',
    // gtag('set', 'user_data', …) für Enhanced Conversions
    // enableGtagUserDataTracking: true,

    // fbq('track', …) auf deinem Meta-Pixel
    // enableMetaTracking: true,
    // zusätzlich fbq('init', id, {…}) mit Kundendaten (erweiterter Abgleich)
    // metaPixelId: '123456789012345',

    // umami.track(…)
    // enableUmamiTracking: true,
    // oaiq('measure', 'order_created', …)
    // enableOpenAiTracking: true,

    // onAddToCart: (data) => {},
    // onInitiateCheckout: (data) => {},
    // onCheckoutSuccess: (data) => {},
  })
</script>
```

`Object.assign` behält Schlüssel, die ein anderes Skript schon gesetzt hat.

## Alle Optionen

| Schlüssel | Typ | Wirkung |
|---|---|---|
| `enableDataLayer` | boolean | Schreibt `add_to_cart`, `begin_checkout`, `add_shipping_info` und `purchase` in `window.dataLayer` |
| `enableGtagTracking` | boolean | Ruft `gtag('event', …)` mit denselben vier Ereignissen auf |
| `gtagAdsSendTo` | string | Ruft beim Kauf zusätzlich `gtag('event', 'conversion', {…, send_to})` auf, z. B. `'AW-123456789/AbCdEfGhIj'` |
| `enableGtagUserDataTracking` | boolean | Ruft beim Speichern der Kundendaten `gtag('set', 'user_data', {…})` auf |
| `enableMetaTracking` | boolean | Ruft `fbq('track', …)` auf: `AddToCart`, `InitiateCheckout`, `Purchase` |
| `metaPixelId` | string | Mit `enableMetaTracking`: Ruft beim Speichern der Kundendaten `fbq('init', metaPixelId, {…})` mit Kundendaten auf |
| `enableUmamiTracking` | boolean | Ruft `umami.track(…)` auf: `add_to_cart`, `begin_checkout`, `purchase` |
| `enableOpenAiTracking` | boolean | Ruft beim Kauf `oaiq('measure', 'order_created', …)` auf |
| `onAddToCart` | function | Wird aufgerufen, nachdem ein Artikel hinzugefügt wurde |
| `onInitiateCheckout` | function | Wird aufgerufen, wenn sich der Checkout öffnet |
| `onCheckoutSuccess` | function | Wird aufgerufen, wenn die Buchung abgeschlossen ist |

## Ereignisse pro Schritt

| Schritt | dataLayer / gtag | Meta-Pixel | Umami | OpenAI | Wird ausgelöst, wenn |
|---|---|---|---|---|---|
| In den Warenkorb | `add_to_cart` | `AddToCart` | `add_to_cart` | – | ein Artikel erfolgreich hinzugefügt wurde |
| Checkout geöffnet | `begin_checkout` | `InitiateCheckout` | `begin_checkout` | – | sich der Checkout öffnet |
| Kundendaten gespeichert | `add_shipping_info` | – | – | – | das Kontakt- und Rechnungsformular gespeichert wird |
| Buchung abgeschlossen | `purchase` | `Purchase` | `purchase` | `order_created` | siehe „Wann purchase ausgelöst wird“ |

Es gibt kein Browser-Ereignis für die Wahl von Standort und Zeitraum, für das Ansehen eines Artikels oder für den Beginn der Zahlung (`add_payment_info` gibt es nur serverseitig). Es gibt keine DOM-`CustomEvent`s.

### Wann purchase ausgelöst wird

`purchase` wird ausgelöst, wenn die Buchung **abgeschlossen** ist. Das ist nicht immer der Zeitpunkt der Zahlung:

- **Reservierung, Vorkasse oder Überweisung, Gutschein:** direkt nach dem Absenden (noch nicht bezahlt).
- **Kartenzahlung im Checkout:** wenn die Zahlung erfolgreich ist.
- **Zahlungen mit Weiterleitung (PayPal, Zahlungsarten, die die Seite verlassen):** erst, wenn der Besucher auf eine Seite mit dem Widget zurückkehrt. Kommt er nie zurück, gibt es kein `purchase` im Browser. Der serverseitige Kauf wird trotzdem gesendet.

## dataLayer

Vor jedem Ereignis schreibt das Widget `{ ecommerce: null }` in den dataLayer, um das vorherige E-Commerce-Objekt zu leeren, wie Google es empfiehlt.

```js
// add_to_cart: jeder Artikel, dessen Menge gestiegen ist, automatisch hinzugefügte eingeschlossen
{ event: 'add_to_cart', ecommerce: {
  currency: 'EUR',
  value: 59.5,                      // Σ Preis × Menge
  items: [{ item_id: 'a1b2c3', item_name: 'SUP Board', price: 29.75, quantity: 2, index: 0 }]
} }

// begin_checkout und add_shipping_info: Nettowert des Warenkorbs
{ event: 'begin_checkout', ecommerce: { currency: 'EUR', value: 59.5 } }
{ event: 'add_shipping_info', ecommerce: { currency: 'EUR', value: 59.5 } }

// purchase
{ event: 'purchase', ecommerce: {
  transaction_id: 'SNLDX1',         // Bestellnummer
  currency: 'EUR',
  value: 59.5,                      // netto, Rabatte abgezogen, Kautionen weggelassen
  tax: 11.31,                       // MwSt.
  items: [{ item_id: 'a1b2c3', item_name: 'SUP Board', price: 29.75, quantity: 2 }]
} }
```

- `price` ist der rabattierte Netto-Stückpreis. Kautionspositionen stehen nie in `items`.
- `transaction_id` ist die **Bestellnummer** (z. B. `SNLDX1`). Der Server sendet denselben Wert als GA4-`transaction_id`, Google-Ads-`order_id` und Meta-`order_id`.
- Der dataLayer enthält **keine** E-Mail, keine Telefonnummer und keine Event-ID.

## gtag

Mit `enableGtagTracking` ruft das Widget `gtag('event', name, params)` mit denselben `ecommerce`-Objekten wie im dataLayer auf (die `items` von `purchase` tragen zusätzlich `index`).

Mit `gtagAdsSendTo` löst der Kauf zusätzlich Folgendes aus:

```js
gtag('event', 'conversion', {
  transaction_id: 'SNLDX1', currency: 'EUR', value: 59.5, tax: 11.31,
  items: [/* wie oben */],
  send_to: 'AW-123456789/AbCdEfGhIj'
})
```

Mit `enableGtagUserDataTracking`, beim Speichern der Kundendaten:

```js
gtag('set', 'user_data', {
  email: 'jane@example.com',
  phone_number: '+4915112345678',   // nur wenn gültiges E.164, sonst weggelassen
  address: { first_name: 'Jane', last_name: 'Doe', city: 'Berlin', postal_code: '10115', country: 'DE' }
})
```

Verwendet wird die Lieferadresse, wenn der Kunde eine eingegeben hat, sonst die Rechnungsadresse.

## Meta-Pixel

```js
fbq('track', 'AddToCart', { content_ids: ['a1b2c3'], content_type: 'product', currency: 'EUR', value: '29.75' })

fbq('track', 'InitiateCheckout', { value: '59.50', currency: 'EUR' })

fbq('track', 'Purchase', {
  order_id: 'SNLDX1',
  currency: 'EUR',
  value: '59.50',
  content_ids: ['a1b2c3'],
  content_type: 'product',
  contents: [{ id: 'a1b2c3', quantity: 2, price: '29.75', item_name: 'SUP Board' }],
  num_items: 1                      // Anzahl der Warenkorbzeilen
}, { eventID: 'purchase_SNLDX1' })
```

- Beträge für Meta sind Strings mit zwei Nachkommastellen.
- `eventID` ist `purchase_` + Bestellnummer, dieselbe `event_id` wie beim serverseitigen Purchase über die Conversions API, damit Meta die beiden dedupliziert.
- Mit `metaPixelId`, beim Speichern der Kundendaten: `fbq('init', metaPixelId, { em, fn, ln, ct, zp, country })` (Klartextwerte; das Pixel hasht sie).

## Umami

Mit `enableUmamiTracking` und dem Umami-Skript auf der Seite:

```js
umami.track('add_to_cart', { item_id: 'a1b2c3', item_name: 'SUP Board', quantity: 1, currency: 'EUR', value: 29.75 })
umami.track('begin_checkout', { value: 59.5, currency: 'EUR' })
umami.track('purchase', { revenue: 59.5, currency: 'EUR', transaction_id: 'SNLDX1', num_items: 1 })
```

Nur `purchase` trägt `revenue`. Leere Felder werden weggelassen. Umami setzt keine Cookies; ob du eine Einwilligung einholst, entscheidest du.

## OpenAI-Ads-Pixel

Mit `enableOpenAiTracking` und dem OpenAI-Pixel (`oaiq`) auf der Seite:

```js
oaiq('measure', 'order_created', {
  type: 'contents',
  amount: 5950,                     // kleinste Währungseinheit (Cent), netto
  currency: 'EUR',
  contents: [{ id: 'a1b2c3', name: 'SUP Board', content_type: 'product', quantity: 2 }]
}, { event_id: 'purchase_SNLDX1' })
```

Du kannst die Option auch weglassen und `oaiq` in `onCheckoutSuccess` selbst aufrufen.

## Callbacks

Fehler, die in einem Callback geworfen werden, werden abgefangen und in der Konsole ausgegeben, damit sie die Buchung nicht unterbrechen.

```js
onAddToCart({ productId, quantity, currency, name })
```

`productId` ist die Artikel-ID, `quantity` die hinzugefügte Menge.

```js
onInitiateCheckout({ transactionTotal, transactionId })
```

`transactionTotal` ist der **zu zahlende Betrag** (brutto, inklusive Kautionen), nicht der Netto-Warenkorbwert der anderen Ereignisse. `transactionId` ist die Bestellnummer.

```js
onCheckoutSuccess({ ecommerceData, fbqData, eventID, price })
```

- `ecommerceData.purchase`: das `purchase`-Objekt aus dem dataLayer (`transaction_id`, `currency`, `value`, `tax`, `items`).
- `fbqData`: die Purchase-Parameter für Meta (ohne `order_id`).
- `eventID`: `purchase_` + Bestellnummer.
- `price`: veraltetes Objekt `{ gross, net, vat }` in Cent, inklusive Kautionen. Nutze stattdessen `ecommerceData`.

Beispiel, ein eigenes Pixel:

```js
window.RTR_HOOKS = Object.assign(window.RTR_HOOKS || {}, {
  onCheckoutSuccess: ({ ecommerceData, eventID }) => {
    const p = ecommerceData.purchase
    myPixel('purchase', { order: p.transaction_id, value: p.value, currency: p.currency, dedup: eventID })
  },
})
```

## Google Tag Manager

1. Füge das Snippet mit `enableDataLayer: true` hinzu, entweder direkt in der Seite oder als GTM-Tag vom Typ **Benutzerdefiniertes HTML** (Custom HTML) mit dem Trigger **Initialisierung – Alle Seiten** (Initialization – All Pages).
2. Lege Trigger vom Typ **Benutzerdefiniertes Ereignis** (Custom Event) für `add_to_cart`, `begin_checkout`, `add_shipping_info` und `purchase` an.
3. Lege Variablen vom Typ **Datenschichtvariable** (Data Layer Variable, Version 2) an, zum Beispiel `ecommerce.value`, `ecommerce.currency`, `ecommerce.transaction_id`, `ecommerce.items`.
4. GA4-Ereignis-Tags: Ereignisname `{{Event}}` oder der feste Name, und „E-Commerce-Daten senden“ mit der Datenquelle „Data Layer“.
5. **Meta-Purchase über GTM:** Setze die Event-ID auf `purchase_{{DLV - ecommerce.transaction_id}}`. Sie passt dann zum serverseitigen Purchase, und Meta dedupliziert die beiden.

Nutzt du für dieselbe GA4-Property auch serverseitiges Tracking über Rentware, sende die dataLayer-Ereignisse nicht zusätzlich dorthin: GA4 dedupliziert nur `purchase` (anhand der `transaction_id`), „In den Warenkorb“ und die anderen Funnel-Ereignisse würden also doppelt gezählt.

## Einwilligung

Das Widget liest die Einwilligung für das, was es an den **Rentware-Server** sendet (der entscheidet, was an GA4, Google Ads und Meta geht). Deine eigenen Tags steuert es nicht.

- Es liest **Google Consent Mode v2** aus `window.dataLayer`: Einträge `gtag('consent', 'default' | 'update', {…})` und Objekte im GTM-Stil wie `{ event: 'consent_update', ad_storage: 'granted', … }`. Es spielt sie der Reihe nach ab: Ein späteres `update` überschreibt frühere Werte.
- Signale: `ad_storage`, `analytics_storage`, `ad_user_data`, `ad_personalization`. Jedes steht standardmäßig auf `denied`.
- Hat die Seite gar keinen Consent-Mode-Eintrag, wird die Anfrage als „kein Einwilligungssignal“ markiert, und das Tracking-Protokoll zeigt `skipped: no consent signal`.
- Regionsspezifische `default`-Einträge (mit `region`) lassen sich im Browser nicht dem Standort des Besuchers zuordnen: Nur ihre `denied`-Werte werden angewendet.
- **Späte Einwilligung funktioniert:** Die Einwilligung wird bei jeder Anfrage neu gelesen. Ein Besucher, der zustimmt, nachdem das Widget geladen hat, ist ab der nächsten Anfrage erfasst, solange dein Banner ein Consent-Mode-`update` in `window.dataLayer` schreibt.
- Das Widget liest nur den dataLayer namens `dataLayer` im selben Fenster. Ein umbenannter dataLayer oder ein Widget in einem iframe bedeutet: kein Einwilligungssignal.

So prüfst du eine Seite: Akzeptiere den Banner, öffne die Konsole und tippe `dataLayer`. Es muss einen `consent`-Eintrag (oder ein `consent_update`-Ereignis) geben, bevor der Besucher bucht.

## Klick-IDs

- Das Widget erfasst `gclid`, `gbraid` und `wbraid` aus der URL (`gclid` auch aus dem Linker-Parameter `_gl` von Google oder dem Cookie `_gcl_aw`) und `fbclid` aus der URL oder dem Cookie `_fbc` von Meta. Die URL hat Vorrang vor dem Cookie.
- Erfasst wird **nur mit `ad_storage`-Einwilligung**. Vorher wird nichts aus Cookies gelesen und nichts gespeichert. Kommt die Einwilligung später, während die Klick-ID noch in der URL steht, wird sie dann erfasst.
- Klick-IDs werden für den Tab im `sessionStorage` gehalten (gleicher Tab, gleiche Domain, weg, sobald der Tab geschlossen wird). Der erste Klick gilt.
- Das Widget sendet sie mit seinen Anfragen an den Rentware-Server, der sie für Google Ads und Meta nutzt. Deinen Browser-Ereignissen werden sie nicht hinzugefügt.
- Landingpage und Buchungsseite sind verschieden? Lass das Widget-Skript auch auf der Landingpage laufen, oder lass dein Google-Tag (mit `url_passthrough`) oder Meta-Pixel das Cookie `_gcl_aw` / `_fbc` schreiben. Für `gbraid` und `wbraid` gibt es keinen Cookie-Fallback.

## Test-Checkliste

1. Füge `RTR_HOOKS` hinzu, öffne die Seite, akzeptiere den Banner.
2. Lege einen Artikel in den Warenkorb und tippe dann `dataLayer` in die Konsole: Die letzten Einträge sind `{ ecommerce: null }` und `{ event: 'add_to_cart', … }`.
3. Schließe eine Buchung mit „Reservieren“ oder Vorkasse ab und prüfe den `purchase`-Eintrag; seine `transaction_id` ist die Bestellnummer.
4. Storniere die Testbuchung danach in Rentware.
