Saviliate Vendor Integration

Tracking API

Everything you need to report affiliate conversions from your website back to Saviliate. Works with any language or framework — it's just three HTTP endpoints secured with HMAC request signing.

How It Works #

Saviliate handles affiliate link clicks entirely on its own servers. Your only job is to capture a ref value when a visitor arrives, then send it back when a conversion happens.

Affiliate
Shares their link
The affiliate promotes your product using a unique Saviliate link.
https://saviliate.com/t/4KqzOAXfvcno0VG
Saviliate
Records the click & redirects
Saviliate logs the click internally, creates a unique affiliate click record with an identifier, then sends the visitor to your site with that identifier appended as ?saviliate=.
https://yourstore.com/product/?saviliate=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
Your Website
Captures and stores the ref
Read ?saviliate= from the URL and persist it in an HttpOnly cookie so it survives navigation to checkout or registration. Cookie name: sv_ref. Recommended TTL: 30 days.
Your Backend
Reports the conversion
When the user registers or pays, POST to Saviliate from your server with the ref value. Saviliate ties it back to the original affiliate and calculates commission.
Saviliate
Pays the affiliate
Commission is recorded and credited to the affiliate automatically.
You never call a click-tracking endpoint for web traffic. Saviliate records the click before the visitor even reaches your site. Your integration starts at step 3 — capturing the ref from the URL. The /track/click endpoint exists only for mobile apps or platforms that can't participate in the redirect flow.

Authentication #

Every API request must include three HTTP headers. Find your credentials in Saviliate Dashboard → Brand Settings → API Keys.

Header Value Notes
Authorization Bearer {secret_key} Your brand's secret key. Saviliate resolves your brand from this value. Never expose in frontend code.
X-TIMESTAMP Unix timestamp (seconds) Current time in seconds. Requests older than 5 minutes are rejected.
X-SIGNATURE HMAC-SHA256 hex digest Computed from the timestamp + raw request body, signed with your secret key. See Signing Requests below.
Server-side only. Domain-locked. All three headers must only be sent from your backend server — never from JavaScript in the browser. Additionally, Saviliate verifies that every request originates from the domain registered against your brand. Requests from unregistered domains are rejected with 403.

Signing Requests #

Every request body is signed using HMAC-SHA256 so Saviliate can verify the payload was not tampered with in transit and that the request is genuinely fresh.

Signature formula

X-SIGNATURE = HMAC-SHA256( X-TIMESTAMP + raw JSON body , secret_key )
X-TIMESTAMP
Unix epoch seconds, e.g. 1742240400 — send the same value in the header and in the signature input
raw JSON body
The exact bytes you send as the request body — no extra whitespace or key reordering
secret_key
Your brand's secret key (same value sent in Authorization: Bearer ...)
String-concatenate timestamp and body — no separator. The signed string is timestamp + body, where + means direct concatenation. For example, if the timestamp is 1742240400 and the body is {"ref":"abc",...}, sign the string 1742240400{"ref":"abc",...}.

Implementation examples

/**
 * Build the three required auth headers for a Saviliate API request.
 *
 * @param  string  $secretKey  Your brand secret key
 * @param  string  $jsonBody   The exact JSON string you'll send as the body
 * @return array               Headers array, merge with your HTTP client options
 */
function saviliateHeaders(string $secretKey, string $jsonBody): array
{
    $timestamp = (string) time();

    $signature = hash_hmac(
        'sha256',
        $timestamp . $jsonBody,  // concatenate — no separator
        $secretKey
    );

    return [
        'Authorization' => 'Bearer ' . $secretKey,
        'X-TIMESTAMP'   => $timestamp,
        'X-SIGNATURE'   => $signature,
        'Content-Type'  => 'application/json',
    ];
}

// Usage with Laravel HTTP client:
$body = json_encode([
    'ref'              => $ref,
    'external_user_id' => (string) $order->user_id,
    'order_id'         => (string) $order->id,
    'order_amount'     => $order->subtotal,
]);

Http::withHeaders(saviliateHeaders(env('SAVILIATE_SECRET_KEY'), $body))
    ->withBody($body, 'application/json')
    ->post('https://saviliate.com/api/v1/track/commission');
