RekomiRekomiBlogPricing
Rekomi Docs
Rekomi Docs
Welcome to Rekomi
API overviewAuthenticationOAuth 2.0Server-to-server trackingTracking script & window.RekomiTrack leads and signupsNo-code & non-Stripe checkoutsCustom domainConversion currencyCoupon code trackingSub-affiliate recruiting APIWebhooksZapierWhite-label embedMCP serverAPI reference
For developers
|Developers|

Server-to-server tracking

HMAC-signed conversion ingest for any payment gateway beyond the native Stripe, Paddle, Braintree, and Shopify connections.

Stripe, Paddle, Braintree, and Shopify connect natively (no relay to build). For any other payment gateway, or for conversions that do not flow through a connected processor at all (mobile in-app purchases, server-side trial conversions, custom event sources), use the S2S tracking endpoint to ingest them directly. This is how you track ANY gateway: if you can POST an HMAC-signed request from your backend, Rekomi records the conversion. Rekomi treats S2S conversions the same as natively-tracked ones: attribution, refund handling, payouts.

Plan tier required: Starter or higher. Trialing orgs (14-day free trial) are always accepted. An org whose plan tier is below Starter and is not trialing returns 402 with { "error": "plan_tier_required", "required": "Starter", "current": "<tier>", ... }.

Works with these platforms

These platforms ship with step-by-step S2S install guides in the Setup checklist:

Shopify logo

Shopify

Recommended for production. Subscribe to order_paid, relay handler calls /api/tracking/s2s.

Gumroad logo

Gumroad

Settings > Advanced > Ping URL. Relay handler calls /api/tracking/s2s with HMAC.

Rails logo

Rails

ApplicationController concern or background worker. OpenSSL::HMAC for signing.

Django logo

Django

View or Celery task. hmac.new(secret, ...).hexdigest() for signing.

Any backend works. S2S is just an HTTPS POST with HMAC-SHA256 signing. The Node.js example below ports 1:1 to Python, Ruby, Go, PHP, .NET, Elixir, or anything that can make HTTP requests and compute HMAC. Use this path whenever you can keep a secret server-side.

Endpoint

POST /api/tracking/s2s

Production: https://api.rekomi.com/api/tracking/s2s Staging: https://rekomi-api-staging-owaet.ondigitalocean.app/api/tracking/s2s

Headers

Authorization: Bearer rk_live_xxxxxxxxxxxxxxxxxxxxx
X-Rekomi-Signature: t=1715366423,sig=8f4e2c5b...
Content-Type: application/json

Authentication accepts either header (Authorization takes precedence when both are sent):

  • Authorization: Bearer rk_live_* (recommended) or Bearer rk_test_*: your bearer API key.
  • X-Rekomi-Api-Key: rk_live_*: legacy header. Same key, same semantics.

Plus:

  • X-Rekomi-Signature (required): HMAC-SHA256 signature in t=<unix-seconds>,sig=<hex> format. Computed as HMAC-SHA256(signing_secret, "<unix-seconds>.<raw-body-bytes>"). The signing secret is the separate rks_... value you got when creating the API key, not the bearer.

Request body

{
  "externalEventId": "purchase_abc123",
  "affiliateSlug": "jane-recommends",
  "amountCents": 9900,
  "currency": "USD",
  "customerId": "cus_external_xyz"
}
  • externalEventId (required): your unique identifier for this conversion. Used for de-duplication.
  • affiliateSlug (required): the affiliate to credit. From the affiliate's tracking URL.
  • amountCents (required): the conversion amount in minor currency units. Positive integer. Max 100,000,000 cents ($1M cap).
  • currency (optional): ISO 4217 code, exactly 3 letters. Defaults to "USD". Normalized to uppercase server-side, so usd and USD are equivalent.
  • customerId (optional): your customer identifier. Useful for joining back to your own database.

Currency

The currency field accepts a 31-code ISO 4217 allowlist (USD, EUR, GBP, JPY and 27 others), matched case-insensitively (usd and USD are equivalent). A well-formed 3-letter code that is not on the allowlist returns 400 unsupported_currency. A value that is not exactly 3 letters is treated as absent and falls back to the USD default, so send a real ISO code to avoid silently logging the wrong currency. See Conversion currency for the full list, error response shapes, and how the org's display home currency is set.

