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 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.

Rails apps already have a server, so the cleanest conversion path is server-side S2S (not browser pixel). Click capture still uses the browser-side head script for cookie setting; conversions fire from your Rails backend via a small RekomiClient service class that handles HMAC signing. The Rekomi loader is compatible with Turbo and Stimulus out of the box.

Install the head script in application.html.erb

The asset pipeline (Propshaft / Sprockets / Importmaps / jsbundling-rails) manages first-party JS only. Third-party scripts go in the layout as raw HTML.

Edit 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 %>

    <script>
      (function(){
        var s=document.createElement('script');
        s.src='https://js.rekomi.com/v1/rekomi.js';
        s.async=true;
        s.dataset.programId='<%= Rails.application.credentials.dig(:rekomi, :program_id) %>';
        document.head.appendChild(s);
      })();
    </script>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

Use Rails.application.credentials.dig(:rekomi, :program_id) to keep the program ID out of source control. Add it via rails credentials:edit:

rekomi:
  program_id: YOUR_PROGRAM_ID
  s2s_secret: YOUR_S2S_SECRET

Content Security Policy

If config/initializers/content_security_policy.rb has restrictive directives, allow Rekomi:

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

Without script_src permission, the browser silently blocks the loader. Without connect_src, the click and convert POSTs fail with a CSP violation visible in the browser console.

Build a RekomiClient service for S2S conversion fires

Create app/services/rekomi_client.rb:

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)
    payload = {
      external_event_id: external_event_id,
      affiliate_slug: affiliate_slug,
      amount_cents: amount_cents,
      currency: currency,
      customer_id: customer_id,
    }.compact

    body = payload.to_json
    secret = Rails.application.credentials.dig(:rekomi, :s2s_secret)
    signature = OpenSSL::HMAC.hexdigest("SHA256", secret, 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",
      "X-Rekomi-Signature" => signature,
    })
    request.body = body

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

  def self.refund(external_event_id:, reason: "platform_refund")
    body = { external_event_id: external_event_id, refund_reason: reason }.to_json
    secret = Rails.application.credentials.dig(:rekomi, :s2s_secret)
    signature = OpenSSL::HMAC.hexdigest("SHA256", secret, body)

    uri = URI(REFUND_ENDPOINT)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    request = Net::HTTP::Post.new(uri.path, {
      "Content-Type" => "application/json",
      "X-Rekomi-Signature" => signature,
    })
    request.body = body

    http.request(request)
  end
end

Call it from your Stripe webhook handler (or wherever your "subscription created" logic lives):

# 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_email,
      )
    end

    head :ok
  end
end

Turbo + Stimulus compatibility

Rails 7 ships with Turbo (full page replacement via XHR + morphdom) and Stimulus (lightweight JS controllers) by default. The Rekomi loader is compatible with both:

  • Turbo: Turbo's turbo:load event fires on every page transition. The Rekomi loader hydrates window.Rekomi exactly once on initial document load and persists through Turbo navigations. The cookie stays set; no per-navigation re-fire needed.
  • Stimulus: Stimulus controllers don't interact with window.Rekomi. If you want to fire convert from a Stimulus controller (e.g., a "thanks-page" controller), reference window.Rekomi directly — it's globally available after the initial load.

No special integration needed for either.

Why server-side S2S over browser pixel

Rails apps have a backend. Server-side S2S is more reliable than browser pixel firing because:

  • Ad blockers and analytics blockers can't block server-side calls. uBlock Origin and Brave Shields block convert POSTs to many tracking endpoints by default; server-side calls go around all of that.
  • Refund handling is cleaner. When Stripe fires charge.refunded, your Rails webhook is the natural place to relay the refund event to Rekomi.
  • Order data is canonical. Browser-pixel convert fires can be tampered with via DevTools; server-side fires use data from your database / Stripe Session.

Quirks worth knowing

Credentials over ENV vars. Rails 7's encrypted credentials are the recommended secret-storage pattern. ENV["REKOMI_PROGRAM_ID"] works too if your deploy uses 12-factor env vars.

CSP must allow the loader AND the endpoint. Most Rails developers forget one or the other. Add both https://js.rekomi.com to script_src AND https://api.rekomi.com to connect_src.

Turbo Drive vs Turbo Frames. Both work. Turbo Drive does full-page-replacement navigation (still preserves window.Rekomi); Turbo Frames does partial-page swaps (also preserves it, since the document <head> doesn't reload).

Sidekiq or GoodJob for async S2S. If you fire S2S from a Stripe webhook handler that needs to return 200 fast (Stripe times out at 30s), wrap the RekomiClient.track call in a Sidekiq job or GoodJob. The HMAC signing is fast; the network call is what can stall.

HMAC signing is over the raw body. If you use body.to_json to construct the payload and then HMAC the same string, that's correct. If you construct the body in one place and HMAC a re-serialized version in another, the signatures won't match — Rekomi's verification will reject the call.

Refunds

When Stripe fires charge.refunded:

if event.type == "charge.refunded"
  charge = event.data.object
  # Find the original session id from your DB lookup, or use charge.id
  RekomiClient.refund(external_event_id: charge.id, reason: "platform_refund")
end

Troubleshooting

Script in layout but no clicks captured. CSP blocking. Check the browser console for Refused to load the script errors and add js.rekomi.com to script_src.

S2S call returns 401. HMAC signature mismatch. Double-check the secret in credentials matches Rekomi Settings > Tracking > S2S secret. Also confirm you're hashing the EXACT raw body you POST, not a re-serialized version.

S2S call returns 200 but no conversion in Rekomi. affiliate_slug was nil. The Stripe Session's client_reference_id field was empty — meaning the browser didn't pass the referral during checkout creation. Check the head install on the page the affiliate sent traffic to.

Convert fires for orders without affiliates. Add a guard before calling RekomiClient.track:

return unless session.client_reference_id.present?

Otherwise you'll waste API calls on every order, including organic ones.

Related

  • JavaScript pixel reference
  • S2S tracking API reference
  • Install on Stripe — for the Stripe-side webhook configuration

Install on Vue

Vite Vue 3 installs in index.html. Nuxt 3 uses useHead in app.vue or the script array in nuxt.config.ts. Convert fires from onMounted on the success page.

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.

On this page

Install the head script in application.html.erbContent Security PolicyBuild a RekomiClient service for S2S conversion firesTurbo + Stimulus compatibilityWhy server-side S2S over browser pixelQuirks worth knowingRefundsTroubleshootingRelated