const crypto = require('crypto');

/**
 * Build auth headers for a Saviliate API request.
 * @param {string} secretKey - Your brand secret key
 * @param {string} jsonBody  - The exact JSON string being sent
 */
function saviliateHeaders(secretKey, jsonBody) {
    const timestamp = String(Math.floor(Date.now() / 1000));

    const signature = crypto
        .createHmac('sha256', secretKey)
        .update(timestamp + jsonBody)  // concatenate — no separator
        .digest('hex');

    return {
        'Authorization': `Bearer ${secretKey}`,
        'X-TIMESTAMP':   timestamp,
        'X-SIGNATURE':   signature,
        'Content-Type':  'application/json',
    };
}

// Usage with fetch:
const body = JSON.stringify({
    ref:              ref,
    external_user_id: String(order.userId),
    order_id:         String(order.id),
    order_amount:     order.subtotal,
});

await fetch('https://saviliate.com/api/v1/track/commission', {
    method:  'POST',
    headers: saviliateHeaders(process.env.SAVILIATE_SECRET_KEY, body),
    body,
});
import hashlib, hmac, json, time, requests, os

def saviliate_headers(secret_key: str, json_body: str) -> dict:
    """Build auth headers for a Saviliate API request."""
    timestamp = str(int(time.time()))

    sig = hmac.new(
        secret_key.encode(),
        (timestamp + json_body).encode(),  # concatenate — no separator
        hashlib.sha256
    ).hexdigest()

    return {
        'Authorization': f'Bearer {secret_key}',
        'X-TIMESTAMP':   timestamp,
        'X-SIGNATURE':   sig,
        'Content-Type':  'application/json',
    }

# Usage:
body = json.dumps({
    'ref':              ref,
    'external_user_id': str(order.user_id),
    'order_id':         str(order.id),
    'order_amount':     order.subtotal,
}, separators=(',', ':'))  # compact JSON — no extra whitespace

requests.post(
    'https://saviliate.com/api/v1/track/commission',
    data=body,
    headers=saviliate_headers(os.environ['SAVILIATE_SECRET_KEY'], body),
)
require 'openssl'
require 'json'
require 'net/http'

def saviliate_headers(secret_key, json_body)
    timestamp = Time.now.to_i.to_s

    sig = OpenSSL::HMAC.hexdigest(
        'SHA256',
        secret_key,
        timestamp + json_body  # concatenate — no separator
    )

    {
        'Authorization' => "Bearer #{secret_key}",
        'X-TIMESTAMP'   => timestamp,
        'X-SIGNATURE'   => sig,
        'Content-Type'  => 'application/json',
    }
end

# Usage:
body = { ref: ref, external_user_id: order.user_id.to_s,
         order_id: order.id.to_s, order_amount: order.subtotal }.to_json

uri  = URI('https://saviliate.com/api/v1/track/commission')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

req      = Net::HTTP::Post.new(uri)
saviliate_headers(ENV['SAVILIATE_SECRET_KEY'], body).each { |k,v| req[k] = v }
req.body = body

http.request(req)
# Shell helper — generate headers and fire a commission request

SECRET_KEY="your_secret_key_here"
BODY='{"ref":"9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d","external_user_id":"user_1042","order_id":"ord_8801","order_amount":49.00}'
TS=$(date +%s)
SIG=$(echo -n "${TS}${BODY}" | openssl dgst -sha256 -hmac "${SECRET_KEY}" | awk '{print $2}')

curl -X POST https://saviliate.com/api/v1/track/commission \
  -H "Authorization: Bearer ${SECRET_KEY}" \
  -H "X-TIMESTAMP: ${TS}" \
  -H "X-SIGNATURE: ${SIG}" \
  -H "Content-Type: application/json" \
  -d "${BODY}"
Sign the exact bytes you send. Serialize your JSON body to a string first, sign that exact string, then send it as the body unchanged. Reformatting or reordering keys after signing will produce a mismatched signature and a 401 response.

Integration Mode #

Every brand on Saviliate has a mode — either Live or ✦ Test. The mode controls whether API calls are processed as real events or treated as sandbox calls for integration testing.

