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.