RekomiRekomiBlogPricing
Rekomi Docs
Rekomi Docs
Welcome to Rekomi
Quickstart for brandsPlans and trialsIntegrationsStripe Connect (sales tracking)Organization settingsTeam managementNotifications
Install tracking

Payment platforms

Install on StripeStripe Marketplace appInstall on PaddleInstall on BraintreeInstall on ShopifyInstall on Lemon SqueezyInstall on ChargebeeInstall on PolarInstall on RecurlyInstall on GumroadInstall on CreemInstall on Dodo PaymentsS2S API (any gateway)

Membership and course platforms

Install on MemberstackInstall on OutsetaInstall on PodiaInstall on TeachableInstall on Thinkific

Newsletter and creator platforms

Install on GhostInstall on beehiivInstall on Kit (ConvertKit)

Site builders

Install on WordPressInstall on WebflowInstall on FramerInstall on SquarespaceInstall on WixInstall on BubbleInstall on Carrd

Frameworks

Install on Vanilla JavaScriptInstall on Next.js App RouterInstall on Next.js Pages RouterInstall on React (Vite, CRA, Remix)Install on VueInstall on RailsInstall on Django

Campaigns and commissions

CampaignsCommission modelsPay per click or lead (CPC & CPL)Coupon-code attributionSub-affiliate recruitingTracking and attribution

Affiliates

Recruit affiliatesManage affiliatesAI co-pilotApply to the curated network

Money flow

SalesPayoutsMulti-currencyTax formsReports

Email

Sending domainBroadcasts
For brandsInstall tracking
|Brands|

Install on Django

Server-side S2S is the primary path. templates/base.html head install for click capture, services/rekomi.py for HMAC-signed conversion fires. Use Celery for async if firing from Stripe webhooks.

Django apps have a backend, so server-side S2S is the right conversion path (more reliable than browser pixels under ad blockers). Click capture uses a head script in templates/base.html (which every other page extends). Conversion fires happen in a small rekomi.py services module using Python's standard hmac and hashlib. If you fire from Stripe webhooks under any load, wrap the network call in a Celery (or Dramatiq) task so Stripe doesn't time out at 30 seconds.

Install the head script in templates/base.html

templates/base.html is the parent template every other page extends via {% extends 'base.html' %}. Add the head script here:

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

    {% block extra_head %}{% endblock %}

    <script>
      (function(){
        var s=document.createElement('script');
        s.src='https://js.rekomi.com/v1/rekomi.js';
        s.async=true;
        s.dataset.programId='{{ REKOMI_PROGRAM_ID }}';
        document.head.appendChild(s);
      })();
    </script>
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>

The {{ REKOMI_PROGRAM_ID }} token is hydrated via a Django context processor (next step).

Context processor for environment-specific config

Add a context processor so {{ REKOMI_PROGRAM_ID }} is available in every template:

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

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

Register it 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.
REKOMI_PROGRAM_ID = os.environ.get("REKOMI_PROGRAM_ID", "")
REKOMI_S2S_SECRET = os.environ.get("REKOMI_S2S_SECRET", "")

Now every template (including base.html) has access to {{ REKOMI_PROGRAM_ID }}.

Build a rekomi.py service for S2S conversion fires

Create yourapp/services/rekomi.py:

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 track_conversion(
    external_event_id: str,
    affiliate_slug: str,
    amount_cents: int,
    currency: str = "USD",
    customer_id: Optional[str] = None,
):
    payload = {
        "external_event_id": external_event_id,
        "affiliate_slug": affiliate_slug,
        "amount_cents": amount_cents,
        "currency": currency,
    }
    if customer_id:
        payload["customer_id"] = customer_id

    body = json.dumps(payload, separators=(",", ":"))
    signature = hmac.new(
        settings.REKOMI_S2S_SECRET.encode("utf-8"),
        body.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    response = requests.post(
        ENDPOINT,
        data=body,
        headers={
            "Content-Type": "application/json",
            "X-Rekomi-Signature": signature,
        },
        timeout=10,
    )
    response.raise_for_status()
    return response


def track_refund(external_event_id: str, reason: str = "platform_refund"):
    body = json.dumps(
        {"external_event_id": external_event_id, "refund_reason": reason},
        separators=(",", ":"),
    )
    signature = hmac.new(
        settings.REKOMI_S2S_SECRET.encode("utf-8"),
        body.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    return requests.post(
        REFUND_ENDPOINT,
        data=body,
        headers={
            "Content-Type": "application/json",
            "X-Rekomi-Signature": signature,
        },
        timeout=10,
    )

Call it from your Stripe webhook view:

# 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.error.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_email"),
            )

    return HttpResponse(status=200)

Async with Celery for production

If your Stripe webhook view does any meaningful work, wrap the track_conversion call in a Celery task so Stripe's 30-second timeout doesn't pressure your stack:

# 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 track_conversion(**kwargs)
    except Exception as exc:
        raise self.retry(exc=exc)

Then in your 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_email"),
)

