API reference

The RevRake API.

Everything an operator's platform sends to RevRake, what comes back, and what RevRake sends to affiliate trackers and to the operator's own systems. For the order to do things in, read the integration guide.

Base URLs

The API is four signed endpoints for the operator's platform, two to write and two to read, and the tracking link that visitors open. The signed endpoints are also published as an OpenAPI 3.1 file.

EndpointMeaning
POST https://app.revrake.com/v1/postbackReport one event, or a batch of events. Signed.
POST https://app.revrake.com/v1/filesPush a daily REG or SALES file. Signed.
GET https://app.revrake.com/v1/reportsRead a report, as JSON or CSV. Signed, with an operator or an affiliate key.
GET https://app.revrake.com/v1/commissionsRead the commission ledger, page by page. Signed, with an operator or an affiliate key.
GET https://app.revrake.com/v1/affiliatesList the brand's affiliates. Signed, with the operator key.
GET https://app.revrake.com/v1/linksList tracking links. Signed, with an operator or an affiliate key.
GET https://app.revrake.com/v1/dealsList the deals with the terms in effect now. Signed, with the operator key.
GET https://track.revrake.com/c/{code}The tracking link: stores the click and redirects. Not signed.

The portals at app.revrake.com use their own endpoints behind a signed-in session. They are not a public API and may change without notice. Reports, the ledger and payout rows can also be downloaded there as CSV.

Keys

There are two kinds of key. Each has a key ID and a secret, and each signs requests the same way. Keys are never shared: every party has its own.

Key ID starts withWhose it isWhat it can do
pb_The operator. One active key per brand, on the Integration page of the operator portal. A workspace admin can see the secret and rotate the key.Sends events and files. Reads the reports and the ledger of the whole program, or of one affiliate with affiliate=<number>.
ak_One affiliate. Each affiliate creates their own on the API tab of their affiliate portal. The secret is shown once, when the key is created.Reads that affiliate's own reports and commissions, and nothing else. It cannot send events: /v1/postback and /v1/files answer 401 unknown_key.

Never give the operator key to an affiliate. It reads every affiliate's numbers and can write events. An affiliate who wants API access issues their own key.

  • A new key revokes the old one at once, for both kinds. An affiliate can also revoke their key and have none.
  • An affiliate key works only while the affiliate is approved in the program. Otherwise a read answers 403 affiliate_inactive.
  • With an affiliate key, the affiliate parameter is ignored and by=affiliate is answered by day.
  • The rate limit of reads is counted per key.

Authentication

Every /v1 request is signed with a key. Every request carries three headers:

HeaderValue
X-RevRake-KeyYour key ID. It starts with pb_ (operator) or ak_ (affiliate).
X-RevRake-TimestampThe current Unix time in seconds. A request is accepted within five minutes before or after it.
X-RevRake-SignatureThe lower-case hex HMAC-SHA256, under your secret, of <timestamp>.<body> for a POST and of <timestamp>.<path>?<query> for a GET.

To sign a request:

  1. Build the body and keep the exact bytes you will send.
  2. Take the current Unix time in seconds, as decimal text.
  3. Concatenate the timestamp, a dot (.) and the body.
  4. Compute HMAC-SHA256 of that under your secret and write it as lower-case hex.
  5. Send the body unchanged, with the three headers.

Signing a read

A GET has no body, so the signature covers the request target instead: the path and the query exactly as sent, for example 1790000000./v1/reports?by=link&days=7. With no query, it is the timestamp, a dot and the path. Build the address once, sign that text and send that text: a client that re-orders or re-encodes the query after signing breaks the signature.

  • The key is checked first, then the timestamp, then the signature.
  • A Content-Type header is not required. Send application/json for events and text/csv for files.
  • A rotated key stops working at once.

Signing samples

Each sample has one signing function, sends a registration and then reads a report. Replace the key ID and the secret with yours.

curl
KEY="pb_your_key_id"
SECRET="your_secret"
BASE="https://app.revrake.com"

# sign <payload>: hex HMAC-SHA256 of "<timestamp>.<payload>" under the secret
sign() { printf '%s.%s' "$TS" "$1" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //'; }

# Write: the payload is the body.
TS=$(date +%s)
BODY='{"type":"registration","player_id":"PLR-1001","click_id":"CID_FROM_LANDING_URL"}'
curl -X POST "$BASE/v1/postback" \
  -H "Content-Type: application/json" \
  -H "X-RevRake-Key: $KEY" \
  -H "X-RevRake-Timestamp: $TS" \
  -H "X-RevRake-Signature: $(sign "$BODY")" \
  -d "$BODY"

# Read: the payload is the path and the query, exactly as sent.
TS=$(date +%s)
TARGET='/v1/reports?by=link&days=7'
curl "$BASE$TARGET" \
  -H "X-RevRake-Key: $KEY" \
  -H "X-RevRake-Timestamp: $TS" \
  -H "X-RevRake-Signature: $(sign "$TARGET")"
Node.js
import { createHmac } from 'node:crypto';

const KEY = 'pb_your_key_id';
const SECRET = 'your_secret';
const BASE = 'https://app.revrake.com';

// hex HMAC-SHA256 of "<timestamp>.<payload>" under the secret
function sign(secret, timestamp, payload) {
  return createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest('hex');
}

function headers(payload) {
  const ts = Math.floor(Date.now() / 1000).toString();
  return { 'X-RevRake-Key': KEY, 'X-RevRake-Timestamp': ts, 'X-RevRake-Signature': sign(SECRET, ts, payload) };
}

// Write: the payload is the body. Send the same string you signed.
const body = JSON.stringify({ type: 'registration', player_id: 'PLR-1001', click_id: 'CID_FROM_LANDING_URL' });
const write = await fetch(`${BASE}/v1/postback`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', ...headers(body) },
  body,
});
console.log(write.status, await write.json());

// Read: the payload is the path and the query, exactly as sent.
const target = '/v1/reports?by=link&days=7';
const read = await fetch(BASE + target, { headers: headers(target) });
console.log(read.status, await read.json());
Python
import hashlib
import hmac
import json
import time
import urllib.error
import urllib.request

KEY = "pb_your_key_id"
SECRET = "your_secret"
BASE = "https://app.revrake.com"


def sign(secret: str, timestamp: str, payload: bytes) -> str:
    """hex HMAC-SHA256 of "<timestamp>.<payload>" under the secret"""
    return hmac.new(secret.encode(), timestamp.encode() + b"." + payload, hashlib.sha256).hexdigest()


def call(method: str, target: str, body: bytes | None = None):
    ts = str(int(time.time()))
    # Write: the payload is the body. Read: the path and the query, as sent.
    payload = body if body is not None else target.encode()
    headers = {"X-RevRake-Key": KEY, "X-RevRake-Timestamp": ts, "X-RevRake-Signature": sign(SECRET, ts, payload)}
    if body is not None:
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(BASE + target, data=body, method=method, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=10) as res:
            return res.status, json.load(res)
    except urllib.error.HTTPError as e:
        return e.code, json.load(e)


event = {"type": "registration", "player_id": "PLR-1001", "click_id": "CID_FROM_LANDING_URL"}
print(*call("POST", "/v1/postback", json.dumps(event).encode()))
print(*call("GET", "/v1/reports?by=link&days=7"))
PHP
<?php
const KEY = 'pb_your_key_id';
const SECRET = 'your_secret';
const BASE = 'https://app.revrake.com';

// hex HMAC-SHA256 of "<timestamp>.<payload>" under the secret
function sign(string $secret, string $timestamp, string $payload): string
{
    return hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
}

function call(string $method, string $target, ?string $body = null): array
{
    $ts = (string) time();
    // Write: the payload is the body. Read: the path and the query, as sent.
    $payload = $body ?? $target;
    $headers = [
        'X-RevRake-Key: ' . KEY,
        'X-RevRake-Timestamp: ' . $ts,
        'X-RevRake-Signature: ' . sign(SECRET, $ts, $payload),
    ];
    $ch = curl_init(BASE . $target);
    curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
    if ($body !== null) {
        $headers[] = 'Content-Type: application/json';
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    }
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $answer = curl_exec($ch);
    return [curl_getinfo($ch, CURLINFO_RESPONSE_CODE), json_decode($answer, true)];
}

$event = ['type' => 'registration', 'player_id' => 'PLR-1001', 'click_id' => 'CID_FROM_LANDING_URL'];
print_r(call('POST', '/v1/postback', json_encode($event)));
print_r(call('GET', '/v1/reports?by=link&days=7'));
Go
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"time"
)

const (
	key    = "pb_your_key_id"
	secret = "your_secret"
	base   = "https://app.revrake.com"
)

// sign is the hex HMAC-SHA256 of "<timestamp>.<payload>" under the secret.
func sign(secret, timestamp string, payload []byte) string {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + "."))
	mac.Write(payload)
	return hex.EncodeToString(mac.Sum(nil))
}

func call(method, target string, body []byte) (int, string, error) {
	ts := strconv.FormatInt(time.Now().Unix(), 10)
	// Write: the payload is the body. Read: the path and the query, as sent.
	payload := body
	if body == nil {
		payload = []byte(target)
	}
	req, err := http.NewRequest(method, base+target, bytes.NewReader(body))
	if err != nil {
		return 0, "", err
	}
	req.Header.Set("X-RevRake-Key", key)
	req.Header.Set("X-RevRake-Timestamp", ts)
	req.Header.Set("X-RevRake-Signature", sign(secret, ts, payload))
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	res, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
	if err != nil {
		return 0, "", err
	}
	defer res.Body.Close()
	answer, err := io.ReadAll(res.Body)
	return res.StatusCode, string(answer), err
}

func main() {
	event := []byte(`{"type":"registration","player_id":"PLR-1001","click_id":"CID_FROM_LANDING_URL"}`)
	fmt.Println(call("POST", "/v1/postback", event))
	fmt.Println(call("GET", "/v1/reports?by=link&days=7", nil))
}

Test vector

With these inputs your code must produce exactly these signatures:

Secret
test_secret
Timestamp
1791072000
Body
{"type":"registration","player_id":"PLR-1001","btag":"1001_1"}
Signature
115d8c73c6069d8953a36ca3a68c114da6232e4ba31889fd519d41ab1350a603
Request target of a read
/v1/reports?by=link&days=7
Signature of the read
ed1ed72dd7ddef22c762f63b8919011e7c36c7214b55b72693c5927fb4b911d0