Response (success)

Two success shapes: fresh conversion vs. duplicate (replay-safe).

Fresh:

{ "ok": true, "conversionId": "c0nv0001-..." }

Duplicate (the same externalEventId was already seen for this org):

{ "ok": true, "deduped": true }

The deduped shape does not echo a conversionId; the original conversion still owns the event. Treat both as success on the caller side. Retries are safe indefinitely.

Response (error)

Error responses use one of three shapes depending on what failed:

401 Unauthorized: empty body. Triggered by:

  • missing or invalid bearer key
  • missing, malformed, or wrong HMAC signature
  • timestamp more than 300 seconds out of sync

402 Plan tier required: JSON body with an upgrade prompt:

{
  "error": "plan_tier_required",
  "required": "Starter",
  "current": "None",
  "trialing": false,
  "message": "S2S tracking requires Starter tier or higher. Upgrade in Settings > Billing."
}

400 Bad Request: JSON body with just an error code:

{ "error": "amount_out_of_range" }

The full list of 400 codes:

  • external_event_id_required: missing/empty externalEventId
  • affiliate_slug_required: missing/empty affiliateSlug
  • amount_out_of_range: amountCents is ≤ 0 or > 100,000,000
  • unsupported_currency: currency is a 3-letter code that is not on the allowlist (body also echoes the offending currency)
  • invalid_body: JSON parse failure
  • no_active_program: your org has no active program
  • affiliate_not_found: affiliateSlug does not match any link in your org

Signature computation

The signature is HMAC-SHA256 of the string {unix-seconds}.{raw-body-bytes} using your plaintext signing secret as the key. Hex-encode the result.

import crypto from "node:crypto";

function sign(body: string, signingSecret: string): string {
  const t = Math.floor(Date.now() / 1000);
  const payload = `${t}.${body}`;
  const sig = crypto.createHmac("sha256", signingSecret).update(payload).digest("hex");
  return `t=${t},sig=${sig}`;
}

Sign the exact bytes you send. The server HMACs the raw request body it receives, so you must sign and send the identical byte array. The #1 cause of a 401 on an otherwise-correct request is serializing the body twice (signing one JSON string, sending another whose whitespace or key casing differs). Serialize once to bytes, sign those bytes, send those bytes.

For example, in .NET: var body = JsonSerializer.SerializeToUtf8Bytes(payload); then HMAC over "{t}." + body and new ByteArrayContent(body) — never a second JsonSerializer.Serialize(payload) call for the request content.

Replay protection

The server rejects signatures with t= more than 300 seconds (5 minutes) old, or more than 300 seconds in the future. Keep your server's clock synced via NTP.

The server also rejects duplicate (externalEventId, organization) pairs. The second call with the same external event id returns the original conversion with deduped = true. Safe to retry indefinitely.

Example: Node.js

import crypto from "node:crypto";

const BEARER = process.env.REKOMI_API_KEY!;        // rk_live_...
const SIGNING = process.env.REKOMI_SIGNING_SECRET!; // rks_...

async function logConversion(input: {
  externalEventId: string;
  affiliateSlug: string;
  amountCents: number;
  currency?: string;
  customerId?: string;
}) {
  const body = JSON.stringify(input);
  const t = Math.floor(Date.now() / 1000);
  const sig = crypto.createHmac("sha256", SIGNING).update(`${t}.${body}`).digest("hex");
  const res = await fetch("https://api.rekomi.com/api/tracking/s2s", {
    method: "POST",
    headers: {
      "X-Rekomi-Api-Key": BEARER,
      "X-Rekomi-Signature": `t=${t},sig=${sig}`,
      "Content-Type": "application/json",
    },
    body,
  });
  if (!res.ok) throw new Error(`Rekomi S2S failed: ${res.status} ${await res.text()}`);
  return await res.json();
}

Example: Python

import hmac, hashlib, time, json, requests

BEARER = "rk_live_..."
SIGNING = "rks_..."

