Rekomi Docs
Für MarkenTracking installieren
Brands

Auf Django installieren

Server-seitiges S2S ist der Hauptweg. Head-Installation in templates/base.html für die Klickerfassung, services/rekomi.py für HMAC-signierte Conversion-Aufrufe. Nutze Celery für asynchrone Aufrufe, wenn du aus Stripe-Webhooks heraus sendest.

Django-Apps haben ein Backend, deshalb ist server-seitiges S2S der richtige Weg für Conversions (zuverlässiger als Browser-Pixel, wenn Werbeblocker im Spiel sind). Die Klickerfassung läuft über ein Head-Skript in templates/base.html (von dem jede andere Seite erbt). Conversions werden aus einem kleinen Service-Modul rekomi.py gesendet, das die Standardmodule hmac und hashlib von Python nutzt. Wenn du unter nennenswerter Last aus Stripe-Webhooks heraus sendest, verpacke den Netzwerkaufruf in einen Task für Celery (oder Dramatiq), damit Stripe nicht nach 30 Sekunden in ein Timeout läuft.

Das Head-Skript in templates/base.html installieren

templates/base.html ist das Eltern-Template, von dem jede andere Seite per {% extends 'base.html' %} erbt. Füge das Head-Skript hier ein:

{% load static %}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>{% block title %}Your App{% endblock %}</title>

    {% block extra_head %}{% endblock %}

    <!-- Rekomi tracking -->
    <script async src="https://api.rekomi.com/api/v1/r/loader.js" data-program-id="{{ 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>
    {% block content %}{% endblock %}
  </body>
</html>

Das Token {{ REKOMI_PROGRAM_ID }} wird über einen Django-Kontextprozessor befüllt (nächster Schritt). Deine Programm-ID findest du im Installationsrezept deiner Kampagne unter Einrichtung > Installieren im Dashboard.

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.

Kontextprozessor für umgebungsabhängige Konfiguration

Füge einen Kontextprozessor hinzu, damit {{ REKOMI_PROGRAM_ID }} in jedem Template verfügbar ist:

# yourapp/context_processors.py
from django.conf import settings

def rekomi(request):
    return {
        "REKOMI_PROGRAM_ID": getattr(settings, "REKOMI_PROGRAM_ID", ""),
    }

Registriere ihn in settings.py:

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        # ... other config ...
        "OPTIONS": {
            "context_processors": [
                # ... existing processors ...
                "yourapp.context_processors.rekomi",
            ],
        },
    },
]

# Pull from environment so the same code works across dev/staging/prod
# (add `import os` at the top of settings.py if it isn't there already).
# The program ID is a public identifier; the API key and signing secret are
# server-side only (issued when you create a Rekomi API key).
REKOMI_PROGRAM_ID = os.environ.get("REKOMI_PROGRAM_ID", "")
REKOMI_API_KEY = os.environ.get("REKOMI_API_KEY", "")          # rk_live_...
REKOMI_SIGNING_SECRET = os.environ.get("REKOMI_SIGNING_SECRET", "")  # rks_...

Jetzt hat jedes Template (auch base.html) Zugriff auf {{ REKOMI_PROGRAM_ID }}.

Einen Service rekomi.py für S2S-Conversions bauen

Lege yourapp/services/rekomi.py 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.

import hashlib
import hmac
import json
import time
from typing import Optional

import requests
from django.conf import settings

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


def _signed_headers(body: str) -> dict:
    t = int(time.time())
    signature = hmac.new(
        settings.REKOMI_SIGNING_SECRET.encode("utf-8"),
        f"{t}.{body}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {settings.REKOMI_API_KEY}",
        "X-Rekomi-Signature": f"t={t},sig={signature}",
    }


def track_conversion(
    external_event_id: str,
    affiliate_slug: str,
    amount_cents: int,
    currency: str = "USD",
    customer_id: Optional[str] = None,
    customer_email: Optional[str] = None,
):
    payload = {
        "externalEventId": external_event_id,
        "affiliateSlug": affiliate_slug,
        "amountCents": amount_cents,
        "currency": currency,
    }
    if customer_id:
        payload["customerId"] = customer_id
    if customer_email:
        payload["customerEmail"] = customer_email

    body = json.dumps(payload, separators=(",", ":"))
    response = requests.post(
        ENDPOINT,
        data=body,
        headers=_signed_headers(body),
        timeout=10,
    )
    response.raise_for_status()
    return response


def track_refund(external_event_id: str, refund_amount_cents: Optional[int] = None):
    payload = {"externalEventId": external_event_id}
    if refund_amount_cents is not None:  # omit for a full refund
        payload["refundAmountCents"] = refund_amount_cents

    body = json.dumps(payload, separators=(",", ":"))
    return requests.post(
        REFUND_ENDPOINT,
        data=body,
        headers=_signed_headers(body),
        timeout=10,
    )

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:

