Rekomi Docs
Für MarkenTracking installieren
Brands

Auf Rails installieren

Serverseitiges S2S ist der Hauptweg für Rails-Apps. Head-Installation in application.html.erb für die Klickerfassung, eine Service-Klasse RekomiClient für HMAC-signierte Conversion-Aufrufe.

Rails-Apps haben schon einen Server, deshalb ist serverseitiges S2S der sauberste Weg für Conversions (nicht das Browser-Pixel). Für die Klickerfassung nutzt du trotzdem das Head-Skript im Browser, das die Empfehlung erfasst; Conversions sendest du aus deinem Rails-Backend über eine kleine Service-Klasse RekomiClient, die sich um die HMAC-Signatur kümmert. Der Rekomi-Loader funktioniert ohne weiteres Zutun mit Turbo und Stimulus.

Das Head-Skript in application.html.erb installieren

Die Asset-Pipeline (Propshaft / Sprockets / Importmaps / jsbundling-rails) verwaltet nur dein eigenes JavaScript. Skripte von Drittanbietern kommen als reines HTML ins Layout.

Bearbeite app/views/layouts/application.html.erb:

<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "Your App" %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application" %>
    <%= javascript_importmap_tags %>

    <!-- Rekomi tracking -->
    <script async src="https://api.rekomi.com/api/v1/r/loader.js"
            data-program-id="<%= Rails.application.credentials.dig(:rekomi, :program_id) %>"
            data-rkmi-email-capture="auto"></script>
    <noscript>
      <img src="https://api.rekomi.com/api/v1/r/c.gif" width="1" height="1" alt="" referrerpolicy="no-referrer-when-downgrade" />
    </noscript>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

Rekomi erfasst eine E-Mail-Adresse, die ein vermittelter Besucher auf den Seiten, auf denen das Tag läuft, in ein beliebiges E-Mail- oder Textfeld tippt (Registrierung, Login, Checkout und andere Formulare), damit der Verkauf auch dann gutgeschrieben werden kann, wenn dein Checkout keine Empfehlung übergibt. Ohne Empfehlung wird nichts erfasst. Entferne das Attribut data-rkmi-email-capture, um das auszuschalten; explizite Aufrufe von Rekomi.convert() funktionieren weiterhin.

Nutze Rails.application.credentials.dig(:rekomi, :program_id), damit die Konfiguration nicht in der Versionsverwaltung landet. Deine Schlüssel trägst du mit rails credentials:edit ein. Die Programm-ID ist eine öffentliche Kennung; API-Schlüssel und Signing Secret gehören nur auf den Server (du bekommst sie, wenn du einen Rekomi-API-Schlüssel erstellst):

rekomi:
  program_id: YOUR_PROGRAM_ID
  api_key: rk_live_xxxxxxxxxxxxxxxxx    # Bearer, authenticates the request
  signing_secret: rks_xxxxxxxxxxxxxxx   # signs the request body

Content Security Policy

Hat config/initializers/content_security_policy.rb strenge Direktiven, erlaube Rekomi:

Rails.application.config.content_security_policy do |policy|
  policy.script_src :self, :https, "https://api.rekomi.com"
  policy.connect_src :self, :https, "https://api.rekomi.com"
  # ... other directives
end

Ohne die Freigabe in script_src blockiert der Browser den Loader stillschweigend. Ohne connect_src scheitert der POST für den Klick mit einem CSP-Verstoß, den du in der Browser-Konsole siehst.

Einen RekomiClient-Service für S2S-Conversions bauen

Lege app/services/rekomi_client.rb an. Der S2S-Endpunkt authentifiziert mit deinem API-Schlüssel als Bearer UND einem Header X-Rekomi-Signature im Format t=<unix-seconds>,sig=<hex>, wobei die Signatur HMAC-SHA256(signing_secret, "<unix-seconds>.<raw-body>") ist. Signiere genau die Bytes, die du sendest. Die Felder im Body sind in camelCase.

require "net/http"
require "openssl"
require "json"