def log_conversion(external_event_id, affiliate_slug, amount_cents, currency="USD"):
    body = json.dumps({
        "externalEventId": external_event_id,
        "affiliateSlug": affiliate_slug,
        "amountCents": amount_cents,
        "currency": currency,
    }, separators=(",", ":"))
    t = int(time.time())
    sig = hmac.new(SIGNING.encode(), f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
    r = requests.post("https://api.rekomi.com/api/tracking/s2s", data=body, headers={
        "X-Rekomi-Api-Key": BEARER,
        "X-Rekomi-Signature": f"t={t},sig={sig}",
        "Content-Type": "application/json",
    })
    r.raise_for_status()
    return r.json()

Tracking leads (signups)

For Stripe and Paddle, leads are captured automatically, with no extra code, so you usually do not need this endpoint there. Use it for Braintree, Shopify, or any other gateway driven from your backend: post a free signup (not a sale) to POST /api/tracking/lead with the same Bearer API key + X-Rekomi-Signature HMAC auth shown above. A lead ties a customer email to the referral before payment, so a later sale is credited even if the click cookie is gone. See Track leads and signups for the body shape, the browser Rekomi.convert() equivalent, and how the email-match fallback works.

When to use S2S vs the edge redirect

  • S2S when conversions originate server-side (mobile app purchases, custom checkout flows, server-side trial conversions).
  • Edge redirect (the /r/{slug} tracking link with first-party cookie) when conversions originate from a browser click on a referral link.
  • Browser pixel when your checkout is not Stripe AND you cannot run server-side code (Webflow, Memberstack, Lemon Squeezy, ConvertKit Commerce).

For most subscription products, S2S is the more reliable path because it does not depend on the browser cookie surviving across the entire conversion funnel.

Refunding conversions

Pixel and S2S conversions are refunded through POST /api/tracking/refund. Stripe-driven conversions are refunded automatically by the charge.refunded webhook and do not need this endpoint.

POST /api/tracking/refund

Same HMAC + bearer auth model as the S2S endpoint above. Body:

{
  "externalEventId": "purchase_abc123",
  "refundAmountCents": 9900
}
  • externalEventId (required): the original conversion's external event id (camelCase, same convention as the S2S body).
  • refundAmountCents (optional): partial refund in minor units. Omit for a full refund.

Response (full refund):

{
  "ok": true,
  "conversionId": "c0nv0001-...",
  "refundedAmountCents": 9900,
  "cumulativeRefundedAmountCents": 9900,
  "isFullRefund": true
}

Partial refunds accumulate: subsequent refunds add to cumulativeRefundedAmountCents and only flip the conversion to status Refunded once the cumulative total reaches the original amount. Commission reversal is proportional; refunding 40% of the order amount reverses 40% of the commission.

Error responses

HTTPerrorCause
401(empty body)Missing / invalid bearer / HMAC signature / timestamp out of sync
402plan_tier_requiredOrg below Starter and not trialing
413payload_too_largeRequest body exceeds 4096 bytes
404conversion_not_foundNo conversion matches the external event id for this org
422external_event_id_requiredMissing / empty
422refund_amount_invalid≤ 0
422already_fully_refundedThe conversion has already been refunded in full
422amount_exceeds_originalThis refund would push cumulative refunded past the conversion amount

Signature

Identical to the S2S signing computation. Reuse the same HMAC helper.

const body = JSON.stringify({
  externalEventId: "purchase_abc123",
  refundAmountCents: 9900,
});
const t = Math.floor(Date.now() / 1000);
const sig = crypto.createHmac("sha256", SIGNING).update(`${t}.${body}`).digest("hex");

await fetch("https://api.rekomi.com/api/tracking/refund", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${BEARER}`,
    "X-Rekomi-Signature": `t=${t},sig=${sig}`,
    "Content-Type": "application/json",
  },
  body,
});

Idempotency and replays

Refund replays for an already-fully-refunded conversion return 422 already_fully_refunded rather than re-decrementing commission. Partial-refund overflows are clamped: if refundAmountCents plus prior refunds would exceed the conversion amount, the request fails with 422 amount_exceeds_original. Always send refundAmountCents equal to the actual refund issued by your processor.

OAuth 2.0

Authorization Code + PKCE flow for third-party app integrations.

Tracking script & window.Rekomi

Install the Rekomi browser loader and read the referral context (affiliate + campaign) on page load, in real time.

On this page

Works with these platformsEndpointHeadersRequest bodyCurrencyResponse (success)Response (error)Signature computationReplay protectionExample: Node.jsExample: PythonTracking leads (signups)When to use S2S vs the edge redirectRefunding conversionsError responsesSignatureIdempotency and replays