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_SECRETContent 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
endWithout 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
endCall 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
endTurbo + 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:loadevent fires on every page transition. The Rekomi loader hydrateswindow.Rekomiexactly 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), referencewindow.Rekomidirectly — 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")
endTroubleshooting
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.