Rekomi Docs
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 referral capture; 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 %>

    <!-- Rekomi tracking -->
    <script async src="https://api.rekomi.com/api/v1/r/loader.js"
            data-program-id="<%= Rails.application.credentials.dig(:rekomi, :program_id) %>"></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>
    <%= yield %>
  </body>
</html>

Use Rails.application.credentials.dig(:rekomi, :program_id) to keep config out of source control. Add your keys via rails credentials:edit. 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: YOUR_PROGRAM_ID
  api_key: rk_live_xxxxxxxxxxxxxxxxx    # Bearer, authenticates the request
  signing_secret: rks_xxxxxxxxxxxxxxx   # signs the request body

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://api.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 POST fails with a CSP violation visible in the browser console.

Build a RekomiClient service for S2S conversion fires

Create app/services/rekomi_client.rb. The S2S endpoint authenticates with your Bearer API key AND an X-Rekomi-Signature header in t=<unix-seconds>,sig=<hex> format, where the signature is HMAC-SHA256(signing_secret, "<unix-seconds>.<raw-body>"). Sign the exact bytes you send. Body fields are camelCase.

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, customer_email: nil)
    body = {
      externalEventId: external_event_id,
      affiliateSlug: affiliate_slug,
      amountCents: amount_cents,
      currency: currency,
      customerId: customer_id,
      customerEmail: customer_email,
    }.compact.to_json

    post(ENDPOINT, body)
  end

  def self.refund(external_event_id:, refund_amount_cents: nil)
    # Omit refundAmountCents for a full refund; pass minor units for a partial.
    body = { externalEventId: external_event_id, refundAmountCents: refund_amount_cents }.compact.to_json
    post(REFUND_ENDPOINT, body)
  end

  def self.post(endpoint, body)
    creds = Rails.application.credentials.dig(:rekomi)
    t = Time.now.to_i
    signature = OpenSSL::HMAC.hexdigest("SHA256", creds[:signing_secret], "#{t}.#{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",
      "Authorization" => "Bearer #{creds[:api_key]}",
      "X-Rekomi-Signature" => "t=#{t},sig=#{signature}",
    })
    request.body = body

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

The webhook below reads the affiliate slug from the Checkout Session's client_reference_id, so stamp it when you create the session. The slug lives in the buyer's browser (window.Rekomi.getReferral()); forward it in the request that starts checkout, then set it server-side:

# app/controllers/checkouts_controller.rb
# params[:rekomi_referral] is window.Rekomi.getReferral(), sent by your checkout button.
session = Stripe::Checkout::Session.create(
  mode: "subscription",
  line_items: [{ price: params[:price_id], quantity: 1 }],
  client_reference_id: params[:rekomi_referral].presence,
  success_url: success_url,
  cancel_url: cancel_url,
)

Note that client_reference_id is read only by YOUR webhook below. Rekomi's native Stripe integration does not read it; that path uses rekomi_affiliate_slug subscription metadata instead.

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,
        customer_email: session.customer_details&.email,
      ) if session.client_reference_id.present?
    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: the Rekomi loader hydrates window.Rekomi once on initial document load and persists through Turbo navigations. The referral stays captured; no per-navigation re-fire needed.
  • Stimulus: if you want to read the referral from a Stimulus controller (e.g., to forward it into a checkout request), reference window.Rekomi.getReferral() 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 firing because:

  • Ad blockers and analytics blockers can't block server-side calls. uBlock Origin and Brave Shields block many browser tracking POSTs 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 to Rekomi.
  • Order data is canonical. Server-side fires use data from your database / Stripe Session rather than anything read in the browser.

Quirks worth knowing

Two secrets, not one. The Bearer API key (rk_live_...) authenticates the request; the separate signing secret (rks_...) signs the body. Both come from creating a Rekomi API key.

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

CSP must allow the loader AND the endpoint. Add https://api.rekomi.com to both script_src (for the loader) and connect_src (for the click POST).

Clock sync matters. Rekomi rejects a signature whose t= timestamp is more than 300 seconds out of sync. Keep the host on NTP.

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 is over "#{t}.#{body}". Build the JSON body once, sign that exact string, and POST the same bytes. Re-serializing between signing and POSTing makes the signatures mismatch and Rekomi rejects the call.

Refunds

When Stripe fires charge.refunded, pass the SAME external event id you sent when recording the sale (the Checkout Session id in this guide). Rekomi looks up the conversion strictly by that id, so passing charge.id returns 404 and the commission is never reversed. Map the charge back to the original id through your own records:

if event.type == "charge.refunded"
  charge = event.data.object
  # Use the SAME external event id you sent when recording the sale
  # (the Checkout Session id in this guide). Look it up from your DB
  # via charge.payment_intent. Omit refund_amount_cents for a full refund.
  order = Order.find_by(payment_intent_id: charge.payment_intent)
  RekomiClient.refund(external_event_id: order.checkout_session_id) if order
end

Troubleshooting

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

S2S call returns 401. Bearer key missing, HMAC mismatch, or clock skew. Confirm both api_key and signing_secret are in credentials, the signature uses the t=<unix>,sig=<hex> format over "#{t}.#{body}", you're hashing the exact body you POST, and the host clock is within 300 seconds of real time.

S2S call returns 200 with deduped: true. You already recorded a conversion with this externalEventId; the call was safely ignored.

S2S call raises with a 400 affiliate_slug_required or affiliate_not_found. The Stripe Session's client_reference_id was empty or stale, meaning the browser didn't carry a valid referral into checkout. Check the head install on the page the affiliate sent traffic to.

S2S call returns 409. Your Rekomi workspace has a payment provider connected, which already records conversions automatically. Use one source only; disconnect the provider from its page under the Connect payment gateway step if you want to send S2S.

S2S call returns 402. S2S requires the Starter plan or higher (trials included).

Firing S2S for orders without affiliates. Guard before calling RekomiClient.track (as shown, if session.client_reference_id.present?) so you don't waste API calls on organic orders.