Limits

These are all the limits the product applies to integrations, tracking and affiliates today, with their numbers. Sign-in, password reset and applications also have limits against abuse.

LimitMeaning
120Reads per key, per minute: GET /v1/reports and GET /v1/commissions together
30 000Event requests per key, per minute: POST /v1/postback, up to 1,000 at once
60Files per key, per minute: POST /v1/files, up to 10 at once
4 MBBody of POST /v1/postback, one event or a batch
32 MBBody of POST /v1/files
1 – 1000Events in one batch
200 000Data rows in one file
300 sDifference between X-RevRake-Timestamp and the real time, in either direction
24 hHow far in the future ts and period may be
100Length of player_id, in characters
200Length of txn_id, in characters
0 / 2Decimals in an amount: those of the currency (two for most, none for currencies such as XOF, UGX or JPY)
1 – 365days of a report
10, 25, 50, 100, 500, 1000per_page of the ledger: one of these values
1 – 100 000page of the ledger
100Length of q, in characters; longer text is cut
200Length of sub1 to sub5, each UTM tag and each ad click ID on a tracking link, in characters; longer values are cut
10Postback destinations per affiliate
2000Length of a postback address template, in characters
5 sTime a tracker has to answer a postback
50Inbound log shown in the portal: latest entries of the last 7 days
2000Length of a webhook endpoint address, in characters
5 sTime a webhook endpoint has to answer
20Failed webhook attempts in a row before the endpoint is switched off
100Deliveries per webhook endpoint shown in the delivery log
2 – 40Length of a promo code: letters, digits, hyphens and underscores
32 MBSize of a file RevRake fetches from your address
60 sTime your address has to deliver a fetched file
0 – 3600 sSeconds after the click for the registration-time rule (0 turns it off)
0 – 365Days before an open fraud flag decides itself (0 means never)
1 – 200Flags released or confirmed in one bulk decision
0 – 10 000 (20)Clicks a minute on one link from one address before the click-burst flag is raised (0 turns it off; 20 unless you change it)
60 + 1 / minTracking links an affiliate can create: 60 at once, then one a minute. Past that the answer is 429 with Retry-After
2 MBSize of a banner image uploaded in the portal: PNG, JPEG, GIF or WebP

The two write endpoints, POST /v1/postback and POST /v1/files, are limited per key (see the table). Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining; a refusal is 429 with Retry-After. Send at the pace of your events, and use batches for a backfill. The tracking link has no rate limit; an unusual burst of clicks on one link from one address raises a fraud flag and is still redirected.

The rate limit of reads

  • Each key may make 120 reads a minute. The allowance is a bucket of 120: a read takes one, and one comes back every half second. A key that has been idle can make 120 reads at once; after that, two a second.
  • Every read answer carries X-RateLimit-Limit (always 120) and X-RateLimit-Remaining (how many reads are left right now).
  • Past the limit the answer is 429 rate_limited with a Retry-After header: the seconds to wait. Wait, then send the same request again, signed with a fresh timestamp.
  • The limit is counted per key: the operator's key and each affiliate's key have their own allowance.
HTTP/1.1 200 OK
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
Retry-After: 1

{"error":"rate_limited","message":"Too many requests: at most 120 reads a minute per key. One more is allowed every half second; wait the seconds in Retry-After and try again."}

Handling a 429