Toggle your brand's mode from Saviliate Dashboard → Brand Details → Integration Mode. Your secret_key, public_key, and webhook_secret are identical in both modes — you do not need different credentials for testing.

Real events. Real commissions. Every successful API call creates actual records: clicks are logged, customers are registered, commissions are credited to affiliates, and vendor balances are debited. Use this mode only when your integration is production-ready.
Sandbox mode — no records created. When your brand is in Test mode, the API accepts and validates requests exactly as it would in Live mode (authentication, HMAC signature, domain check, required fields), but no database records are written and no balances are modified. Each endpoint returns a fixed test response so you can verify your integration end-to-end without side effects.

Test responses by endpoint

All test responses include "mode": "test" and a "note" field to make them unambiguous in your logs. HTTP status codes mirror what the real endpoint would return on success.

POST /track/commission — test response ✦ test
{
  "status":        "success",
  "message":       "Commission recorded successfully",
  "commission_id": "test_commission_id",
  "mode":          "test",
  "note":          "Brand is in test mode. No records were created."
}
POST /track/customer — test response ✦ test
{
  "status": "success",
  "message": "Customer recorded successfully",
  "mode":    "test",
  "note":    "Brand is in test mode. No records were created."
}
POST /track/click — test response ✦ test
{
  "status": "success",
  "message": "Click recorded",
  "mode":    "test",
  "note":    "Brand is in test mode. No records were created."
}
Authentication is enforced in both modes. Requests with missing headers, an invalid signature, an expired timestamp, or an unrecognised domain will still return the appropriate error response — even in Test mode. Only requests that would otherwise succeed are intercepted and replaced with the test response above.

Recommended integration workflow

Step 1
Build in Test mode
Keep your brand in Test mode while writing your integration. Fire real requests from your server — check that you get the test response shape shown above, with no errors.
Step 2
Verify authentication end-to-end
Intentionally send a wrong signature or an expired timestamp to confirm your error-handling path catches 401 and 400 correctly — without blocking real users.
Step 3
Switch to Live
Once you're satisfied, go to Saviliate Dashboard → Brand Details → Integration Mode and flip the toggle to Live. From that point, all events create real records and commissions.

Step 1 — Capture the ref #

When a visitor arrives on any page of your site, check whether the URL contains a ?saviliate= query parameter. If it does, that value is the click UID you'll use for all reporting. It looks like a UUID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d.

The best place to do this is in a middleware or request filter that runs on every incoming web request, so you never miss a landing page.

// app/Http/Middleware/CaptureSaviliateRef.php

namespace App\Http\Middleware;

use Closure, Illuminate\Http\Request;

class CaptureSaviliateRef
{
    public function handle(Request $request, Closure $next)
    {
        $uuid = $request->query('saviliate');

        if ($uuid && preg_match('/^[a-f0-9\-]{36}$/i', $uuid)) {
            // 30 days, HttpOnly, Secure, SameSite=Lax
            cookie()->queue(
                'sv_ref', $uuid, 43200, '/', null, true, true, false, 'Lax'
            );
        }

        return $next($request);
    }
}
// middleware/captureSaviliateRef.js

const UUID_RE = /^[a-f0-9\-]{36}$/i;

function captureSaviliateRef(req, res, next) {
    const uuid = req.query.saviliate;

    if (uuid && UUID_RE.test(uuid)) {
        res.cookie('sv_ref', uuid, {
            maxAge:   30 * 24 * 60 * 60 * 1000,
            httpOnly: true,
            secure:   process.env.NODE_ENV === 'production',
            sameSite: 'lax',
        });
    }

    next();
}

module.exports = captureSaviliateRef;

// Register in app.js:  app.use(captureSaviliateRef);
# saviliate/middleware.py

import re

UUID_RE = re.compile(r'^[a-f0-9\-]{36}$', re.IGNORECASE)

class CaptureSaviliateRef:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)

        uuid = request.GET.get('saviliate')
        if uuid and UUID_RE.match(uuid):
            response.set_cookie(
                'sv_ref', uuid,
                max_age  = 30 * 24 * 60 * 60,
                httponly = True,
                secure   = True,
                samesite = 'Lax',
            )

        return response