class RekomiClient
  ENDPOINT = "https://api.rekomi.com/api/tracking/s2s".freeze
  REFUND_ENDPOINT = "https://api.rekomi.com/api/tracking/refund".freeze

  def self.track(external_event_id:, affiliate_slug:, amount_cents:, currency: "USD", customer_id: nil, customer_email: nil)
    body = {
      externalEventId: external_event_id,
      affiliateSlug: affiliate_slug,
      amountCents: amount_cents,
      currency: currency,
      customerId: customer_id,
      customerEmail: customer_email,
    }.compact.to_json

    post(ENDPOINT, body)
  end

  def self.refund(external_event_id:, refund_amount_cents: nil)
    # Omit refundAmountCents for a full refund; pass minor units for a partial.
    body = { externalEventId: external_event_id, refundAmountCents: refund_amount_cents }.compact.to_json
    post(REFUND_ENDPOINT, body)
  end

  def self.post(endpoint, body)
    creds = Rails.application.credentials.dig(:rekomi)
    t = Time.now.to_i
    signature = OpenSSL::HMAC.hexdigest("SHA256", creds[:signing_secret], "#{t}.#{body}")

    uri = URI(endpoint)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Post.new(uri.path, {
      "Content-Type" => "application/json",
      "Authorization" => "Bearer #{creds[:api_key]}",
      "X-Rekomi-Signature" => "t=#{t},sig=#{signature}",
    })
    request.body = body

    response = http.request(request)
    raise "Rekomi S2S failed: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
    response
  end
end

Der Webhook unten liest den Slug des Affiliates aus der client_reference_id der Checkout Session, setze ihn also beim Anlegen der Session. Der Slug liegt im Browser des Käufers (window.Rekomi.getReferral()); gib ihn mit der Anfrage weiter, die den Checkout startet, und setze ihn dann auf dem Server:

# app/controllers/checkouts_controller.rb
# params[:rekomi_referral] is window.Rekomi.getReferral(), sent by your checkout button.
session = Stripe::Checkout::Session.create(
  mode: "subscription",
  line_items: [{ price: params[:price_id], quantity: 1 }],
  client_reference_id: params[:rekomi_referral].presence,
  success_url: success_url,
  cancel_url: cancel_url,
)

Beachte, dass client_reference_id nur von DEINEM Webhook unten gelesen wird. Die direkte Stripe-Integration von Rekomi liest es nicht; dieser Weg nutzt stattdessen die Abo-Metadaten rekomi_affiliate_slug.

Rufe den Service aus deinem Stripe-Webhook-Handler auf (oder dort, wo deine Logik für „Abonnement angelegt“ liegt):

# app/controllers/stripe_webhooks_controller.rb
class StripeWebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    event = Stripe::Webhook.construct_event(
      request.body.read,
      request.env["HTTP_STRIPE_SIGNATURE"],
      Rails.application.credentials.dig(:stripe, :webhook_secret)
    )

    if event.type == "checkout.session.completed"
      session = event.data.object
      RekomiClient.track(
        external_event_id: session.id,
        affiliate_slug: session.client_reference_id,
        amount_cents: session.amount_total,
        currency: session.currency,
        customer_id: session.customer,
        customer_email: session.customer_details&.email,
      ) if session.client_reference_id.present?
    end

    head :ok
  end
end

Kompatibilität mit Turbo und Stimulus

Rails 7 bringt standardmäßig Turbo (kompletter Seitenaustausch per XHR + morphdom) und Stimulus (schlanke JavaScript-Controller) mit. Der Rekomi-Loader funktioniert mit beiden:

  • Turbo: Der Rekomi-Loader richtet window.Rekomi einmal beim ersten Laden des Dokuments ein, und es bleibt über Turbo-Navigationen hinweg erhalten. Die Empfehlung bleibt erfasst; du musst nichts bei jeder Navigation neu auslösen.
  • Stimulus: Willst du die Empfehlung in einem Stimulus-Controller auslesen (etwa um sie an eine Checkout-Anfrage weiterzugeben), greif direkt auf window.Rekomi.getReferral() zu. Es steht nach dem ersten Laden global zur Verfügung.

Für keins von beiden brauchst du eine besondere Integration.

Warum serverseitiges S2S statt Browser-Pixel

Rails-Apps haben ein Backend. Serverseitiges S2S ist zuverlässiger als Aufrufe aus dem Browser, weil:

  • Werbe- und Analytics-Blocker keine serverseitigen Aufrufe blockieren können. uBlock Origin und Brave Shields blockieren standardmäßig viele Tracking-POSTs aus dem Browser; serverseitige Aufrufe umgehen das alles.
  • Erstattungen sauberer laufen. Wenn Stripe charge.refunded auslöst, ist dein Rails-Webhook der natürliche Ort, um die Erstattung an Rekomi weiterzugeben.
  • Die Bestelldaten maßgeblich sind. Serverseitige Aufrufe nutzen Daten aus deiner Datenbank oder der Stripe-Session statt etwas, das im Browser ausgelesen wurde.

Gut zu wissen