# yourapp/views.py
# request.POST["rekomi_referral"] is window.Rekomi.getReferral(), sent by your checkout button.
def create_checkout(request):
    session = stripe.checkout.Session.create(
        mode="subscription",
        line_items=[{"price": request.POST["price_id"], "quantity": 1}],
        client_reference_id=request.POST.get("rekomi_referral") or None,
        success_url=SUCCESS_URL,
        cancel_url=CANCEL_URL,
    )
    return JsonResponse({"url": session.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 deiner Stripe-Webhook-View auf:

# yourapp/views.py
import stripe
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from yourapp.services.rekomi import track_conversion

stripe.api_key = settings.STRIPE_SECRET_KEY

@csrf_exempt
def stripe_webhook(request):
    payload = request.body
    sig_header = request.META.get("HTTP_STRIPE_SIGNATURE", "")
    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
        )
    except (ValueError, stripe.SignatureVerificationError):
        return HttpResponse(status=400)

    if event["type"] == "checkout.session.completed":
        session = event["data"]["object"]
        if session.get("client_reference_id"):
            track_conversion(
                external_event_id=session["id"],
                affiliate_slug=session["client_reference_id"],
                amount_cents=session["amount_total"],
                currency=session["currency"],
                customer_id=session.get("customer"),
                customer_email=(session.get("customer_details") or {}).get("email"),
            )

    return HttpResponse(status=200)

Asynchron mit Celery für den Produktivbetrieb

Wenn deine Stripe-Webhook-View nennenswert Arbeit erledigt, verpacke den Aufruf von track_conversion in einen Celery-Task, damit das 30-Sekunden-Timeout von Stripe deinen Stack nicht unter Druck setzt:

# yourapp/tasks.py
from celery import shared_task
from yourapp.services.rekomi import track_conversion

@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def track_conversion_async(self, **kwargs):
    try:
        # Return something serializable, not the Response object, or the
        # result backend fails to store it and triggers a needless retry.
        resp = track_conversion(**kwargs)
        return resp.status_code
    except Exception as exc:
        raise self.retry(exc=exc)

Dann in deiner View:

from yourapp.tasks import track_conversion_async

# In your webhook handler:
track_conversion_async.delay(
    external_event_id=session["id"],
    affiliate_slug=session["client_reference_id"],
    amount_cents=session["amount_total"],
    currency=session["currency"],
    customer_id=session.get("customer"),
    customer_email=(session.get("customer_details") or {}).get("email"),
)

Stripe bekommt sofort ein 200; der S2S-Aufruf läuft im Hintergrund, mit Wiederholung bei Fehlern.

Kompatibilität mit CSRF und Middleware

Das Loader-Skript von Rekomi wird über ein <script src>-Tag geladen (ein einfacher GET vom Browser an die Domain von Rekomi). Die CSRF-Middleware von Django schlägt dabei nicht an.

Der S2S-Endpunkt wird von Server zu Server mit HMAC aufgerufen und läuft nie über die CSRF-Prüfung deiner Django-App.

Wenn du eine strenge CSP-Middleware nutzt (z. B. django-csp), erlaube Rekomi. Bei django-csp 4.x:

# settings.py (django-csp 4.x)
CONTENT_SECURITY_POLICY = {
    "DIRECTIVES": {
        "script-src": ["'self'", "https://api.rekomi.com"],
        "connect-src": ["'self'", "https://api.rekomi.com"],
    }
}

Bei django-csp 3.x sind die Entsprechungen die Tupel CSP_SCRIPT_SRC und CSP_CONNECT_SRC. Wähle die Form passend zu deiner installierten Version: Einstellungen im Stil von 3.x werden unter 4.x stillschweigend ignoriert, und das sieht genau so aus, als würde der Browser den Loader blockieren.

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. Halte beide ausschließlich auf dem Server.

Signiere genau die Bytes, die du per POST sendest. Nutze json.dumps(payload, separators=(",", ":")) einmal, bilde den HMAC über genau diesen String als f"{t}.{body}" und sende per POST dieselben Bytes. Wird zwischen Hashen und Senden neu serialisiert, ist das die häufigste Ursache für ein 401.

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

requests.post blockiert in asynchronen Views die Event-Loop. Wenn du die asynchronen Views ab Django 4.1 nutzt (async def my_view), nimm httpx.AsyncClient statt requests. Das Signieren bleibt identisch.

Celery ist empfohlen, aber optional. Wenn du kein Celery betreibst, funktioniert der synchrone Aufruf von track_conversion bei Webhooks mit wenig Volumen problemlos. Behalte timeout=10 bei, damit eine langsame Antwort von Rekomi das Webhook-Zeitfenster von Stripe nicht blockiert.

Django-Templates escapen automatisch. Das Token {{ REKOMI_PROGRAM_ID }} wird automatisch escaped, was für eine alphanumerische Programm-ID kein Problem ist.

Erstattungen

Wenn Stripe charge.refunded sendet, ü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"]
    from yourapp.services.rekomi import track_refund
    # Resolve the original external event id (the Checkout Session id in
    # this guide) via charge["payment_intent"] in your own DB.
    # Omit refund_amount_cents for a full refund; pass minor units for a partial.
    order = Order.objects.filter(payment_intent_id=charge["payment_intent"]).first()
    if order:
        track_refund(external_event_id=order.checkout_session_id)

Verpacke das genauso in Celery wie den Conversion-Aufruf, wenn du eine asynchrone Infrastruktur hast.

Fehlerbehebung

{{ REKOMI_PROGRAM_ID }} ist in der HTML-Ausgabe leer. Der Kontextprozessor ist nicht registriert, oder die Umgebungsvariable ist nicht gesetzt. Prüfe settings.TEMPLATES[0]["OPTIONS"]["context_processors"] und deine .env.

Der S2S-Aufruf liefert 401. Bearer-Schlüssel fehlt, HMAC stimmt nicht, oder die Uhrzeit weicht ab. Prüfe, ob REKOMI_API_KEY und REKOMI_SIGNING_SECRET gesetzt sind, 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 Servers 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 löst einen HTTPError mit 400 affiliate_slug_required oder affiliate_not_found aus. 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 Stripe-Webhook läuft nach 30 Sekunden in ein Timeout. Dein Aufruf von track_conversion blockiert die Antwort. Verpacke ihn wie oben gezeigt in Celery.

requests.exceptions.SSLError. Deiner Python-Umgebung fehlt ein CA-Bundle. Installiere es mit pip install certifi oder setze REQUESTS_CA_BUNDLE.