Stripe gets a 200 immediately; the S2S call happens in the background with retry-on-failure.

CSRF + middleware compatibility

The Rekomi loader script is loaded via a <script src> tag (a plain GET from the browser to a third-party CDN). It doesn't trip Django's CSRF middleware.

The S2S endpoint is called server-to-server with HMAC, never going through your Django app's CSRF surface.

If you have a strict CSP middleware (e.g., django-csp), allow Rekomi:

# settings.py
CSP_SCRIPT_SRC = ("'self'", "https://js.rekomi.com")
CSP_CONNECT_SRC = ("'self'", "https://api.rekomi.com")

Quirks worth knowing

Environment variables vs Django settings. The pattern above uses os.environ.get in settings.py. If you prefer django-environ or python-decouple for cleaner env management, the pattern adapts trivially.

HMAC signing is over the raw serialized body. Use json.dumps(payload, separators=(",", ":")) with a compact separator and HMAC that exact string. Don't re-serialize between hashing and POSTing.

requests.post blocks the event loop in async views. If you're using Django 4.1+'s async views (async def my_view), use httpx.AsyncClient instead of requests. The pattern is identical.

Celery is recommended but optional. If you don't run Celery, the synchronous track_conversion call works fine for low-volume webhooks. Add timeout=10 to keep slow Rekomi responses from blocking Stripe's webhook timeout window.

Django templates auto-escape. The {{ REKOMI_PROGRAM_ID }} token is auto-escaped, which is fine for a numeric/alphanumeric program ID. If you ever pass user-controlled strings into a <script> block, use {{ value|escapejs }} to escape for JS context.

Refunds

When Stripe fires charge.refunded:

if event["type"] == "charge.refunded":
    charge = event["data"]["object"]
    from yourapp.services.rekomi import track_refund
    track_refund(external_event_id=charge["id"], reason="platform_refund")

Wrap in Celery the same way as the conversion call if you have async infrastructure.

Troubleshooting

{{ REKOMI_PROGRAM_ID }} renders as empty in HTML output. The context processor isn't registered, or the env var isn't set. Check settings.TEMPLATES[0]["OPTIONS"]["context_processors"] and your .env.

S2S call returns 401. HMAC mismatch. Triple-check the secret in settings.REKOMI_S2S_SECRET matches Rekomi's dashboard. Also confirm you're hashing the exact body you POST (no re-serialization).

Stripe webhook times out at 30 seconds. Your track_conversion call is blocking the response. Wrap in Celery as shown above.

requests.exceptions.SSLError. Your Python environment is missing CA bundle. Install via pip install certifi or set REQUESTS_CA_BUNDLE env var.

Related

  • JavaScript pixel reference
  • S2S tracking API reference
  • Install on Rails — for the Rails equivalent

Install on Rails

Server-side S2S is the primary path for Rails apps. application.html.erb head install for click capture, RekomiClient service class for HMAC-signed conversion fires.

Migrations from other platforms

Move your affiliate roster onto Rekomi from Rewardful, Tapfiliate, FirstPromoter, PartnerStack, Dub Partners, Tolt, Lemon Squeezy, or Gumroad. Self-serve importers for all eight with slug preservation and optional auto-notify.

On this page

Install the head script in templates/base.htmlContext processor for environment-specific configBuild a rekomi.py service for S2S conversion firesAsync with Celery for productionCSRF + middleware compatibilityQuirks worth knowingRefundsTroubleshootingRelated