Zwei Secrets, nicht eins. Der Bearer-API-Schlüssel (rk_live_...) authentifiziert die Anfrage; das separate Signing Secret (rks_...) signiert den Body. Beide bekommst du, wenn du einen Rekomi-API-Schlüssel erstellst.

Credentials statt Umgebungsvariablen. Die verschlüsselten Credentials von Rails 7 sind das empfohlene Muster, um Secrets zu speichern. ENV["REKOMI_API_KEY"] funktioniert auch, wenn dein Deployment mit Umgebungsvariablen nach dem 12-Factor-Prinzip arbeitet.

Die CSP muss den Loader UND den Endpunkt erlauben. Füge https://api.rekomi.com sowohl zu script_src (für den Loader) als auch zu connect_src (für den POST des Klicks) hinzu.

Die Uhrzeit muss stimmen. Rekomi lehnt eine Signatur ab, deren Zeitstempel t= um mehr als 300 Sekunden abweicht. Halte deinen Host per NTP synchron.

Sidekiq oder GoodJob für asynchrones S2S. Sendest du S2S aus einem Stripe-Webhook-Handler, der schnell mit 200 antworten muss (Stripe bricht nach 30 Sekunden ab), verpacke den Aufruf von RekomiClient.track in einen Job für Sidekiq oder GoodJob. Das HMAC-Signieren geht schnell; der Netzwerkaufruf ist das, was hängen kann.

Der HMAC wird über "#{t}.#{body}" gebildet. Erzeuge den JSON-Body einmal, signiere genau diesen String und sende per POST dieselben Bytes. Wird zwischen Signieren und Senden neu serialisiert, passen die Signaturen nicht zusammen, und Rekomi lehnt den Aufruf ab.

Erstattungen

Wenn Stripe charge.refunded auslöst, übergib DIESELBE externe Ereignis-ID, die du beim Erfassen des Verkaufs gesendet hast (in dieser Anleitung die ID der Checkout Session). Rekomi sucht die Conversion ausschließlich über diese ID, die Übergabe von charge.id liefert also 404, und die Provision wird nie zurückgeholt. Ordne die Zahlung über deine eigenen Daten der ursprünglichen ID zu:

if event.type == "charge.refunded"
  charge = event.data.object
  # Use the SAME external event id you sent when recording the sale
  # (the Checkout Session id in this guide). Look it up from your DB
  # via charge.payment_intent. Omit refund_amount_cents for a full refund.
  order = Order.find_by(payment_intent_id: charge.payment_intent)
  RekomiClient.refund(external_event_id: order.checkout_session_id) if order
end

Fehlerbehebung

Das Skript steht im Layout, aber es werden keine Klicks erfasst. Die CSP blockiert es. Prüfe die Browser-Konsole auf Fehler Refused to load the script und füge api.rekomi.com zu script_src hinzu.

Der S2S-Aufruf liefert 401. Bearer-Schlüssel fehlt, HMAC stimmt nicht, oder die Uhrzeit weicht ab. Prüfe, ob api_key und signing_secret in den Credentials stehen, ob die Signatur das Format t=<unix>,sig=<hex> über "#{t}.#{body}" nutzt, ob du genau den Body hashst, den du per POST sendest, und ob die Uhr deines Hosts höchstens 300 Sekunden von der echten Zeit abweicht.

Der S2S-Aufruf liefert 200 mit deduped: true. Du hast mit dieser externalEventId schon eine Conversion erfasst; der Aufruf wurde gefahrlos ignoriert.

Der S2S-Aufruf bricht mit 400 affiliate_slug_required oder affiliate_not_found ab. Die client_reference_id der Stripe-Session war leer oder veraltet, der Browser hat also keine gültige Empfehlung in den Checkout mitgenommen. Prüfe die Head-Installation auf der Seite, auf die der Affiliate den Traffic geschickt hat.

Der S2S-Aufruf liefert 409. In deinem Rekomi-Workspace ist ein Zahlungsanbieter verbunden, der Conversions schon automatisch erfasst. Nutze nur eine Quelle; trenne den Zahlungsanbieter auf seiner Seite im Schritt Zahlungsanbieter verbinden, wenn du S2S senden willst.

Der S2S-Aufruf liefert 402. S2S setzt den Starter-Plan oder höher voraus (Testphasen eingeschlossen).

S2S wird für Bestellungen ohne Affiliate gesendet. Prüfe vor dem Aufruf von RekomiClient.track (wie gezeigt mit if session.client_reference_id.present?), damit du keine API-Aufrufe für organische Bestellungen verschwendest.