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 click record with an identifier, then sends the visitor to your
site with
that identifier appended as ?saviliate=.
?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.
ref value. Saviliate ties it back to the original affiliate and calculates
commission.
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. |
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-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 ...)
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}"
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.
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.
{
"status": "success",
"message": "Commission recorded successfully",
"commission_id": "test_commission_id",
"mode": "test",
"note": "Brand is in test mode. No records were created."
}
{
"status": "success",
"message": "Customer recorded successfully",
"mode": "test",
"note": "Brand is in test mode. No records were created."
}
{
"status": "success",
"message": "Click recorded",
"mode": "test",
"note": "Brand is in test mode. No records were created."
}
Recommended integration workflow
401 and 400 correctly
— without blocking real users.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();
/^[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;
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.
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.
|
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
{
"status": "success",
"message": "Commission recorded successfully",
"commission_id": 4821
}
{
"status": "error",
"message": "Duplicate commission
ignored (idempotent)",
"commission_id": 4821
}
(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.
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. |
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
{
"status": "success",
"message": "Customer recorded successfully"
}
{
"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.
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. |
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...)"
}
Response
{
"status": "success",
"message": "Click recorded"
}
{
"status": "error",
"message": "Invalid ref: ..."
}
Responses & Errors #
{
"status": "error",
"message": "Bad Request - Missing
required headers"
}
{
"status": "error",
"message": "Unauthorized - Invalid
credentials"
}
{
"status": "error",
"message": "Request expired"
}
{
"status": "error",
"message": "Invalid signature"
}
{
"status": "error",
"message": "Invalid request origin"
}
{
"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.
/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.
// 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,
);
}
/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.
// 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,
);
}
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.
// 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.
Authorization: Bearer header and as the HMAC
signing key for X-SIGNATURE.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
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
{
"status": "success",
"message": "Commission recorded successfully",
"commission_id": "test_commission_id",
"mode": "test",
"note": "Test credentials detected. No records were created."
}
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.