// read(target) signs one GET with a fresh timestamp and sends it,
// as in the signing samples.
async function readWithRetry(target, tries = 5) {
  for (let i = 0; i < tries; i++) {
    const res = await read(target);
    if (res.status !== 429) return res;
    const seconds = Number(res.headers.get('Retry-After')) || 1;
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
  throw new Error('still rate limited after ' + tries + ' tries');
}

POST /v1/postback

Reports one event from the operator's platform. The body is a JSON object with a type and the fields of that type, or a JSON array of such objects (a batch).

POST https://app.revrake.com/v1/postback
  • type is one of registration, deposit, chargeback, ngr.
  • A field that is not listed for the type is refused with 422, so a typo is never silently ignored.
  • ts is the time of the event in RFC 3339 form (2026-10-04T08:15:00Z). When it is left out, the time of the request is used. It must carry a UTC offset (Z or +03:00); a time without one is refused. It may not be more than a day in the future.
  • An amount is a decimal, as JSON text or a JSON number: "25.00" or 25.5. No thousands separator, no exponent, and no more decimals than the currency has.
  • currency is a three-letter ISO 4217 code. Lower case is accepted. When it is left out, the workspace's reporting currency is used.
  • Text values are trimmed of spaces at both ends, except click_id and btag: send them exactly as they arrived on the landing page.

registration

A new player. Send click_id, btag, promo_code, or several of them; with none, the player is recorded as not attributed. The player is attributed once, here. Answer: attribution.

{"type":"registration","player_id":"PLR-1001","click_id":"01K6QF3M8Z5T7W9XB2C4D6E8GH","btag":"1001_1"}
FieldTypeRequiredExampleMeaning
player_idstringyes"PLR-1001"Your ID for the player. Up to 100 characters. The key of the event.
click_idstringno"01K6QF3M8Z5T7W9XB2C4D6E8GH"The cid the player arrived with: 26 characters, letters and digits; upper or lower case.
btagstringno"1001_1"The btag your platform stored on the player: <affiliate number>_<link number>, digits only.
promo_codestringno"GARAGE50"The promo code the player typed at sign-up. Not case-sensitive. It attributes the player only when click_id and btag do not.
email_hashstringno"9f86d081…0f00a08"SHA-256 of the lower-cased email, in hex. Used only by the self-referral check.
email_domainstringno"example.com"The domain of the player's email, the part after the @ (example.com). Used only by the throwaway-email rule. Send the domain only: if a whole address arrives, only the domain is kept, but the inbound log still shows the request as it arrived.
ipstringno"203.0.113.7"The IP address at registration. It is hashed, and used only by the self-referral check and to find the matching click for a btag.
countrystringno"KE"The player's country: two letters, ISO 3166-1 alpha-2 (GB, KE). Lower case is accepted. A value that is not two letters leaves the player without a country. Without the field, the country of the click that brought the player is used. A deal can list countries it does not pay for.
tsstringno"2026-10-04T08:15:00Z"When the player registered. The attribution window is measured from it.
{
  "status": "accepted",
  "event_type": "registration",
  "event_id": "0199b2c4-6f1e-7a53-9d0c-2b8f4e1a7c33",
  "attribution": {
    "attributed": true,
    "reason": "click",
    "affiliate_number": 1001,
    "link_number": 1,
    "click_id": "01K6QF3M8Z5T7W9XB2C4D6E8GH",
    "policy_version": 1,
    "window_days": 30,
    "model": "last_click"
  }
}

deposit

Send a deposit event for each deposit, with your transaction ID in txn_id. The deal chooses whether CPA requires the first deposit or cumulative deposits, net of chargebacks, to reach its minimum. Qualification creates one CPA commission per player, on hold.

{"type":"deposit","player_id":"PLR-1001","txn_id":"TX-88213","amount":"25.00","currency":"KES","first":true}
FieldTypeRequiredExampleMeaning
player_idstringyes"PLR-1001"Your ID for the player. Up to 100 characters.
txn_idstringyes"TX-88213"Your transaction ID. Up to 200 characters. The key of the event.
amountstring | numberyes"25.00"The deposit, above zero.
currencystringno"KES"The currency of the deposit. Any currency is accepted.
tsstringno"2026-10-04T08:20:00Z"When the deposit was made. It decides which deal version and which day's exchange rate apply.
firstbooleannotruetrue if this is the player's first deposit. When left out, the deposit counts as first if RevRake knows no earlier one for the player.
{
  "status": "accepted",
  "event_type": "deposit",
  "event_id": "0199b2c9-03aa-7e10-b6d2-91c07f5e2a18",
  "commission": {
    "effect": "created",
    "type": "cpa",
    "amount_minor": 4000,
    "status": "hold"
  }
}

chargeback

Send a chargeback event with its own txn_id and the original deposit’s txn_id in ref_txn_id. It reverses that whole deposit. For cumulative CPA, the commission is taken back only if the remaining qualifying total falls below the minimum: a held commission is cancelled; an approved or paid one gets a reversal row.

{"type":"chargeback","txn_id":"CB-5001","ref_txn_id":"TX-88213"}
FieldTypeRequiredExampleMeaning
txn_idstringyes"CB-5001"Your ID for the chargeback itself. Up to 200 characters. The key of the event.
ref_txn_idstringyes"TX-88213"The txn_id of the deposit that was charged back.
player_idstringno"PLR-1001"Optional. If supplied, must match the player of the referenced deposit.
tsstringno"2026-10-09T11:00:00Z"When the chargeback happened.
{
  "status": "accepted",
  "event_type": "chargeback",
  "event_id": "0199cc10-7b2d-7c41-a3f0-5d2e8b9c1f47",
  "commission": {
    "effect": "cancelled",
    "type": "cpa",
    "amount_minor": 4000,
    "status": "rejected"
  }
}

ngr

The net gaming revenue of a player for a day or a month. It earns the revenue share of the deal version in effect at the end of the period, on hold. The player must be registered. Answer: commission.

{"type":"ngr","player_id":"PLR-1001","period":"2026-10-03","amount":"118.40","currency":"KES"}
FieldTypeRequiredExampleMeaning
player_idstringyes"PLR-1001"Your ID for the player. Up to 100 characters.
periodstringyes"2026-10-03"The day (YYYY-MM-DD) or the month (YYYY-MM) the amount covers. A month and its first day are different periods. Not more than a day in the future.
amountstring | numberyes"118.40"The net gaming revenue of the period. Negative when the player won.
currencystringno"KES"The currency of the amount.
ggrstring | numberno"240.00"Optional. The gross gaming revenue of the period, in the same currency. Shown to the affiliate; it does not change the commission.
bonusesstring | numberno"80.00"Optional. Bonuses taken off the gross revenue. Shown only.
feesstring | numberno"21.60"Optional. Fees taken off the gross revenue, such as payment or platform fees. Shown only.
taxesstring | numberno"20.00"Optional. Taxes taken off the gross revenue. Shown only.
tsstringno"2026-10-04T02:00:00Z"When the figure was reported.
{
  "status": "accepted",
  "event_type": "ngr",
  "event_id": "0199b2d1-5c0e-7b42-8a17-3e9d6f0c4b21",
  "commission": {
    "effect": "created",
    "type": "revshare",
    "amount_minor": 2960,
    "status": "hold"
  }
}

A click_id, btag, promo_code or player_id that contains { or } is refused: it means your platform sent a placeholder without filling it in.

The answer

An event that was stored, or was already stored, is answered with 200 and a JSON object. Fields that do not apply are left out.

FieldTypeMeaning
statusstringaccepted, or duplicate when the event was already stored.
event_typestringThe type you sent.
event_idstringRevRake's ID of the stored event, a UUID. The same on every replay.
attributionobjectFor a registration: the decision.
attribution.attributedbooleanWhether the player belongs to an affiliate.
attribution.reasonstringWhy. See the table below.
attribution.affiliate_numberintegerThe affiliate. Only when attributed.
attribution.link_numberintegerThe affiliate's link. Only when attributed.
attribution.click_idstringThe click that was matched, when there is one.
attribution.policy_versionintegerThe version of the brand's attribution policy that decided.
attribution.window_daysintegerThe attribution window of that version, in days.
attribution.modelstringlast_click or first_click: the model of that version.
commissionobjectFor a deposit, a chargeback and ngr: what the event did to the ledger.
commission.effectstringcreated, cancelled (a commission on hold was rejected), reversed (a reversal row was added) or none.
commission.typestringcpa, revshare, adjustment or reversal.
commission.amount_minorintegerThe amount in minor units of the deal's currency (4000 is 40.00). Negative for a reversal and for a downward adjustment.
commission.statusstringhold, approved or rejected.
commission.reasonstringWhy nothing was created, when effect is none. See the table below.
waiting_for_playerbooleantrue for a deposit whose player is not registered yet. It is kept and priced when the registration arrives.
duplicate_ofstringFor a legacy first-deposit match across live events and older daily files: the earlier event_id. Current SALES summaries use daily coverage and retain each actual transaction ID.
detailstringWith duplicate_of: a sentence saying which earlier deposit this one repeats.
{
  "status": "accepted",
  "event_type": "deposit",
  "event_id": "0199b2c9-03aa-7e10-b6d2-91c07f5e2a18",
  "waiting_for_player": true
}
{
  "status": "duplicate",
  "event_type": "deposit",
  "event_id": "0199b31f-8c02-7a66-b4e1-2d7f9a3c6e15",
  "duplicate_of": "0199b2c9-03aa-7e10-b6d2-91c07f5e2a18",
  "detail": "The same deposit (TX-88213) was already reported through the API; this one adds nothing."
}
{
  "status": "duplicate",
  "event_type": "deposit",
  "event_id": "0199b2ca-11d4-7f09-8c55-0a6e2d4b7f90",
  "commission": { "effect": "none", "reason": "not_first" }
}

A commission is created with status hold. It is approved automatically when the hold period of the deal ends, unless something still holds it: an open fraud flag, an affiliate who is not approved, or a missing exchange rate.

Attribution reasons

The click ID is tried first. If it is not one of yours, the btag is tried. When the policy says the btag wins and the btag matches a link, the btag is tried first instead. A btag that matches a link attributes the player even when no click is found. A promo code is tried last, only when neither the click ID nor the btag attributed the player. The policy, its window and which of the two wins are set on the Integration page; each change is a new version. When the policy says the promo code wins, a code that matches a link is tried before both, and one that does not falls back to them. A deal may set its own window, which replaces the policy's for clicks on its affiliates' links.

reasonMeaning
clickAttributedThe click ID matched a click inside the window.
first_clickAttributedThe policy is first click: the click ID matched a click inside the window, and an earlier counted click of the same visitor (same address and device) inside the window was credited instead.
btag_clickAttributedThe btag matched a link, and a click on that link was found inside the window.
btag_onlyAttributedThe btag matched a link; no click was found inside the window.
promo_codeAttributedNeither the click ID nor the btag attributed the player, and the promo code matched a code of an approved affiliate.
no_tagNot attributedThe registration carried neither a click ID nor a btag.
unknown_clickNot attributedThe click ID is not one of yours, and no btag was sent.
unknown_btagNot attributedThe btag is malformed or matches no link.
outside_windowNot attributedThe click is older than the attribution window, or later than the registration.
affiliate_inactiveNot attributedThe affiliate is not approved, or was not when the click was made.
unknown_promo_codeNot attributedOnly a promo code was sent, and it matches none of your codes.
promo_code_retiredNot attributedOnly a promo code was sent, and it was retired.

Why no commission was created

commission.reasonMeaning
unattributedThe player belongs to no affiliate.
not_firstNot the player's first deposit.
already_earnedThe player's CPA was already earned. A second "first" deposit pays nothing and raises a flag.
no_deal_versionThe affiliate has no deal, or the deal had no version at the time of the event.
country_blockedThe player is from a country the deal does not pay for. The player stays recorded and attributed.
no_cpa_in_dealThe deal pays no CPA.
below_thresholdThe first-deposit or cumulative net-deposit amount is below the deal’s minimum, according to its CPA baseline.
no_commissionFor a chargeback: the deposit had earned nothing.
no_revshare_in_dealFor ngr: the deal pays no revenue share.
fx_rate_missingFor ngr: there is no exchange rate for the period yet. The report is stored and priced when a rate exists.
unchangedFor ngr: a corrected figure that changes nothing in the commission.

Batches

The body may be a JSON array of 1 to 1,000 events, sent to the same endpoint with the same headers. The signature covers the whole array. Events are handled in order, each on its own, so one refused event never stops the rest.

A batch that could be read is answered with 200, also when every event in it was refused. Look at the counts.

[
  {"type":"deposit","player_id":"PLR-1001","txn_id":"TX-88213","amount":"25.00","first":true},
  {"type":"ngr","player_id":"PLR-1001","period":"2026-10-03","amount":"118.40"},
  {"type":"ngr","player_id":"PLR-1002","period":"October","amount":"-12.50"}
]
{
  "results": [
    {
      "index": 0,
      "status": "accepted",
      "event_type": "deposit",
      "event_id": "0199b2c9-03aa-7e10-b6d2-91c07f5e2a18",
      "commission": { "effect": "created", "type": "cpa", "amount_minor": 4000, "status": "hold" }
    },
    {
      "index": 1,
      "status": "duplicate",
      "event_type": "ngr",
      "event_id": "0199b2d1-5c0e-7b42-8a17-3e9d6f0c4b21",
      "commission": { "effect": "created", "type": "revshare", "amount_minor": 2960, "status": "hold" }
    },
    {
      "index": 2,
      "status": "rejected",
      "field": "period",
      "message": "period must be a day (2026-10-03) or a month (2026-10), not in the future."
    }
  ],
  "accepted": 1,
  "duplicate": 1,
  "rejected": 1
}
FieldTypeMeaning
resultsarrayOne entry per event, in the order sent.
results[].indexintegerThe position of the event in the array, from 0.
results[].statusstringaccepted, duplicate or rejected.
results[].event_typestringAs in a single answer. Not for a rejected event.
results[].event_idstringAs in a single answer. Not for a rejected event.
results[].attributionobjectAs in a single answer.
results[].commissionobjectAs in a single answer.
results[].waiting_for_playerbooleanAs in a single answer.
results[].fieldstringFor a rejected event: the field to fix.
results[].messagestringFor a rejected event: what to fix.
acceptedintegerHow many events were stored.
duplicateintegerHow many were already stored.
rejectedintegerHow many were refused.

POST /v1/files

Pushes a daily CSV file, as an alternative to uploading it on the Integration page or letting RevRake fetch it from an address your platform publishes it at. The body is the file itself; the headers and the signature are those of an event, and the signature covers the timestamp and the file. The file name goes in the filename query parameter; it is shown in the portal and is otherwise not used.

POST https://app.revrake.com/v1/files?filename=sales-2026-10-03.csv

The header row decides which file it is: a file with a registration_date column is a REG file, one with an activitydate column is a SALES file. Column names are matched without regard to case. Columns that are not listed below may stay in the file; they are ignored.

Dates are read as YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, YYYY-MM-DDTHH:MM:SS, RFC 3339 or YYYY/MM/DD. Without a UTC offset, the file timezone in workspace Settings applies (UTC by default). A timestamp with an offset identifies an exact moment; for SALES, its calendar day in the file timezone is used. Day and month are never guessed: 30/09/2026 is refused.

REG file: one row per registered player

ColumnRequiredMeaning
registration_dateyesWhen the player registered. Becomes ts.
player_idyesYour ID for the player.
btagyesThe btag stored on the player. May be empty: the player is then not attributed, unless a promo code attributes them.
promo_codenoOptional. The promo code the player typed at sign-up, used as the promo_code of a registration.
player_countrynoOptional. The player's two-letter country code (ISO 3166-1 alpha-2), used as the country of a registration. Empty: the country of the click.
brand_name,registration_date,btag,promo_code,player_id,player_country
Example,2026-10-03,1001_1,,PLR-1001,KE
Example,2026-10-03,,,PLR-1002,KE
Example,2026-10-03,,GARAGE50,PLR-1003,KE

SALES file: one row per player per day

ColumnRequiredMeaning
activitydateyesThe activity day, determined in the workspace’s file timezone.
player_idyesYour ID for the player.
currencyyesThe currency of the amounts in the row. Empty: the workspace's reporting currency.
depositsyesThe day’s gross deposits, including the first deposit that occurred that day. Not a lifetime total; do not subtract chargebacks.
first_deposit_amountyesThe first deposit’s amount, only on the day it occurred; zero on other days. Already included in deposits, never added again. Cannot exceed that day’s gross deposits.
chargebackyesTotal charged back on this activity day, separate from gross deposits. Not a lifetime total. It reduces cumulative CPA deposits but does not identify a particular transaction.
*_net_revenuenoEvery column named net_revenue or ending in _net_revenue. The filled-in cells are summed into the player's net revenue for the day, reported as ngr. A row with none filled in reports no revenue.
*_gross_revenuenoOptional. Columns named gross_revenue or ending in _gross_revenue, summed into the ggr of the day's ngr. Shown to the affiliate only.
*_bonusnoOptional. Columns named bonus or ending in _bonus, summed into bonuses. Shown only.
*_feesnoOptional. Columns named fees or ending in _fees, summed into fees. Shown only.
*_taxesnoOptional. Columns named taxes or ending in _taxes, summed into taxes. Shown only.
brand_name,activitydate,player_id,btag,currency,deposits,first_deposit_amount,chargeback,casino_net_revenue,sports_net_revenue
Example,2026-10-03,PLR-1001,1001_1,KES,3500.00,2500.00,0,118.40,0
Example,2026-10-03,PLR-1002,,KES,900.00,0,0,-12.50,
  • Each SALES row records one daily money summary and, when revenue cells are filled, a separate NGR event. They are processed independently: a rejected monetary summary does not undo accepted revenue, or vice versa. Check every problem in the answer.
  • One summary per brand, player and activity day; the first accepted currency is fixed. Identical monetary values replay even after renaming or reordering the file. Changing amounts, currency or the timezone boundaries of an accepted day is a conflict, including changes from zero. Previously recorded money stays unchanged.
  • deposits, first_deposit_amount and chargeback are required columns. Empty cells mean zero. Amounts must be nonnegative decimals with the precision of the row’s currency. Confirm these definitions with your platform before connecting its export.
  • Send REG before SALES. Money summaries received first are kept and priced when registration arrives. NGR for an unknown player is refused: after REG, resend SALES or NGR. The saved money summary replays.
  • A summary covers individual postbacks for its activity day and currency in the file timezone, without adding their amounts twice. Transaction IDs remain addressable. Known deposits or chargebacks exceeding the summary, or inconsistent first-deposit evidence, are conflicts.
  • For cumulative CPA, deposits minus chargebacks must reach the deal’s minimum. Each daily cashflow uses its activity date’s exchange rate; individual postbacks use their own UTC event day. Missing conversion rates hold settlement under fx_rate_missing. Missing chargeback rates block payment under chargeback_fx_missing until reconciliation, including commissions already claimed by a payout.
  • For first-deposit CPA, an aggregate chargeback may not identify which deposit was reversed. An ambiguous money summary for a registered player is refused. If the summary arrived before REG, it is kept and registration succeeds, but the resulting held CPA waits under chargeback_reference_missing. For transaction-reference review, send a chargeback with ref_txn_id set to the original deposit’s transaction ID.
  • Revenue is separate from immutable money totals: a changed daily NGR amount is a restatement, and its commission difference becomes an adjustment.

The answer

A readable file returns 200. Counts are per event, not just per row: a SALES row can record a money summary and NGR separately, including a partial success. Always check rejected and problems.

{
  "id": "0199b31e-2a40-7d18-9b6c-4f1e7a2c5d03",
  "filename": "sales-2026-10-03.csv",
  "kind": "sales",
  "uploaded_at": "2026-10-04T02:00:07Z",
  "rows_total": 2,
  "accepted": 3,
  "duplicates": 0,
  "rejected": 1,
  "skipped": 0,
  "problems": ["line 3: Player PLR-1002 is not registered. Send the registration first."]
}
FieldTypeMeaning
idstringRevRake's ID of this import.
filenamestringThe name you gave, or reg.csv / sales.csv.
kindstringreg or sales.
uploaded_atstringWhen it was received.
rows_totalintegerData rows in the file.
acceptedintegerEvents stored.
duplicatesintegerEvents that were already stored.
rejectedintegerRows or events refused. Each is in the inbound log with the line itself.
skippedintegerRows with nothing to record.
problemsarray of stringThe first 20 refused lines, as line N: reason. Left out when there are none.

GET /v1/reports

Answers the report of the Reports page: clicks, flagged clicks, registrations, first deposits and earnings, grouped one way, for the days up to today in the workspace's timezone. Today's figures are refreshed when a report is requested, at most every 30 seconds on a busy program.

GET https://app.revrake.com/v1/reports?by=link&days=7
ParameterTypeExampleMeaning
bystringlinkHow rows are grouped: day, link, sub1, country or affiliate. Default day. With affiliate set, by=affiliate is answered as day.
daysinteger7How many days, counting today: 1 to 365. Default 30.
fromstring2026-09-01The first day of any period, YYYY-MM-DD in the workspace's timezone. With from or to, days is ignored. At most 366 days.
tostring2026-09-30The last day of the period, included. Default today.
affiliateinteger1001An affiliate number. Narrows the report to that affiliate. Operator key only; ignored with an affiliate key, which always reads its own affiliate.
tagstringInfluencersA group's name, as on the Groups page (repeat for several: any of them). Operator key only; ignored with affiliate or an affiliate key. An unknown name is 422 unknown_tag.
formatstringcsvcsv answers a CSV file instead of JSON.
{
  "by": "link",
  "from": "2026-09-28",
  "to": "2026-10-04",
  "currency": "KES",
  "rows": [
    {
      "key": "0199a7f0-41c2-7e55-b1d9-6c3a8e2f7b10",
      "label": "telegram-october",
      "clicks": 1840,
      "flagged_clicks": 212,
      "registrations": 96,
      "ftds": 31,
      "earnings_minor": 12400000
    }
  ],
  "total": {
    "key": "",
    "label": "",
    "clicks": 1840,
    "flagged_clicks": 212,
    "registrations": 96,
    "ftds": 31,
    "earnings_minor": 12400000
  }
}
FieldTypeMeaning
bystringThe grouping that was used.
fromstringThe first day of the report, YYYY-MM-DD.
tostringThe last day: today in the workspace's timezone, or the to you gave.
currencystringThe workspace's reporting currency. Earnings are in it.
rowsarrayOne entry per group.
rows[].keystringWhat identifies the group: the day, RevRake's ID of the link or the affiliate, the sub1 value, or the two-letter country.
rows[].labelstringThe text the portal shows for the group: the day, the campaign label of the link, the sub1 value, the country, or the affiliate's name and number.
rows[].clicksintegerClean clicks.
rows[].flagged_clicksintegerClicks flagged as bots, repeats, bursts or of an affiliate who is not approved. Not in clicks.
rows[].registrationsintegerPlayers attributed.
rows[].ftdsintegerFirst deposits.
rows[].earnings_minorintegerCommissions earned, in minor units of the reporting currency, at the rate of the day each was earned.
totalobjectThe sum of the rows, in the shape of a row.

As CSV

With format=csv the answer is a file. Its first column is named after the grouping and holds the label; then clicks, flagged_clicks, registrations, first_deposits and earnings_<currency> as a decimal. The last line is the total.

link,clicks,flagged_clicks,registrations,first_deposits,earnings_KES
telegram-october,1840,212,96,31,124000.00
total,1840,212,96,31,124000.00

GET /v1/commissions

Answers one page of the commission ledger, newest first unless you sort otherwise. Rows are never edited in their amounts and never deleted, but a row's status changes after it is written: hold, then approved, then paid, or rejected. A sync must read a period again and update rows by id, not only fetch new rows.

GET https://app.revrake.com/v1/commissions?from=2026-09-01&to=2026-10-01&per_page=500
ParameterTypeExampleMeaning
statusstringapprovedhold, approved, paid or rejected. Default: all.
fromstring2026-09-01Only rows that occurred at or after this time: RFC 3339, or a day YYYY-MM-DD in UTC.
tostring2026-10-01Only rows that occurred before this time. Same forms.
affiliateinteger1001An affiliate number. Narrows the rows, totals and owed to that affiliate. Operator key only; ignored with an affiliate key.
tagstringInfluencersA group's name, as on the Groups page (repeat for several: any of them). Operator key only; ignored with affiliate or an affiliate key. An unknown name is 422 unknown_tag.
qstringPLR-1001Rows whose player ID or affiliate name contains this text. Up to 100 characters.
sortstringwhenwhen, affiliate, player, status, deposit or commission. Default when.
dirstringascasc or desc. Default desc.
pageinteger2The page, from 1. Default 1.
per_pageinteger50010, 25, 50, 100, 500 or 1000. Default 25.
{
  "currency": "KES",
  "totals": { "hold": 1240000, "approved": 3720000, "paid": 9800000 },
  "owed": { "KES": { "hold": 1240000, "approved": 3720000, "paid": 9800000 } },
  "period": true,
  "commissions": [
    {
      "id": "0199b2c9-0a11-7c3e-9f20-7d4b1e6a2c58",
      "occurred_at": "2026-09-14T08:20:00Z",
      "type": "cpa",
      "currency": "KES",
      "gross_currency": "KES",
      "reporting_minor": 400000,
      "affiliate_name": "Odds Garage",
      "affiliate_number": 1001,
      "player_ref": "PLR-1001",
      "deal_name": "CPA 4000",
      "deal_version": 2,
      "gross_minor": 250000,
      "rate_snapshot": 400000,
      "amount_minor": 400000,
      "status": "approved",
      "status_reason": "",
      "hold_until": "2026-10-14T08:20:00Z",
      "status_note": ""
    }
  ],
  "total": 1,
  "page": 1,
  "per_page": 500
}
FieldTypeMeaning
currencystringThe workspace's reporting currency.
totalsobjectThe sum per status, in minor units of the reporting currency: over the whole ledger, or over the period when from or to is given. Not narrowed by the other filters, except affiliate. Rows that still wait for an exchange rate are not in it.
owedobjectThe sum per currency and status, in each commission's own currency: what is owed. Over the same rows as totals.
periodbooleantrue when from or to was given, so totals and owed cover that period only.
commissionsarrayThe rows of this page.
totalintegerHow many rows match the filters.
pageintegerThe page that was answered.
per_pageintegerRows per page that was used.

A commission row

FieldTypeMeaning
idstringRevRake's ID of the row, a UUID. It never changes: update by it.
occurred_atstringWhen the commission was earned: the time of the deposit, the end of the revenue period, or the time of the chargeback.
typestringcpa, revshare, adjustment, reversal or tier.
amount_minorintegerThe commission, in minor units of currency. Negative for a reversal and for a downward adjustment.
currencystringThe currency of the commission: the deal's.
reporting_minorinteger | nullThe commission in the workspace's reporting currency, at the rate of the day it was earned. null while that rate is missing.
gross_minorintegerThe deposit or the net revenue behind the commission, in minor units of gross_currency.
gross_currencystringThe currency of the deposit or the revenue.
rate_snapshotintegerThe terms that were applied: the CPA amount in minor units, or the revenue share in hundredths of a percent (2500 is 25%).
statusstringhold, approved, paid or rejected. See commission statuses.
status_reasonstringWhy a row is still on hold or was rejected. Empty otherwise. Every value is in the table of reasons.
status_notestringThe operator's note on a commission taken back, as the affiliate reads it. Empty otherwise.
hold_untilstring | nullWhen the hold period ends. null for a reversal.
affiliate_numberintegerThe affiliate's number.
affiliate_namestringThe affiliate's name.
player_refstringYour ID for the player.
deal_namestringThe deal.
deal_versionintegerThe version of the deal that priced the row.

Program lists: affiliates, links, deals

Your backend can list the program as the portal shows it, signed like the other reads and counted in the same limit. GET /v1/affiliates and GET /v1/deals need the operator key; with an affiliate key they answer 403 operator_key_required. GET /v1/links answers the brand's links, or one affiliate's with affiliate=<number>; with an affiliate key, that affiliate's own links.

Lists are paged with page (from 1) and per_page (1 to 500, default 100) and carry total. GET /v1/affiliates also takes status: pending, approved, rejected or suspended. Creating affiliates or links through the API is not offered yet: tell us what you would use it for.

GET https://app.revrake.com/v1/links?affiliate=1001&per_page=100

Each affiliate

FieldTypeMeaning
numberintegerThe affiliate number, the first part of the btag; in a link, the link number, its second part.
namestringThe affiliate's name.
emailstringThe affiliate's email address.
statusstringpending, approved, rejected or suspended.
dealstringThe name of the deal the affiliate is on; empty before approval.
created_atstringWhen it was created, RFC 3339.
decided_atstring | nullWhen the application was approved or rejected, or the account suspended; null before.

Each link

FieldTypeMeaning
affiliate_numberintegerThe number of the affiliate the link belongs to.
numberintegerThe affiliate number, the first part of the btag; in a link, the link number, its second part.
codestringThe code in the link's address.
urlstringThe tracking link, on the brand's verified tracking domain or the shared one.
btagstring<affiliate_number>_<number>.
campaignstringThe link's campaign label.
landing_pathstring | nullThe page on your site the link lands on; null for the brand's default.
subsstring[5]sub1 to sub5 fixed on the link; empty strings when unset.
promo_codestring | nullThe promo code, on a link made for one; else null.
vanity_urlstring | nullThe short address /go/<name>, when the affiliate set one; else null.
created_atstringWhen it was created, RFC 3339.

Each deal

FieldTypeMeaning
namestringThe affiliate's name.
currencystringThe currency the deal earns and pays in.
listedbooleanWhether the public join page shows the deal.
defaultbooleanWhether new affiliates get this deal.
versionintegerThe version of the deal in effect now.
effective_atstringWhen that version took effect, RFC 3339.
termsobjectThe terms of that version: amounts in minor units, shares in basis points (2500 = 25%), hold days, losing-month rule, the deal's own attribution window, tiers.

Warehouse export

Set up on the Integration page: every night RevRake writes the previous day of each brand to your own bucket on any S3-compatible service, at <folder>/<brand>/<YYYY-MM-DD>/, the day counted in the workspace's timezone. Four gzipped CSV files (UTF-8, a header row, RFC 4180 quoting), then manifest.json. The manifest is written last: load a day once it exists, and check each file's rows, bytes and sha256 against it.

A row belongs to the day it arrived (a click: when it was clicked), so a daily file that comes late is in the day it came, never lost. Times are RFC 3339 in UTC with milliseconds; amounts are decimals in their currency. Statuses are as they were at the export: a commission's status changes later, so read GET /v1/commissions again for the current one. A failed day is retried every hour and caught up for up to seven days; exporting a day again overwrites its files. New columns may be added at the end of a file; schema_version in the manifest changes when a loader must change.

manifest.json: schema_version, workspace, brand, day, timezone, from and to (the day's bounds in UTC), generated_at, and files: for each, name, rows, bytes, sha256 (of the gzipped file).

clicks.csv.gz: every click on the brand's links that day, flagged ones included.

FieldTypeMeaning
click_idstringThe click ID, as passed to your landing page and in postbacks.
clicked_atstringWhen the click happened.
affiliate_numberintegerThe affiliate number.
link_numberintegerThe link number.
btagstring<affiliate_number>_<link_number>.
countrystringTwo-letter country code; empty when unknown.
devicestringdesktop, mobile, tablet or bot, from the browser.
osstringThe operating system, from the browser.
browserstringThe browser.
sub1 … sub5stringsub1 to sub5: the sub-IDs of the click.
utm_source … utm_contentstringutm_source to utm_content: the UTM tags of the click.
gclid, fbclid, ttclid, msclkid, wbraidstringgclid, fbclid, ttclid, msclkid, wbraid: ad-platform click IDs on the landing URL.
refererstringThe page the visitor came from.
flagsstringWhy the click is not counted, separated by ;: bot, duplicate, burst, inactive. Empty for a clean click.

players.csv.gz: players whose registration arrived that day.

FieldTypeMeaning
player_idstringYour player ID.
registered_atstringThe registration time you reported.
arrived_atstringWhen RevRake recorded it.
countrystringTwo-letter country code; empty when unknown.
attributedbooleantrue when an affiliate is credited.
attribution_reasonstringWhy, as in the postback answer: click, btag_only, promo_code, no_tag…
affiliate_numberintegerThe affiliate number.
link_numberintegerThe link number.
btagstring<affiliate_number>_<link_number>.
click_idstringThe click that credited the player; empty without one.
policy_versionintegerThe attribution policy version that decided it.

events.csv.gz: every accepted brand event that arrived that day, including daily SALES summaries, with its payload.

FieldTypeMeaning
event_idstringRevRake's ID of the event.
typestringregistration, deposit, chargeback, daily_sales or ngr; for a commission, cpa, revshare, tier, adjustment or reversal.
external_idstringThe event's key: the player ID, the transaction ID, or player and period.
player_idstringYour player ID.
occurred_atstringWhen it happened, as you reported it; for a commission, when it was earned.
received_atstringWhen RevRake accepted the event.
sourcestrings2s (the API) or file (a daily file).
payloadJSONThe event as RevRake read it, JSON.
resultJSONWhat RevRake answered, JSON.

commissions.csv.gz: commission rows written that day.

FieldTypeMeaning
commission_idstringThe row's ID, the id in GET /v1/commissions.
created_atstringWhen the row was written.
occurred_atstringWhen it happened, as you reported it; for a commission, when it was earned.
typestringregistration, deposit, chargeback, daily_sales or ngr; for a commission, cpa, revshare, tier, adjustment or reversal.
statusstringhold, approved, paid or rejected, at the export.
status_reasonstringWhy it is held or rejected; empty otherwise.
affiliate_numberintegerThe affiliate number.
player_idstringYour player ID.
dealstringThe deal's name.
deal_versionintegerThe deal version that priced it.
grossdecimalThe deposit or net revenue behind it.
gross_currencystringIts currency.
amountdecimalThe commission.
currencystringThe deal's currency.
reporting_amountdecimalThe commission in the reporting currency; empty while the exchange rate is missing.
reporting_currencystringThe workspace's reporting currency.
reverses_idstringFor a reversal, the row it reverses.
payout_item_idstringThe payout line that claimed it, once a month closed.

Idempotency

Each event has a natural key. An identical retry returns the stored answer with "status":"duplicate". A deposit waiting for registration has its answer updated when registration arrives. Changed monetary facts can conflict; NGR restatements use revisions. After a timeout, retry with a fresh signature timestamp.

EventKeyThe same key again
registrationplayer_idThe first answer. The attribution is not decided again, even if the tags differ.
deposittxn_idThe stored answer, updated after a waiting registration arrives. Changing the player, amount or currency conflicts. A covering SALES summary does not discard the transaction ID.
chargebacktxn_idThe first answer. A second chargeback of the same deposit under a new txn_id is refused.
ngrplayer_id and periodThe same amount and currency: the first answer. A different amount: a correction, stored as a new revision, with the difference as an adjustment.
CSVREG row: player_idAs a registration.
CSVSALES money summary: brand, player and activity dayIdentical totals replay. Changed deposits, first-deposit amount, chargebacks, currency or day boundaries conflict, even for a previously zero summary.
CSVSALES row, revenue: the player and the dayAs ngr: a changed figure in a re-export is a correction.

Statuses and errors

Every answer the API can give and every status a record can have, in one place. An error is answered with a JSON object: error (the code), message (a sentence in English) and, for invalid, field.

{"error":"invalid","field":"amount","message":"amount must be a positive decimal in KES, for example 25.00."}

HTTP statuses and error codes

StatuserrorEndpointWhenWhat to do
200–/v1/*The request was handled. For an event: stored, or already stored. For a batch or a file: read; look at the counts, some items may have been refused.
400bad_requestPOST /v1/postbackThe body starts as an array but is not a JSON array of events.Fix the request. The same request again gets the same answer.
401unknown_key/v1/*The key in X-RevRake-Key is unknown or revoked, or it is an affiliate key on a write endpoint.Use the current key. Sending it again does not help.
401bad_timestamp/v1/*X-RevRake-Timestamp is missing, is not a whole number of seconds, or is more than five minutes off.Check the clock and the signing code, then send it again with a fresh timestamp.
401bad_signature/v1/*The signature does not match the timestamp and the body (for a read: the path and the query).Check the clock and the signing code, then send it again with a fresh timestamp.
403affiliate_inactiveGET /v1/*A read with an affiliate key whose affiliate is not approved in the program: pending, rejected or suspended.Nothing to fix in the request: the account must be approved by the operator.
404unknown_affiliateGET /v1/*A read with an operator key: no affiliate has the number in affiliate.Fix the request. The same request again gets the same answer.
405–/v1/*The endpoint does not accept that method. No JSON body.Fix the request. The same request again gets the same answer.
413too_largePOST /v1/*The body is larger than the limit of the endpoint.Fix the request. The same request again gets the same answer.
422invalidPOST /v1/*, GET /v1/reportsThe event or the file cannot be used as it is.Fix the request. The same request again gets the same answer.
429rate_limitedGET /v1/*More reads than the key's allowance. The answer carries Retry-After.Wait the seconds in Retry-After, then send it again.
500internal/v1/*Something went wrong on our side.Send it again.
  • A batch with no events or more than 1,000 is refused with 422 invalid and no field.
  • A 401 for a bad timestamp or signature with an operator key is listed in the inbound log as bad_signature. A request with an unknown key, or with an affiliate key, is not logged.
  • A read answers 422 only for a report period that cannot be used (from and to). Any other parameter with a value that is not allowed is ignored and its default is used. Check the by, page and per_page in the answer.
  • Retry on a timeout, on 429 and on 500. Do not retry any other 4xx unchanged.

What field names in a 422

fieldWhen
bodyThe body is not valid JSON, or carries an unknown or malformed field (for example a ts that is not RFC 3339).
typetype is missing or not one of the four.
player_idMissing or longer than 100 characters; a placeholder that was not filled in; for ngr, a player who is not registered.
click_idA placeholder that was not filled in. The message starts with misconfigured_macro.
btagA placeholder that was not filled in. The message starts with misconfigured_macro.
promo_codeA placeholder that was not filled in. The message starts with misconfigured_macro.
txn_idMissing or longer than 200 characters.
ref_txn_idMissing; no deposit with that txn_id is known; or the deposit was already charged back.
amountNot a decimal the currency allows; for a deposit, not above zero.
currencyNot a three-letter code.
ggrFor ngr: not a decimal amount in the currency.
bonusesFor ngr: not a decimal amount in the currency.
feesFor ngr: not a decimal amount in the currency.
taxesFor ngr: not a decimal amount in the currency.
tsMore than a day in the future.
periodNot a day or a month, or in the future.
fromFor GET /v1/reports: from or to is not YYYY-MM-DD, from is after to, or they are more than 366 days apart.
fileFor /v1/files: the file is empty, is not CSV, is neither a REG nor a SALES file, lacks a required column, or has more than 200,000 rows.
chargebackIn a SALES file: the chargeback cell cannot be applied. The message says why.

The status of an event

In the answer to one event, and per event in results of a batch.

statusMeaning
acceptedStored now. The answer says what it did.
duplicateAlready stored under the same key; an identical replay changes nothing. A legacy cross-channel first-deposit match may also return duplicate_of. Current SALES summaries keep actual postback transactions and count covered daily amounts only once.
rejectedOnly in a batch: the event was refused, with field and message. A single event that is refused is a 422 instead.

The effect of an event on the ledger

commission.effectMeaning
createdA commission row was created: a CPA, a revenue share, or an adjustment for a corrected figure.
cancelledA chargeback rejected a commission that was still on hold.
reversedA chargeback added a reversal row against a commission already approved or paid.
noneNo row was created. reason says why.

Commission types

typeMeaning
cpaOne per player when the deal’s minimum is met by the first deposit or cumulative deposits net of chargebacks, according to its CPA baseline.
revshareA share of a player's net gaming revenue for a period. Negative for a period the player won.
adjustmentThe difference a corrected revenue figure makes.
reversalThe negative of a commission taken back after it was approved or paid: by a chargeback, or by the operator with a reason. It is approved at once and nets against the next payout.
tierA top-up from a volume tier of the deal: once an affiliate brings enough new depositing players in a calendar month, the difference between the tier's rate and the base rate is added beside each commission of that month. It waits on the same hold as its commission, or is approved at once if that one already is. Negative when it takes back an earlier top-up, after its commission was rejected or reversed.

Commission statuses

A row is written once and its amounts never change. Its status does:

statusMeaning
holdWaiting: for the hold period of the deal to end, or for what status_reason names.
approvedThe hold period ended and nothing holds the row. It goes into the next monthly payout batch. Reversals and some adjustments are approved from the start.
paidThe payout item that contains the row was marked as paid.
rejectedIt will not be paid. status_reason says why, and status_note carries the operator's note when there is one.

Values of status_reason

Empty when a row only waits for its hold period, and normally for approved or paid commissions. A missing chargeback rate can also block an approved row. Fraud holds wait for the operator or the configured flag expiry.

status_reasonWith statusMeaning
fx_rate_missingholdA required exchange rate is missing. Settlement waits until a rate arrives; daily SALES cashflows use their activity date, individual postbacks their UTC event day.
chargeback_fx_missinghold, approved, paidA chargeback cannot yet be valued. The CPA and its tier top-ups cannot enter a new payout or be marked paid in an existing one until the rate arrives and reconciliation completes.
chargeback_reference_missingholdA summary received before registration contains an aggregate chargeback that cannot identify the reversed deposit for first-deposit CPA. Registration and the summary are kept; the resulting held CPA waits for transaction-reference review.
self_referralholdAn open fraud flag: the player looks like the affiliate themselves. Waits for the operator to release or confirm the flag.
click_velocityholdAn open fraud flag, or a click that was part of a burst: more clicks on one link from one address in a minute than the brand allows. Waits for the operator.
duplicate_conversionholdAn open fraud flag: a second "first" deposit was reported for the player. Waits for the operator.
paid_trafficholdAn open fraud flag: the click that brought the player carried an ad network's click ID. Waits for the operator.
disposable_emailholdAn open fraud flag: the player signed up with a throwaway email domain. Waits for the operator.
geo_mismatchholdAn open fraud flag: the player registered in another country than the one of the click. Waits for the operator.
fast_registrationholdAn open fraud flag: the player registered within the set number of seconds after the click. Waits for the operator.
affiliate_pendingholdThe affiliate was not yet approved when the commission was earned. When the affiliate is approved, the row waits only for its normal hold period.
affiliate_rejectedholdThe affiliate's application was rejected.
affiliate_suspendedholdThe affiliate was suspended when the commission was earned. When the affiliate is reinstated, the row waits only for its normal hold period.
chargebackrejected, approvedOn a rejected row: a chargeback cancelled it while on hold. On a reversal row: it reverses a commission after a chargeback.
below_thresholdrejectedOnce rates were available, the qualifying first-deposit or cumulative net-deposit amount was below the deal’s minimum.
fraudrejected, approvedThe operator confirmed the fraud flag that held the row, or took the commission back for fraud.
base_rejectedrejectedA tier top-up whose commission was rejected while both were on hold.
self_excludedrejected, approvedThe operator took the commission back: the player self-excluded. On a rejected row the commission was not yet in a payout; on a reversal row it was.
duplicate_accountrejected, approvedThe operator took the commission back: the player has another account.
bonus_abuserejected, approvedThe operator took the commission back for bonus abuse.
terms_breachrejected, approvedThe operator took the commission back for a breach of the program's terms.
test_accountrejected, approvedThe operator took the commission back: the player was a test account.
otherrejected, approvedThe operator took the commission back for another reason, written in status_note.

Payout statuses

A month closes into a payout batch with one item per affiliate and currency. Batches and items are worked on in the portal; the API shows their result as the paid status of the commission rows.

Batch statusMeaning
draftThe month was closed. The operator reviews the items.
approvedLocked. With invoicing on, each payable item gets an invoice number. The bank file (CSV) can be downloaded and the items paid.
paidEvery payable item was marked as paid.
Item statusMeaning
payableTo be paid in this batch.
carriedBelow the minimum payout, or not above zero: carried forward into the next batch.
heldThe affiliate has an open fraud flag, or the program holds payouts until identity is checked and the check is not recorded yet. The portal says which. The operator can release the item, and recording the identity check releases it.
no_detailsThe affiliate has not entered where to be paid. Not in the bank file.
written_offThe month ended below zero and the affiliate's deal does not carry a losing month: the balance is written off at the close and nothing carries into the next batch. With a cap, the item is carried and records the part above the cap as written off.
paidMarked as paid. Its commission rows are paid, and the affiliate's tracker is told.

Why a file row is refused

A refused row is counted in rejected, listed in problems as line N: reason, and kept in the inbound log with the line itself. Besides the reasons an event can be refused for:

  • The line is not valid CSV.
  • REG: registration_date or player_id is missing or cannot be read.
  • SALES: activitydate or player_id is missing or cannot be read.
  • SALES, revenue: the player is not registered.
  • SALES, money: an amount is negative, unreadable or has too many decimals for its currency.
  • SALES: the first-deposit amount exceeds gross deposits.
  • SALES: an accepted day is re-exported with changed money totals, currency or timezone boundaries.
  • SALES: known individual deposits or chargebacks exceed the day’s summary.
  • SALES: first-deposit evidence conflicts with another day or a known transaction.
  • SALES: an aggregate chargeback is ambiguous for first-deposit CPA of an already registered player.

Outcomes in the inbound log

StatusMeaning
acceptedStored, with what it did.
duplicateAlready stored; the first answer was returned.
rejectedRefused; the detail says which field and why.
bad_signatureThe timestamp or the signature was wrong.
testA test event from the Integration page. It runs through the real path and stores nothing.

States of a postback delivery

StatusMeaning
queuedWaiting for its first try, or for the next retry. The portal shows it as waiting or retrying.
deliveredThe tracker answered with a 2xx status.
deadEvery retry failed. The portal shows it as given up; the affiliate can send it again by hand.

Statuses of an affiliate

StatusMeaning
pendingApplied; the operator has not decided.
approvedActive in the program.
rejectedThe application was declined.
suspendedStopped by the operator.

The tracking link

A tracking link is opened by a visitor, not called by your platform. It stores the click and answers with a 302 redirect to the brand's landing page. Until the brand has verified its site, the visitor first sees a short page saying where the link leads. The destination host comes from the brand settings; a link can only choose a path on it.

GET https://track.revrake.com/c/a8Kq2Zx?sub1=TRACKER_CLICK_ID&utm_source=telegram
HTTP/1.1 302 Found
Location: https://your-site.example/landing?btag=1001_1&cid=01K6QF3M8Z5T7W9XB2C4D6E8GH&utm_source=telegram
Cache-Control: no-store

Parameters on the link

ParameterMeaning
sub1 … sub5The affiliate's own values, stored with the click and returned in postbacks. A value on the address replaces the one fixed on the link.
utm_source utm_medium utm_campaign utm_term utm_contentStored with the click and passed on to the landing page. A value on the address replaces the one fixed on the link.
gclid fbclid ttclid msclkid wbraidAd-network click IDs. Stored with the click; available as postback macros. Not passed on.

Parameters added to the landing URL

ParameterMeaning
cidThe click ID: 26 characters of Crockford base32 (digits and upper-case letters without I, L, O, U). It sorts by time. Store it as text.
btag<affiliate number>_<link number>: one to nine digits, an underscore, one to nine digits.
utm_source … utm_contentThe UTM tags of the click, when they are not empty.
  • If the landing URL of the brand contains {click_id} in its query, the click ID is put there and cid is not added.
  • A click on the link of an affiliate who is not approved is stored and flagged, and lands without cid and btag: it cannot be attributed.
  • Bots, link previews, HEAD requests, requests without a user agent and repeat clicks within the hour are stored and flagged, not dropped. Reports leave them out.
  • Other parameters on the link are not stored and not passed on.
  • A link the affiliate has given a short name also answers at /go/<name>, exactly as at /c/<code>. A brand with its own verified tracking domain serves both paths on that domain too.
  • An unknown code is answered with 404 and an HTML error page, and so is a link whose brand has not set a destination yet.

Postbacks to affiliate trackers

This part needs no work from the operator. Each affiliate adds up to 10 destinations in their portal: an address template with macros, and the events to be told about. When an event is recorded for one of the affiliate's players, RevRake renders the template and calls it with a GET request.

Events

Event{status}When
registrationleadA player was attributed to the affiliate.
ftdsaleA first deposit earned the affiliate a CPA commission.
commission_approvedapprovedA commission was approved when its hold period ended.
commission_rejectedrejectedA commission on hold was rejected after a review.
commission_paidpaidA commission was paid in a payout batch.
chargebackrejectedA chargeback took back a commission.

Status words per preset

A destination made from a preset sends, in {status}, the words its tracker knows. The presets of Voluum, PeerClick and FunnelFlux Pro, and "Other / custom", send the words of the table above. The words follow the preset the destination carries; its saved address is never changed.

EventKeitaroBinomRedTrackBeMob
registrationleadleadothernew
ftdsalesalependingapproved
commission_approvedsalesaleapprovedapproved
commission_rejectedrejectedrejectdeclineddeclined
commission_paidsalesaleapprovedapproved
chargebackrejectedrejectdeclinedchargeback

Macros

A macro is a name in braces. It is replaced by its value, URL-encoded. Names are matched without regard to case. A name RevRake does not know is sent as written, so a typo shows in the delivery log. A value that is not known is empty.

MacroMeaning
{click_id}The RevRake click ID (the cid on the landing URL). Empty when the player was attributed by btag alone.
{sub1} … {sub5}sub1 to sub5 of the click. sub1 usually carries the click ID of the affiliate's tracker.
{event}The event name, as in the table above.
{status}The word trackers use for the event, as in the table above, or the tracker's own word at a destination made from a preset (see the next table).
{event_id}Different for every event and stable across retries.
{conversion_id}The same for every event about one commission: ftd, commission_approved, commission_rejected, commission_paid and chargeback. A registration has its own. Use it as the transaction ID in a tracker that updates a conversion arriving again under the same ID, so that the revenue is counted once.
{player_id} {affiliate_id} {brand_id} {link_id}The operator's player ID, the affiliate number, the program and the link number.
{campaign}The campaign label of the link.
{commission} {commission_minor}The commission, as a decimal (40.00) or in minor units (4000). Empty for a registration, 0 for a rejected commission, negative for a chargeback.
{amount} {amount_minor}The deposit or the revenue behind the commission, as a decimal or in minor units, in the currency it was made in. Empty for a registration.
{amount_currency}The ISO code of the deposit's currency. Empty for a registration.
{commission_currency}The ISO code of the commission's currency: the deal's. Empty for a registration.
{currency}The ISO code of the money the postback is about: the commission's currency when there is a commission, else the deposit's, else the workspace's reporting currency.
{occurred_at} {occurred_at_unix}When it happened, in RFC 3339 form or in Unix seconds.
{country} {device} {os} {browser} {referer}What is known about the click: country, device kind, operating system, browser, referrer.
{utm_source} {utm_medium} {utm_campaign} {utm_term} {utm_content}The UTM tags of the click.
{gclid} {fbclid} {ttclid} {msclkid} {wbraid}Ad-network click IDs, when the click carried them.

Other names for the same macros

Trackers use different names for the same things. These are accepted as well:

{subid}= {sub1}
{clickid}= {click_id}
{payout}= {commission}
{txid}= {event_id}
{transaction_id}= {event_id}

Presets

The affiliate portal ships these templates. The affiliate replaces the host with their tracker's. Each follows its tracker's documentation: {conversion_id} is in the transaction parameter where the tracker updates a conversion that arrives again under the same ID, {event_id} where it would discard the later events.

Keitarohttps://YOUR-TRACKER-DOMAIN/POSTBACK-KEY/postback?subid={sub1}&status={status}&payout={commission}&currency={currency}&tid={conversion_id}
Binomhttps://YOUR-TRACKER-DOMAIN/click?cnv_id={sub1}&payout={commission}&cnv_status={status}&cnv_currency={currency}
Voluumhttps://YOUR-TRACKER-DOMAIN/postback?cid={sub1}&payout={commission}&currency={currency}&txid={event_id}&et={event}
RedTrackhttps://YOUR-TRACKER-DOMAIN/postback?clickid={sub1}&sum={commission}&status={status}&type={event}&rdtk_event_id={event_id}
BeMobhttps://YOUR-TRACKER-DOMAIN/postback?cid={sub1}&payout={commission}&txid={conversion_id}&status={status}
PeerClickhttps://YOUR-TRACKER-DOMAIN/postback?cid={sub1}&payout={commission}&txid={conversion_id}&status={status}&currency={currency}
FunnelFlux Prohttps://YOUR-TRACKER-DOMAIN/pb/?hit={sub1}&tx={conversion_id}&rev={commission}&status={status}

An example

https://YOUR-TRACKER-DOMAIN/postback?click_id={sub1}&event={event}&status={status}&payout={commission}&currency={currency}&tid={conversion_id}&eid={event_id}
GET https://tracker.example/postback?click_id=TRACKER_CLICK_ID&event=ftd&status=sale&payout=40.00&currency=KES&tid=0199b2c9-0a11-7c3e-9f20-7d4b1e6a2c58&eid=0199b2c9-03aa-7e10-b6d2-91c07f5e2a18%3Aftd

Delivery

  • A postback is queued in the same transaction as the event it reports, and sent within about ten seconds, oldest first.
  • The request is a GET with the user agent RevRake-Postback/1. It follows at most three redirects and gives up after 5 seconds. Certificates are verified.
  • A 2xx answer is a success. Anything else is a failure and is retried after 1 minute, 5 minutes, 25 minutes, 2 hours, 10 hours and 24 hours.
  • After the last retry the delivery is marked as given up. The affiliate can send it again by hand.
  • Every attempt is recorded with its duration, the status and the first 500 characters of the answer.
  • After 5 and after 15 failures in a row the owner of the destination gets an email. At 20 the destination is switched off. Its deliveries, including those of events that happen while it is off, keep queuing and are sent when the affiliate switches it back on. A destination the affiliate switched off themselves gets nothing new.
  • RevRake only calls public addresses. Loopback, private, link-local and carrier-grade NAT addresses are refused when the connection is made, so a redirect cannot get around it.
  • The template must be a full http:// or https:// address of at most 2,000 characters.

Why an attempt failed

timeoutNo answer in 5 seconds.
dnsThe host name was not found.
refusedThe connection was refused.
blocked_addressThe address is private or local.
tlsA certificate problem.
too_many_redirectsMore than three redirects.
http_errorThe tracker answered with a status other than 2xx.
otherThe connection failed for another reason.

Operator webhooks

Webhooks tell the operator's own systems (a CRM, a data warehouse, a chat channel) what happens in the program. A workspace admin adds an endpoint on the Integration page of the operator portal: the address on your server, the events to send, and an optional description. For each of those events, RevRake sends a signed JSON POST to the address. Other team members cannot see or change the endpoints.

The address is a full https:// address on a public host (http:// is accepted, but use https://). Each endpoint has its own secret, which starts with whsec_. It is shown once, when the endpoint is created or its secret is rotated: copy it then. Rotating makes a new secret at once, and the old one stops signing.

An endpoint can also be a Slack or Microsoft Teams channel: choose “Slack message” or “Microsoft Teams message” and paste the channel's incoming-webhook address (for Teams, a workflow “When a Teams webhook request is received”). Each event then arrives as a sentence in the channel instead of signed JSON, and an amount can keep only first deposits from that size.

Events

Tick one or more per endpoint. The fields of data are described below; a field that does not apply is left out.

EventFields of dataWhen
affiliate.appliedaffiliate_number affiliate_name email promotes_on trafficSomeone applied on your program page or through a per-deal invite link. It is sent when the application is made, which can be before the applicant has confirmed their email. An affiliate you add and invite yourself sends affiliate.approved instead.
affiliate.approvedaffiliate_number affiliate_name email reason decided_by invitedYou approved an affiliate: a new application, or one you had rejected or suspended. An affiliate you add and invite yourself is approved at once and sends this event with invited set to true.
affiliate.rejectedaffiliate_number affiliate_name email reason decided_byYou rejected an application.
affiliate.suspendedaffiliate_number affiliate_name email reason decided_byYou suspended an approved affiliate.
player.registeredplayer_id occurred_at attributed affiliate_number affiliate_name attribution_reasonA player was registered, by a signed event or a row of a REG file, attributed or not. Once per player.
commission.createdplayer_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currencyA commission row was created: a qualifying CPA under the deal’s first-deposit or cumulative-deposit baseline, or a revenue share or adjustment for reported net revenue. Tier top-ups and reversals send none.
commission.approvedplayer_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currencyA commission on hold was approved because its hold period ended, tier top-ups included. A row approved from the start sends none.
commission.rejectedplayer_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currencyA commission on hold was rejected because you confirmed the fraud flag that held it. commission is zero. A CPA cancelled by a chargeback sends commission.reversed instead.
commission.paidplayer_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currencyA commission row was paid: one event per row of a payout item marked as paid, reversal and tier rows included.
commission.reversedplayer_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currencyA chargeback took back a CPA: cancelled while on hold, or reversed by a new row after it was approved or paid. commission_id names the CPA, commission is its negative and amount is the deposit that was charged back.
payout.batch_closedbatch_id period reference items payable_by_currency closed_by_handA month was closed into a draft payout batch, by a person or by the scheduled close.
payout.batch_approvedbatch_idA draft payout batch was approved and locked.
payout.paidbatch_id item_id affiliate_number affiliate_name currency amountAn item of an approved batch was marked as paid. A batch whose items are all paid sends no event of its own.
tracking.health_alertbrand kind since quiet_hours clicks_at_leastA brand's links got at least the set number of clicks in the set hours and no registration arrived (Integration page, Tracking health). Sent once; checked every hour.
tracking.health_clearedbrand raised_at cleared_atThe first registration after a tracking health alert arrived, and the alert is cleared.
flag.raisedkind detail affiliate_number affiliate_nameA new fraud flag was raised on an affiliate (by a rule, a repeated first deposit or self-referral). Flags for click bursts are on the Flags page only.
postback.destination_offdestination failures affiliate_number affiliate_nameAn affiliate's postback destination was switched off after the failed deliveries in a row that the retry ladder allows.
file.latesource dateA daily file fetched on a schedule had not arrived three hours after its fetch time. Once per file and day.
webhook.testmessageOnly from the "Send test" button. It cannot be ticked, is not retried and is not in the delivery log.

The body

Every request carries one JSON object:

FieldTypeMeaning
idstringThe ID of the event, a UUID. The same for every endpoint that receives the event and on every retry: use it to drop repeats.
typestringThe event, as in the table above.
created_atstringWhen the event was queued, in RFC 3339 form in UTC, with fractions of a second.
dataobjectThe facts of the event. See the fields below.
{
  "id": "0199b2c9-0a11-7c3e-9f20-7d4b1e6a2c58",
  "type": "commission.created",
  "created_at": "2026-10-05T09:12:44.318204Z",
  "data": {
    "player_id": "PLR-1001",
    "occurred_at": "2026-10-05T09:12:40Z",
    "attributed": true,
    "affiliate_number": 1001,
    "affiliate_name": "Odds Garage",
    "commission_id": "0199b2c9-0a0f-7a51-8c3d-5e2f4a6b7c90",
    "commission_type": "cpa",
    "amount": "25.00",
    "amount_currency": "USD",
    "commission": "40.00",
    "commission_currency": "USD"
  }
}

Keys may come in any order, and the spacing of the JSON is not fixed. Verify the bytes as received, then parse them.

Headers

HeaderValue
RevRake-EventThe event, the same as type in the body.
RevRake-DeliveryThe ID of this delivery to this endpoint, a UUID. The same on every retry, and when a delivery is sent again by hand.
RevRake-TimestampWhen this attempt was sent, in Unix seconds. New on every attempt.
RevRake-SignatureThe lower-case hex HMAC-SHA256, under the endpoint's secret, of <timestamp>.<raw body>. New on every attempt.

The request also carries Content-Type: application/json and the user agent RevRake-Webhook/1.

POST /revrake-webhook HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: RevRake-Webhook/1
RevRake-Event: commission.created
RevRake-Delivery: 0199b2c9-0b7e-7d02-8a41-3c5e6f708192
RevRake-Timestamp: 1791191571
RevRake-Signature: <hex HMAC-SHA256 of "1791191571.<raw body>">

The fields of data

FieldTypeMeaning
affiliate_numberintegerThe affiliate's number.
affiliate_namestringThe affiliate's name.
emailstringThe email address the affiliate signs in with.
promotes_onstringWhat the applicant wrote about where they promote.
trafficarray of stringThe traffic sources the applicant ticked: seo, telegram, paid_social, youtube, email or offline.
reasonstringThe reason given to the affiliate. Always there for a rejection or a suspension; may be empty for an approval.
decided_bystringThe email address of the team member who decided, or who added the affiliate.
invitedbooleantrue when you added and invited the affiliate yourself, without an application. Left out otherwise.
player_idstringYour ID for the player.
occurred_atstringWhen it happened, in RFC 3339 form: the registration, the deposit, the end of the revenue period or the chargeback. For an approval, a rejection or a payment: the moment of the change.
attributedbooleanWhether the player belongs to an affiliate. Always true for a commission.
attribution_reasonstringWhy the player was or was not attributed: one of the attribution reasons.
commission_idstringRevRake's ID of the commission row: the id of the row in GET /v1/commissions. The same in every event about that commission.
commission_typestringThe type of the commission row: cpa, revshare, adjustment or tier, and reversal for a reversal row in commission.paid. In commission.reversed, the type of the CPA that was taken back.
amountstringFor a commission: the deposit or the net revenue behind it, as a decimal, in its own currency. For payout.paid: the amount of the item, as a decimal.
amount_currencystringThe ISO code of the currency of amount.
commissionstringThe commission, as a decimal in the deal's currency (40.00). Negative for a reversal, a downward adjustment and a period the player won.
commission_currencystringThe ISO code of the commission's currency: the deal's.
batch_idstringRevRake's ID of the payout batch, a UUID.
periodstringThe month the batch closes, YYYY-MM.
referencestringThe reference of the batch, the one in the bank file, such as po_2026-09_demo.
itemsintegerHow many items the batch has, whatever their status.
payable_by_currencyobjectPer currency, the sum of the amounts of all the items, as a decimal (1250.00), whatever their status, carried items included.
closed_by_handbooleantrue when a person closed the month, false for the scheduled close.
item_idstringRevRake's ID of the payout item, a UUID.
currencystringThe currency of the item.
messagestringA sentence saying that the test arrived.

Verify a request

  1. Read the raw body as bytes, before any JSON parsing.
  2. Check that RevRake-Timestamp is within 5 minutes of your clock, and refuse the request otherwise, so a recorded request cannot be played again later.
  3. Compute the hex HMAC-SHA256 of the timestamp, a dot and the raw body under the endpoint's secret, and compare it with RevRake-Signature in constant time.
  4. Only then parse the body. If you have already handled its id, answer 2xx and do nothing.
  5. Answer with any 2xx within 5 seconds, and do slow work afterwards.

The same check in Node.js and Python, as the Integration page shows it:

Node.js
const crypto = require("node:crypto");

// raw: the body exactly as received (a Buffer), before parsing it as JSON.
function verify(raw, headers, secret) {
  const ts = headers["revrake-timestamp"] ?? "";
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(ts + ".").update(raw).digest();
  const got = Buffer.from(headers["revrake-signature"] ?? "", "hex");
  return got.length === expected.length && crypto.timingSafeEqual(got, expected);
}
Python
import hashlib, hmac, time

# raw: the body exactly as received (bytes), before parsing it as JSON.
def verify(raw: bytes, headers, secret: str) -> bool:
    ts = headers.get("RevRake-Timestamp", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    expected = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, headers.get("RevRake-Signature", ""))

Delivery, retries and switch-off

  • A delivery is queued in the same transaction as the change it reports, so an event is sent if and only if the change was saved. It is sent within about ten seconds, oldest first.
  • Delivery is at least once: the same event can arrive more than once, for example when your answer is lost on the way. Drop repeats by the body's id.
  • Order is not guaranteed: a retry can arrive after a later event. Order events by created_at or occurred_at, not by the time they arrive.
  • A 2xx answer within 5 seconds is a success. Anything else is a failure and is retried after 1 minute, 5 minutes, 25 minutes, 2 hours, 10 hours and 24 hours, with the same body and a fresh timestamp and signature.
  • After the last retry the delivery is marked as given up. An admin can send it again from the delivery log; it then gets the whole retry schedule again.
  • After 20 failed attempts in a row the endpoint is switched off. Its events keep queuing, and everything that queued is sent when an admin switches it back on. No email is sent when this happens: the state of each endpoint is on the Integration page.
  • An endpoint you switch off yourself gets nothing new: no event is queued for it while it is off.
  • RevRake calls public addresses only: it gives up after 5 seconds and verifies certificates. Loopback, private, link-local and carrier-grade NAT addresses are refused. A redirect is not followed: a 3xx answer is a failure with the error redirect, and the delivery log shows the address it pointed to.

Test it and read the delivery log

  • Press "Send test" on an endpoint: a webhook.test event is sent at once, signed like a real one, and you see the status your endpoint answered and how long it took. A test is not retried, not listed in the delivery log and not counted as a failure.
  • The delivery log of an endpoint shows its latest 100 deliveries: the event, the state (waiting, retrying, given up or delivered), the number of attempts, the last status or error, the first 500 characters of your answer, the next try, and the body as it was sent.
  • A failed attempt is described with the same words as a postback (see why an attempt failed), plus redirect: your endpoint answered with a 3xx. The states are those of a postback delivery.
{
  "id": "0199b2d4-5c60-7b1a-9e3f-1a2b3c4d5e6f",
  "type": "webhook.test",
  "created_at": "2026-10-05T09:30:02.104377Z",
  "data": {
    "message": "A test from RevRake. If you can read this, your endpoint works."
  }
}

Best practice

  • Keep the secret on your server, never in a page or an app. If it leaks, rotate it and move your endpoint to the new one right away: the old secret stops at once.
  • Answer a request whose signature does not match with a 4xx. It is retried, signed with the current secret, so a delivery made while you rotate is not lost.
  • Store the id of every event you have handled, and ignore an event you have seen.
  • Answer first, work later: put the event on a queue of your own and answer 2xx, so a slow database never makes RevRake retry.
  • Enter the final address of your endpoint. A redirect is not followed: the attempt fails with redirect, and the delivery log names the address to use instead.
  • Use commission_id to join the events about one commission, and reconcile with GET /v1/commissions: a nightly read of the last 60 days catches anything that was missed.
  • Tick only the events you use. Each one is a request to your server.

OpenAPI file

The four signed endpoints, with every status and reason as an enumeration, are described in openapi.yaml (OpenAPI 3.1), for code generators and API clients. Its address does not change.

Ready to connect? Create your program and get your key.

You create your program yourself: sign up, confirm your email, and you are in within a few minutes. Free for operators, forever. No card.

Questions? Write to hello@revrake.com