# settings.py MIDDLEWARE list: 'saviliate.middleware.CaptureSaviliateRef'
# app/controllers/concerns/saviliate_tracking.rb

module SaviliateTracking
  extend ActiveSupport::Concern

  UUID_RE = /\A[a-f0-9\-]{36}\z/i

  included do
    before_action :capture_saviliate_ref
  end

  private

  def capture_saviliate_ref
    uuid = params[:saviliate]
    return unless uuid&.match?(UUID_RE)

    cookies[:sv_ref] = {
      value:     uuid,
      expires:   30.days,
      httponly:  true,
      secure:    Rails.env.production?,
      same_site: :lax,
    }
  end
end

# ApplicationController: include SaviliateTracking
// Call early in your request bootstrap / front controller

function saviliate_capture_ref(): void
{
    $uuid = $_GET['saviliate'] ?? null;

    if (!$uuid || !preg_match('/^[a-f0-9\-]{36}$/i', $uuid)) {
        return;
    }

    setcookie('sv_ref', $uuid, [
        'expires'  => time() + (30 * 86400),
        'path'     => '/',
        'secure'   => true,
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
}

saviliate_capture_ref();
Always validate the UUID format before storing it. The regex /^[a-f0-9\-]{36}$/i ensures only a valid UUID is ever written to the cookie. Reject anything else silently.

Step 2 — Persist the ref Across the Session #

The ref only appears in the URL on the first landing page visit. It needs to survive navigation to your checkout page, registration form, or any other conversion point.

Reading the ref when you need it

$ref = $request->cookie('sv_ref'); // null if no affiliate session
const ref = req.cookies['sv_ref'] ?? null; // requires cookie-parser
ref = request.COOKIES.get('sv_ref')  # None if not present
ref = cookies[:sv_ref]  # nil if not present
$ref = $_COOKIE['sv_ref'] ?? null;
Persist in your database for SaaS and subscription products. If your conversion happens days or weeks after the initial visit (free trial, payment webhook), store the ref on the user record at registration time. Payment webhooks (Stripe, Paddle, etc.) do not carry browser cookies — the ref must come from your database.

Step 3 — Report the Conversion #

Once a conversion happens, POST to the appropriate endpoint from your server. Never from the browser. Include the ref as ref — that's what links the conversion to the affiliate.

If no sv_ref cookie is present (the visitor did not arrive via an affiliate link), skip the report entirely.

POST /track/commission #

Report a completed purchase. Commission is calculated server-side based on your brand's configured percentage and fixed bonus. This is the primary conversion endpoint for e-commerce and SaaS.

POST https://saviliate.com/api/v1/track/commission

Required Headers

Header Value
Authorization Bearer {secret_key}
X-TIMESTAMP Unix timestamp in seconds
X-SIGNATURE HMAC-SHA256 of timestamp + body
Content-Type application/json

Body Parameters

Parameter Type Description
ref required string The click UID from ?saviliate=. This is what ties the commission to the affiliate.
external_user_id required string Your internal user or customer ID. Used for idempotency — Saviliate won't double-pay an affiliate for the same user on non-recurring plans.
order_id required string Your internal order or transaction ID. Stored against the commission record for your reference.
order_amount required numeric The order value commission is calculated from. Must be ≥ 0. Pass 0 for non-monetary conversion events.
Test mode: no records written. If your brand is in ✦ Test mode, this endpoint validates your request normally but returns {"status":"success","message":"Commission recorded successfully","commission_id":"test_commission_id","mode":"test","note":"Brand is in test mode. No records were created."} without creating any commission, transaction, or earnings record. See Integration Mode.
Example Request
POST /api/v1/track/commission
Authorization: Bearer your_secret_key_here
X-TIMESTAMP:   1742240400
X-SIGNATURE:   a3f8c2d1e9b045...  (HMAC-SHA256 hex)
Content-Type:  application/json

{
  "ref":              "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "external_user_id": "user_1042",
  "order_id":         "ord_8801",
  "order_amount":     49.00
}

Responses

201 Created — commission recorded
{
  "status":        "success",
  "message":       "Commission recorded successfully",
  "commission_id": 4821
}
200 OK — duplicate ignored
{
  "status":        "error",
  "message":       "Duplicate commission
  ignored (idempotent)",
  "commission_id": 4821
}
Recurring vs non-recurring products. For non-recurring products, duplicate commission requests for the same (external_user_id, affiliate_id, brand_id) combination are silently ignored — it is safe to call from both a webhook and a confirmation page without double-paying. For recurring products, each call creates a new commission record; the fixed bonus is only applied on the first commission, subsequent ones use the percentage only.

POST /track/customer #

Report a new customer registration attributed to an affiliate. Call this when a user signs up so Saviliate has a record of who registered — their name and email appear in the affiliate's dashboard. Typically called alongside /track/commission at the same event.

POST https://saviliate.com/api/v1/track/customer

Required Headers

Header Value
Authorization Bearer {secret_key}
X-TIMESTAMP Unix timestamp in seconds
X-SIGNATURE HMAC-SHA256 of timestamp + body
Content-Type application/json

Body Parameters

Parameter Type Description
ref required string The click UID from ?saviliate=.
external_user_id required string Your internal user ID. Duplicate registrations for the same user are silently ignored.
email required string Customer's email address.
name required string Customer's full name.
Test mode: no records written. If your brand is in ✦ Test mode, this endpoint validates your request normally but returns {"status":"success","message":"Customer recorded successfully","mode":"test","note":"Brand is in test mode. No records were created."} without creating any customer record. See Integration Mode.
Example Request
POST /api/v1/track/customer
Authorization: Bearer your_secret_key_here
X-TIMESTAMP:   1742240400
X-SIGNATURE:   a3f8c2d1e9b045...  (HMAC-SHA256 hex)
Content-Type:  application/json

{
  "ref":              "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "external_user_id": "user_1042",
  "email":            "[email protected]",
  "name":             "Jane Doe"
}

Response

202 Accepted — customer recorded
{
  "status":  "success",
  "message": "Customer recorded successfully"
}
200 OK — already recorded
{
  "status":  "success",
  "message": "Affiliate customer already
  recorded for this customer..."
}

POST /track/click #

Manually record a click event. This endpoint is not needed for standard web integrations — Saviliate records web clicks automatically before the visitor reaches your site. Use this only for platforms where the redirect flow isn't available, such as mobile apps or native desktop apps.

POST https://saviliate.com/api/v1/track/click

Required Headers

Header Value
Authorization Bearer {secret_key}
X-TIMESTAMP Unix timestamp in seconds
X-SIGNATURE HMAC-SHA256 of timestamp + body
Content-Type application/json

Body Parameters

Parameter Type Description
ref required string The affiliate click UID (from the Saviliate redirect) or the affiliate's username.
ip_address required string The visitor's IP address, captured server-side.
user_agent required string The visitor's User-Agent string.
Test mode: no records written. If your brand is in ✦ Test mode, this endpoint validates your request normally but returns {"status":"success","message":"Click recorded","mode":"test","note":"Brand is in test mode. No records were created."} without creating a click record. See Integration Mode.
Example Request
POST /api/v1/track/click
Authorization: Bearer your_secret_key_here
X-TIMESTAMP:   1742240400
X-SIGNATURE:   a3f8c2d1e9b045...  (HMAC-SHA256 hex)
Content-Type:  application/json

{
  "ref":        "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "ip_address": "203.0.113.42",
  "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0...)"
}
Web integrations: do not call this endpoint. Calling it for visitors who arrived through the standard redirect will create a duplicate click record. This endpoint is for out-of-band environments only.

Response

202 Accepted — click queued
{
  "status":  "success",
  "message": "Click recorded"
}
404 Not Found — bad ref
{
  "status":  "error",
  "message": "Invalid ref: ..."
}

Responses & Errors #

400 Bad Request — missing headers
{
  "status":  "error",
  "message": "Bad Request - Missing
  required headers"
}
401 Unauthorized — bad credentials
{
  "status":  "error",
  "message": "Unauthorized - Invalid
  credentials"
}
401 Unauthorized — expired request
{
  "status":  "error",
  "message": "Request expired"
}
401 Unauthorized — bad signature
{
  "status":  "error",
  "message": "Invalid signature"
}
403 Forbidden — domain mismatch
{
  "status":  "error",
  "message": "Invalid request origin"
}
404 Not Found — bad ref
{
  "status":  "error",
  "message": "Invalid ref: ..."
}

Handling a 404 on ref

A 404 on ref means the identifier didn't match a click record. This can happen if the visitor's cookie was too old, was cleared, or was tampered with. Log it quietly and continue — don't block the user's purchase.

Handling a 401 "Request expired"

Saviliate rejects requests where the X-TIMESTAMP is more than 5 minutes old. Ensure your server clock is synced (NTP) and that you generate the timestamp immediately before making the request, not at application boot time.

Handling a 403 "Invalid request origin"

Saviliate extracts your domain from the Origin or Referer header and compares it to the domain registered on your brand. Ensure your server sends one of these headers and that your registered domain matches exactly (no trailing slashes, correct subdomain).

Full Reporting Examples #

Server-side examples that build the HMAC signature and post a commission report after a successful payment. Each example uses the helper function from Generating a Signature above.

use Illuminate\Support\Facades\Http;

function reportCommission(
    string  $ref,
    string  $externalUserId,
    string  $orderId,
    float   $orderAmount,
): void {
    $secret    = env('SAVILIATE_SECRET_KEY');
    $timestamp = (string) time();
    $body      = json_encode([
        'ref'              => $ref,
        'external_user_id' => $externalUserId,
        'order_id'         => $orderId,
        'order_amount'     => $orderAmount,
    ]);

    $signature = hash_hmac('sha256', $timestamp . $body, $secret);

    Http::withHeaders([
        'Authorization' => 'Bearer ' . $secret,
        'X-TIMESTAMP'   => $timestamp,
        'X-SIGNATURE'   => $signature,
        'Content-Type'  => 'application/json',
    ])->withBody($body, 'application/json')
       ->post('https://saviliate.com/api/v1/track/commission');
}

// In your payment confirmed handler:
$ref = $request->cookie('sv_ref');   // or $user->saviliate_ref from DB

if ($ref) {
    reportCommission(
        $ref,
        (string) $order->user_id,
        (string) $order->id,
        $order->subtotal,
    );
}
const crypto = require('crypto');

async function reportCommission(ref, externalUserId, orderId, orderAmount) {
    const secret    = process.env.SAVILIATE_SECRET_KEY;
    const timestamp = String(Math.floor(Date.now() / 1000));
    const body      = JSON.stringify({ ref, external_user_id: externalUserId, order_id: orderId, order_amount: orderAmount });
    const sig       = crypto.createHmac('sha256', secret).update(timestamp + body).digest('hex');

    try {
        await fetch('https://saviliate.com/api/v1/track/commission', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${secret}`,
                'X-TIMESTAMP':   timestamp,
                'X-SIGNATURE':   sig,
                'Content-Type':  'application/json',
            },
            body,
        });
    } catch (err) {
        // Log but never throw — don't block the user's purchase
        console.error('Saviliate report failed:', err.message);
    }
}

// In your order confirmation handler:
const ref = req.cookies['sv_ref'];  // or from user record in DB
if (ref) await reportCommission(ref, order.userId, order.id, order.subtotal);
import hashlib, hmac, json, time, requests, os

def report_commission(ref: str, external_user_id: str, order_id: str, order_amount: float):
    secret    = os.environ['SAVILIATE_SECRET_KEY']
    timestamp = str(int(time.time()))
    body      = json.dumps({
        'ref': ref, 'external_user_id': external_user_id,
        'order_id': order_id, 'order_amount': order_amount,
    }, separators=(',', ':'))

    sig = hmac.new(
        secret.encode(), (timestamp + body).encode(), hashlib.sha256
    ).hexdigest()

    try:
        requests.post(
            'https://saviliate.com/api/v1/track/commission',
            data=body,
            headers={
                'Authorization': f'Bearer {secret}',
                'X-TIMESTAMP':   timestamp,
                'X-SIGNATURE':   sig,
                'Content-Type':  'application/json',
            },
            timeout=10,
        )
    except Exception as e:
        print(f'Saviliate report failed: {e}')  # log and continue

# In your payment confirmed view:
ref = request.COOKIES.get('sv_ref')  # or from user record in DB
if ref:
    report_commission(ref, str(order.user_id), str(order.id), order.subtotal)
require 'openssl', 'json', 'net/http'

def report_commission(ref:, external_user_id:, order_id:, order_amount:)
    secret    = ENV['SAVILIATE_SECRET_KEY']
    timestamp = Time.now.to_i.to_s
    body      = { ref: ref, external_user_id: external_user_id,
                  order_id: order_id, order_amount: order_amount }.to_json

    sig = OpenSSL::HMAC.hexdigest('SHA256', secret, timestamp + body)

    uri  = URI('https://saviliate.com/api/v1/track/commission')
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    req                  = Net::HTTP::Post.new(uri)
    req['Authorization'] = "Bearer #{secret}"
    req['X-TIMESTAMP']   = timestamp
    req['X-SIGNATURE']   = sig
    req['Content-Type']  = 'application/json'
    req.body             = body

    http.request(req)
rescue => e
    Rails.logger.error("Saviliate report failed: #{e.message}")
end

# In your orders controller:
ref = cookies[:sv_ref]  # or @user.saviliate_ref from DB
report_commission(ref: ref, external_user_id: @order.user_id.to_s,
                  order_id: @order.id.to_s, order_amount: @order.subtotal) if ref
# Full working example — generates the HMAC signature inline

SECRET="your_secret_key_here"
BODY='{"ref":"9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d","external_user_id":"user_1042","order_id":"ord_8801","order_amount":49.00}'
TS=$(date +%s)
SIG=$(echo -n "${TS}${BODY}" | openssl dgst -sha256 -hmac "${SECRET}" | awk '{print $2}')

curl -X POST https://saviliate.com/api/v1/track/commission \
  -H "Authorization: Bearer ${SECRET}" \
  -H "X-TIMESTAMP: ${TS}" \
  -H "X-SIGNATURE: ${SIG}" \
  -H "Content-Type: application/json" \
  -d "${BODY}"

Conversion Levels #

You decide at which point in your funnel to pay affiliates. Pick the level that matches your product's goals. Your campaign's configured level is set in the Saviliate dashboard.

Level 1
Website Visit
Pay per visitor who lands on your site via an affiliate link.
Level 2
Registration
Pay when the visitor creates an account — common for lead-gen and SaaS trials.
Level 3
Purchase
Pay only when the customer completes a payment — the most common model.
Call /track/commission when the landing page loads and the sv_ref cookie is present. Since the user may not be logged in, pass the session ID as external_user_id. Pass order_amount: 0 and a synthetic order_id such as the session ID.
Level 1 — Visit
// In your landing page / home controller

$ref = $request->cookie('sv_ref');

if ($ref) {
    reportCommission(
        ref:            $ref,
        externalUserId: session()->getId(),
        orderId:        'visit_' . session()->getId(),
        orderAmount:    0,
    );
}
Call after the user record is saved. Optionally also call /track/customer to send registration details to Saviliate. Both calls can be fired together. Pass order_amount: 0 if registration itself carries no monetary value.
Level 2 — Registration
// After $user = User::create([...])

$ref = $request->cookie('sv_ref');

if ($ref) {
    // Commission: registration is the conversion
    reportCommission(
        ref:            $ref,
        externalUserId: (string) $user->id,
        orderId:        'reg_' . $user->id,
        orderAmount:    0,
    );

    // Customer record — sends name + email to affiliate dashboard
    reportCustomer(
        ref:            $ref,
        externalUserId: (string) $user->id,
        email:          $user->email,
        name:           $user->name,
    );
}
Call after payment is confirmed — ideally from a payment webhook (e.g. Stripe payment_intent.succeeded), not from the frontend. Webhooks don't carry browser cookies, so save the ref on the user record at registration time and read it from there when the webhook fires.
Level 3 — Purchase
// In your Stripe webhook handler (or equivalent)
// The cookie is not available here — use the ref stored on the user record

$ref = $user->saviliate_ref; // saved at registration time

if ($ref) {
    reportCommission(
        ref:            $ref,
        externalUserId: (string) $order->user_id,
        orderId:        (string) $order->id,
        orderAmount:    $order->subtotal,
    );
}

Test Credentials #

Use these credentials to verify your integration end-to-end before you have a Saviliate account, or before your brand is configured. They work against the live API — authentication, signature verification, and domain checks all run normally — but any request carrying the test ref below is intercepted and returns a canned response without writing any records.

Different from your brand's Test mode. These are shared, read-only credentials for first-time integration testing. They are not a replacement for the Integration Mode toggle on your own brand — once you have a Saviliate account and brand, use that instead. Your brand's credentials and Test mode are entirely separate from these.
Secret Key
test_sk_sav_4KqzOAXfvcno0VGmRzKBXwqc
Used in Authorization: Bearer header and as the HMAC signing key for X-SIGNATURE.
Public Key
test_pk_sav_9mRzKBXwqcpl3FHvTyNsDxJe
Used for client-side brand identification e.g. the storefront script. Safe to expose in frontend code.
Webhook Secret
test_whsec_sav_7NpXaYQvrd2JmLc8sFbKtWHz
Used to verify HMAC signatures on inbound Saviliate webhook payloads to your server.
Test Ref (affiliate click identifier )
a1b2c3d4-e5f6-7890-abcd-ef1234567890
Send this as ref in any payload. Saviliate recognises it as a pre-seeded test click and returns a canned response — no records created.

What the test ref does

Every tracking endpoint requires a ref that resolves to a real affiliate click record. In normal operation that ref comes from a real affiliate clicking a real link. The test ref above is a permanently pre-seeded click record on Saviliate's side — send it as ref and the API resolves it successfully, validates the rest of your payload shape and data types, then returns the same canned response you'd see in brand Test mode.

You can send any valid values in the other payload fields — external_user_id, order_id, order_amount, email, name — as long as they match the required types. Only the payload shape and data types are validated; the actual values are ignored.

Quick start example

cURL — first test call
SECRET="test_sk_sav_4KqzOAXfvcno0VGmRzKBXwqc"
BODY='{"ref":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","external_user_id":"user_001","order_id":"ord_001","order_amount":49.00}'
TS=$(date +%s)
SIG=$(echo -n "${TS}${BODY}" | openssl dgst -sha256 -hmac "${SECRET}" | awk '{print $2}')

curl -X POST https://saviliate.com/api/v1/track/commission \
  -H "Authorization: Bearer ${SECRET}" \
  -H "X-TIMESTAMP: ${TS}" \
  -H "X-SIGNATURE: ${SIG}" \
  -H "Content-Type: application/json" \
  -d "${BODY}"

Expected response

201 — test credentials detected
{
  "status":        "success",
  "message":       "Commission recorded successfully",
  "commission_id": "test_commission_id",
  "mode":          "test",
  "note":          "Test credentials detected. No records were created."
}
Never use test credentials in production. These keys are shared across all developers testing against Saviliate. They are rate-limited and domain-unrestricted for convenience, and will never generate real commissions or records. Swap them out for your brand's own credentials before going live.

Security Notes #

What you must do

Never send API calls from the browser. Your secret_key must only appear in server-side code. If it leaks to the browser, anyone can inspect network requests, extract the key, compute valid HMAC signatures, and fabricate commission reports against your account.

Generate the timestamp immediately before signing. Saviliate rejects requests with a timestamp older than 5 minutes. Don't cache the timestamp between requests — generate it fresh for each API call.

Sign the exact bytes you send. Serialize your JSON body to a string, sign it, then send that exact string as the body. Any reformatting after signing will break the signature.

Store the ref on the user record for SaaS and subscription products. Payment webhooks (Stripe, Paddle, etc.) do not carry browser cookies. Save the ref at registration time so it's available when a webhook fires later.

Set sv_ref as HttpOnly and Secure. This prevents JavaScript from reading the cookie, protecting it from XSS attacks.

What Saviliate does

Saviliate validates every request through three independent checks: the Authorization header resolves a brand record, the HMAC signature confirms the payload hasn't been tampered with, and the Origin or Referer header is verified against your registered domain. All three must pass. Replay attacks are mitigated by the 5-minute timestamp window and timing-safe signature comparison via some custom method.