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.
| Endpoint | Meaning |
|---|---|
POST https://app.revrake.com/v1/postback | Report one event, or a batch of events. Signed. |
POST https://app.revrake.com/v1/files | Push a daily REG or SALES file. Signed. |
GET https://app.revrake.com/v1/reports | Read a report, as JSON or CSV. Signed, with an operator or an affiliate key. |
GET https://app.revrake.com/v1/commissions | Read the commission ledger, page by page. Signed, with an operator or an affiliate key. |
GET https://app.revrake.com/v1/affiliates | List the brand's affiliates. Signed, with the operator key. |
GET https://app.revrake.com/v1/links | List tracking links. Signed, with an operator or an affiliate key. |
GET https://app.revrake.com/v1/deals | List 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 with | Whose it is | What 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
403affiliate_inactive. - With an affiliate key, the
affiliateparameter is ignored andby=affiliateis 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:
| Header | Value |
|---|---|
X-RevRake-Key | Your key ID. It starts with pb_ (operator) or ak_ (affiliate). |
X-RevRake-Timestamp | The current Unix time in seconds. A request is accepted within five minutes before or after it. |
X-RevRake-Signature | The 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:
- Build the body and keep the exact bytes you will send.
- Take the current Unix time in seconds, as decimal text.
- Concatenate the timestamp, a dot (
.) and the body. - Compute HMAC-SHA256 of that under your secret and write it as lower-case hex.
- 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-Typeheader is not required. Sendapplication/jsonfor events andtext/csvfor 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.
| Limit | Meaning |
|---|---|
| 120 | Reads per key, per minute: GET /v1/reports and GET /v1/commissions together |
| 30 000 | Event requests per key, per minute: POST /v1/postback, up to 1,000 at once |
| 60 | Files per key, per minute: POST /v1/files, up to 10 at once |
| 4 MB | Body of POST /v1/postback, one event or a batch |
| 32 MB | Body of POST /v1/files |
| 1 – 1000 | Events in one batch |
| 200 000 | Data rows in one file |
| 300 s | Difference between X-RevRake-Timestamp and the real time, in either direction |
| 24 h | How far in the future ts and period may be |
| 100 | Length of player_id, in characters |
| 200 | Length of txn_id, in characters |
| 0 / 2 | Decimals in an amount: those of the currency (two for most, none for currencies such as XOF, UGX or JPY) |
| 1 – 365 | days of a report |
| 10, 25, 50, 100, 500, 1000 | per_page of the ledger: one of these values |
| 1 – 100 000 | page of the ledger |
| 100 | Length of q, in characters; longer text is cut |
| 200 | Length of sub1 to sub5, each UTM tag and each ad click ID on a tracking link, in characters; longer values are cut |
| 10 | Postback destinations per affiliate |
| 2000 | Length of a postback address template, in characters |
| 5 s | Time a tracker has to answer a postback |
| 50 | Inbound log shown in the portal: latest entries of the last 7 days |
| 2000 | Length of a webhook endpoint address, in characters |
| 5 s | Time a webhook endpoint has to answer |
| 20 | Failed webhook attempts in a row before the endpoint is switched off |
| 100 | Deliveries per webhook endpoint shown in the delivery log |
| 2 – 40 | Length of a promo code: letters, digits, hyphens and underscores |
| 32 MB | Size of a file RevRake fetches from your address |
| 60 s | Time your address has to deliver a fetched file |
| 0 – 3600 s | Seconds after the click for the registration-time rule (0 turns it off) |
| 0 – 365 | Days before an open fraud flag decides itself (0 means never) |
| 1 – 200 | Flags 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 / min | Tracking links an affiliate can create: 60 at once, then one a minute. Past that the answer is 429 with Retry-After |
| 2 MB | Size 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(always120) andX-RateLimit-Remaining(how many reads are left right now). - Past the limit the answer is
429rate_limitedwith aRetry-Afterheader: 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 typeis one ofregistration,deposit,chargeback,ngr.- A field that is not listed for the type is refused with
422, so a typo is never silently ignored. tsis 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 (Zor+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"or25.5. No thousands separator, no exponent, and no more decimals than the currency has. currencyis 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_idandbtag: 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"} | Field | Type | Required | Example | Meaning |
|---|---|---|---|---|
player_id | string | yes | "PLR-1001" | Your ID for the player. Up to 100 characters. The key of the event. |
click_id | string | no | "01K6QF3M8Z5T7W9XB2C4D6E8GH" | The cid the player arrived with: 26 characters, letters and digits; upper or lower case. |
btag | string | no | "1001_1" | The btag your platform stored on the player: <affiliate number>_<link number>, digits only. |
promo_code | string | no | "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_hash | string | no | "9f86d081…0f00a08" | SHA-256 of the lower-cased email, in hex. Used only by the self-referral check. |
email_domain | string | no | "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. |
ip | string | no | "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. |
country | string | no | "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. |
ts | string | no | "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} | Field | Type | Required | Example | Meaning |
|---|---|---|---|---|
player_id | string | yes | "PLR-1001" | Your ID for the player. Up to 100 characters. |
txn_id | string | yes | "TX-88213" | Your transaction ID. Up to 200 characters. The key of the event. |
amount | string | number | yes | "25.00" | The deposit, above zero. |
currency | string | no | "KES" | The currency of the deposit. Any currency is accepted. |
ts | string | no | "2026-10-04T08:20:00Z" | When the deposit was made. It decides which deal version and which day's exchange rate apply. |
first | boolean | no | true | true 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"} | Field | Type | Required | Example | Meaning |
|---|---|---|---|---|
txn_id | string | yes | "CB-5001" | Your ID for the chargeback itself. Up to 200 characters. The key of the event. |
ref_txn_id | string | yes | "TX-88213" | The txn_id of the deposit that was charged back. |
player_id | string | no | "PLR-1001" | Optional. If supplied, must match the player of the referenced deposit. |
ts | string | no | "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"} | Field | Type | Required | Example | Meaning |
|---|---|---|---|---|
player_id | string | yes | "PLR-1001" | Your ID for the player. Up to 100 characters. |
period | string | yes | "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. |
amount | string | number | yes | "118.40" | The net gaming revenue of the period. Negative when the player won. |
currency | string | no | "KES" | The currency of the amount. |
ggr | string | number | no | "240.00" | Optional. The gross gaming revenue of the period, in the same currency. Shown to the affiliate; it does not change the commission. |
bonuses | string | number | no | "80.00" | Optional. Bonuses taken off the gross revenue. Shown only. |
fees | string | number | no | "21.60" | Optional. Fees taken off the gross revenue, such as payment or platform fees. Shown only. |
taxes | string | number | no | "20.00" | Optional. Taxes taken off the gross revenue. Shown only. |
ts | string | no | "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.
| Field | Type | Meaning |
|---|---|---|
status | string | accepted, or duplicate when the event was already stored. |
event_type | string | The type you sent. |
event_id | string | RevRake's ID of the stored event, a UUID. The same on every replay. |
attribution | object | For a registration: the decision. |
attribution.attributed | boolean | Whether the player belongs to an affiliate. |
attribution.reason | string | Why. See the table below. |
attribution.affiliate_number | integer | The affiliate. Only when attributed. |
attribution.link_number | integer | The affiliate's link. Only when attributed. |
attribution.click_id | string | The click that was matched, when there is one. |
attribution.policy_version | integer | The version of the brand's attribution policy that decided. |
attribution.window_days | integer | The attribution window of that version, in days. |
attribution.model | string | last_click or first_click: the model of that version. |
commission | object | For a deposit, a chargeback and ngr: what the event did to the ledger. |
commission.effect | string | created, cancelled (a commission on hold was rejected), reversed (a reversal row was added) or none. |
commission.type | string | cpa, revshare, adjustment or reversal. |
commission.amount_minor | integer | The amount in minor units of the deal's currency (4000 is 40.00). Negative for a reversal and for a downward adjustment. |
commission.status | string | hold, approved or rejected. |
commission.reason | string | Why nothing was created, when effect is none. See the table below. |
waiting_for_player | boolean | true for a deposit whose player is not registered yet. It is kept and priced when the registration arrives. |
duplicate_of | string | For 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. |
detail | string | With 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.
reason | Meaning | |
|---|---|---|
click | Attributed | The click ID matched a click inside the window. |
first_click | Attributed | The 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_click | Attributed | The btag matched a link, and a click on that link was found inside the window. |
btag_only | Attributed | The btag matched a link; no click was found inside the window. |
promo_code | Attributed | Neither the click ID nor the btag attributed the player, and the promo code matched a code of an approved affiliate. |
no_tag | Not attributed | The registration carried neither a click ID nor a btag. |
unknown_click | Not attributed | The click ID is not one of yours, and no btag was sent. |
unknown_btag | Not attributed | The btag is malformed or matches no link. |
outside_window | Not attributed | The click is older than the attribution window, or later than the registration. |
affiliate_inactive | Not attributed | The affiliate is not approved, or was not when the click was made. |
unknown_promo_code | Not attributed | Only a promo code was sent, and it matches none of your codes. |
promo_code_retired | Not attributed | Only a promo code was sent, and it was retired. |
Why no commission was created
commission.reason | Meaning |
|---|---|
unattributed | The player belongs to no affiliate. |
not_first | Not the player's first deposit. |
already_earned | The player's CPA was already earned. A second "first" deposit pays nothing and raises a flag. |
no_deal_version | The affiliate has no deal, or the deal had no version at the time of the event. |
country_blocked | The player is from a country the deal does not pay for. The player stays recorded and attributed. |
no_cpa_in_deal | The deal pays no CPA. |
below_threshold | The first-deposit or cumulative net-deposit amount is below the deal’s minimum, according to its CPA baseline. |
no_commission | For a chargeback: the deposit had earned nothing. |
no_revshare_in_deal | For ngr: the deal pays no revenue share. |
fx_rate_missing | For ngr: there is no exchange rate for the period yet. The report is stored and priced when a rate exists. |
unchanged | For 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
} | Field | Type | Meaning |
|---|---|---|
results | array | One entry per event, in the order sent. |
results[].index | integer | The position of the event in the array, from 0. |
results[].status | string | accepted, duplicate or rejected. |
results[].event_type | string | As in a single answer. Not for a rejected event. |
results[].event_id | string | As in a single answer. Not for a rejected event. |
results[].attribution | object | As in a single answer. |
results[].commission | object | As in a single answer. |
results[].waiting_for_player | boolean | As in a single answer. |
results[].field | string | For a rejected event: the field to fix. |
results[].message | string | For a rejected event: what to fix. |
accepted | integer | How many events were stored. |
duplicate | integer | How many were already stored. |
rejected | integer | How 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
| Column | Required | Meaning |
|---|---|---|
registration_date | yes | When the player registered. Becomes ts. |
player_id | yes | Your ID for the player. |
btag | yes | The btag stored on the player. May be empty: the player is then not attributed, unless a promo code attributes them. |
promo_code | no | Optional. The promo code the player typed at sign-up, used as the promo_code of a registration. |
player_country | no | Optional. 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
| Column | Required | Meaning |
|---|---|---|
activitydate | yes | The activity day, determined in the workspace’s file timezone. |
player_id | yes | Your ID for the player. |
currency | yes | The currency of the amounts in the row. Empty: the workspace's reporting currency. |
deposits | yes | The day’s gross deposits, including the first deposit that occurred that day. Not a lifetime total; do not subtract chargebacks. |
first_deposit_amount | yes | The 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. |
chargeback | yes | Total 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_revenue | no | Every 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_revenue | no | Optional. Columns named gross_revenue or ending in _gross_revenue, summed into the ggr of the day's ngr. Shown to the affiliate only. |
*_bonus | no | Optional. Columns named bonus or ending in _bonus, summed into bonuses. Shown only. |
*_fees | no | Optional. Columns named fees or ending in _fees, summed into fees. Shown only. |
*_taxes | no | Optional. 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_amountandchargebackare 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 underchargeback_fx_missinguntil 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 withref_txn_idset 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."]
} | Field | Type | Meaning |
|---|---|---|
id | string | RevRake's ID of this import. |
filename | string | The name you gave, or reg.csv / sales.csv. |
kind | string | reg or sales. |
uploaded_at | string | When it was received. |
rows_total | integer | Data rows in the file. |
accepted | integer | Events stored. |
duplicates | integer | Events that were already stored. |
rejected | integer | Rows or events refused. Each is in the inbound log with the line itself. |
skipped | integer | Rows with nothing to record. |
problems | array of string | The 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 | Parameter | Type | Example | Meaning |
|---|---|---|---|
by | string | link | How rows are grouped: day, link, sub1, country or affiliate. Default day. With affiliate set, by=affiliate is answered as day. |
days | integer | 7 | How many days, counting today: 1 to 365. Default 30. |
from | string | 2026-09-01 | The first day of any period, YYYY-MM-DD in the workspace's timezone. With from or to, days is ignored. At most 366 days. |
to | string | 2026-09-30 | The last day of the period, included. Default today. |
affiliate | integer | 1001 | An affiliate number. Narrows the report to that affiliate. Operator key only; ignored with an affiliate key, which always reads its own affiliate. |
tag | string | Influencers | A 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. |
format | string | csv | csv 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
}
} | Field | Type | Meaning |
|---|---|---|
by | string | The grouping that was used. |
from | string | The first day of the report, YYYY-MM-DD. |
to | string | The last day: today in the workspace's timezone, or the to you gave. |
currency | string | The workspace's reporting currency. Earnings are in it. |
rows | array | One entry per group. |
rows[].key | string | What identifies the group: the day, RevRake's ID of the link or the affiliate, the sub1 value, or the two-letter country. |
rows[].label | string | The 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[].clicks | integer | Clean clicks. |
rows[].flagged_clicks | integer | Clicks flagged as bots, repeats, bursts or of an affiliate who is not approved. Not in clicks. |
rows[].registrations | integer | Players attributed. |
rows[].ftds | integer | First deposits. |
rows[].earnings_minor | integer | Commissions earned, in minor units of the reporting currency, at the rate of the day each was earned. |
total | object | The 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 | Parameter | Type | Example | Meaning |
|---|---|---|---|
status | string | approved | hold, approved, paid or rejected. Default: all. |
from | string | 2026-09-01 | Only rows that occurred at or after this time: RFC 3339, or a day YYYY-MM-DD in UTC. |
to | string | 2026-10-01 | Only rows that occurred before this time. Same forms. |
affiliate | integer | 1001 | An affiliate number. Narrows the rows, totals and owed to that affiliate. Operator key only; ignored with an affiliate key. |
tag | string | Influencers | A 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. |
q | string | PLR-1001 | Rows whose player ID or affiliate name contains this text. Up to 100 characters. |
sort | string | when | when, affiliate, player, status, deposit or commission. Default when. |
dir | string | asc | asc or desc. Default desc. |
page | integer | 2 | The page, from 1. Default 1. |
per_page | integer | 500 | 10, 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
} | Field | Type | Meaning |
|---|---|---|
currency | string | The workspace's reporting currency. |
totals | object | The 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. |
owed | object | The sum per currency and status, in each commission's own currency: what is owed. Over the same rows as totals. |
period | boolean | true when from or to was given, so totals and owed cover that period only. |
commissions | array | The rows of this page. |
total | integer | How many rows match the filters. |
page | integer | The page that was answered. |
per_page | integer | Rows per page that was used. |
A commission row
| Field | Type | Meaning |
|---|---|---|
id | string | RevRake's ID of the row, a UUID. It never changes: update by it. |
occurred_at | string | When the commission was earned: the time of the deposit, the end of the revenue period, or the time of the chargeback. |
type | string | cpa, revshare, adjustment, reversal or tier. |
amount_minor | integer | The commission, in minor units of currency. Negative for a reversal and for a downward adjustment. |
currency | string | The currency of the commission: the deal's. |
reporting_minor | integer | null | The commission in the workspace's reporting currency, at the rate of the day it was earned. null while that rate is missing. |
gross_minor | integer | The deposit or the net revenue behind the commission, in minor units of gross_currency. |
gross_currency | string | The currency of the deposit or the revenue. |
rate_snapshot | integer | The terms that were applied: the CPA amount in minor units, or the revenue share in hundredths of a percent (2500 is 25%). |
status | string | hold, approved, paid or rejected. See commission statuses. |
status_reason | string | Why a row is still on hold or was rejected. Empty otherwise. Every value is in the table of reasons. |
status_note | string | The operator's note on a commission taken back, as the affiliate reads it. Empty otherwise. |
hold_until | string | null | When the hold period ends. null for a reversal. |
affiliate_number | integer | The affiliate's number. |
affiliate_name | string | The affiliate's name. |
player_ref | string | Your ID for the player. |
deal_name | string | The deal. |
deal_version | integer | The 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
| Field | Type | Meaning |
|---|---|---|
number | integer | The affiliate number, the first part of the btag; in a link, the link number, its second part. |
name | string | The affiliate's name. |
email | string | The affiliate's email address. |
status | string | pending, approved, rejected or suspended. |
deal | string | The name of the deal the affiliate is on; empty before approval. |
created_at | string | When it was created, RFC 3339. |
decided_at | string | null | When the application was approved or rejected, or the account suspended; null before. |
Each link
| Field | Type | Meaning |
|---|---|---|
affiliate_number | integer | The number of the affiliate the link belongs to. |
number | integer | The affiliate number, the first part of the btag; in a link, the link number, its second part. |
code | string | The code in the link's address. |
url | string | The tracking link, on the brand's verified tracking domain or the shared one. |
btag | string | <affiliate_number>_<number>. |
campaign | string | The link's campaign label. |
landing_path | string | null | The page on your site the link lands on; null for the brand's default. |
subs | string[5] | sub1 to sub5 fixed on the link; empty strings when unset. |
promo_code | string | null | The promo code, on a link made for one; else null. |
vanity_url | string | null | The short address /go/<name>, when the affiliate set one; else null. |
created_at | string | When it was created, RFC 3339. |
Each deal
| Field | Type | Meaning |
|---|---|---|
name | string | The affiliate's name. |
currency | string | The currency the deal earns and pays in. |
listed | boolean | Whether the public join page shows the deal. |
default | boolean | Whether new affiliates get this deal. |
version | integer | The version of the deal in effect now. |
effective_at | string | When that version took effect, RFC 3339. |
terms | object | The 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.
| Field | Type | Meaning |
|---|---|---|
click_id | string | The click ID, as passed to your landing page and in postbacks. |
clicked_at | string | When the click happened. |
affiliate_number | integer | The affiliate number. |
link_number | integer | The link number. |
btag | string | <affiliate_number>_<link_number>. |
country | string | Two-letter country code; empty when unknown. |
device | string | desktop, mobile, tablet or bot, from the browser. |
os | string | The operating system, from the browser. |
browser | string | The browser. |
sub1 … sub5 | string | sub1 to sub5: the sub-IDs of the click. |
utm_source … utm_content | string | utm_source to utm_content: the UTM tags of the click. |
gclid, fbclid, ttclid, msclkid, wbraid | string | gclid, fbclid, ttclid, msclkid, wbraid: ad-platform click IDs on the landing URL. |
referer | string | The page the visitor came from. |
flags | string | Why 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.
| Field | Type | Meaning |
|---|---|---|
player_id | string | Your player ID. |
registered_at | string | The registration time you reported. |
arrived_at | string | When RevRake recorded it. |
country | string | Two-letter country code; empty when unknown. |
attributed | boolean | true when an affiliate is credited. |
attribution_reason | string | Why, as in the postback answer: click, btag_only, promo_code, no_tag… |
affiliate_number | integer | The affiliate number. |
link_number | integer | The link number. |
btag | string | <affiliate_number>_<link_number>. |
click_id | string | The click that credited the player; empty without one. |
policy_version | integer | The attribution policy version that decided it. |
events.csv.gz: every accepted brand event that arrived that day, including daily SALES summaries, with its payload.
| Field | Type | Meaning |
|---|---|---|
event_id | string | RevRake's ID of the event. |
type | string | registration, deposit, chargeback, daily_sales or ngr; for a commission, cpa, revshare, tier, adjustment or reversal. |
external_id | string | The event's key: the player ID, the transaction ID, or player and period. |
player_id | string | Your player ID. |
occurred_at | string | When it happened, as you reported it; for a commission, when it was earned. |
received_at | string | When RevRake accepted the event. |
source | string | s2s (the API) or file (a daily file). |
payload | JSON | The event as RevRake read it, JSON. |
result | JSON | What RevRake answered, JSON. |
commissions.csv.gz: commission rows written that day.
| Field | Type | Meaning |
|---|---|---|
commission_id | string | The row's ID, the id in GET /v1/commissions. |
created_at | string | When the row was written. |
occurred_at | string | When it happened, as you reported it; for a commission, when it was earned. |
type | string | registration, deposit, chargeback, daily_sales or ngr; for a commission, cpa, revshare, tier, adjustment or reversal. |
status | string | hold, approved, paid or rejected, at the export. |
status_reason | string | Why it is held or rejected; empty otherwise. |
affiliate_number | integer | The affiliate number. |
player_id | string | Your player ID. |
deal | string | The deal's name. |
deal_version | integer | The deal version that priced it. |
gross | decimal | The deposit or net revenue behind it. |
gross_currency | string | Its currency. |
amount | decimal | The commission. |
currency | string | The deal's currency. |
reporting_amount | decimal | The commission in the reporting currency; empty while the exchange rate is missing. |
reporting_currency | string | The workspace's reporting currency. |
reverses_id | string | For a reversal, the row it reverses. |
payout_item_id | string | The 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.
| Event | Key | The same key again |
|---|---|---|
registration | player_id | The first answer. The attribution is not decided again, even if the tags differ. |
deposit | txn_id | The 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. |
chargeback | txn_id | The first answer. A second chargeback of the same deposit under a new txn_id is refused. |
ngr | player_id and period | The same amount and currency: the first answer. A different amount: a correction, stored as a new revision, with the difference as an adjustment. |
| CSV | REG row: player_id | As a registration. |
| CSV | SALES money summary: brand, player and activity day | Identical totals replay. Changed deposits, first-deposit amount, chargebacks, currency or day boundaries conflict, even for a previously zero summary. |
| CSV | SALES row, revenue: the player and the day | As 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
| Status | error | Endpoint | When | What 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. | |
| 400 | bad_request | POST /v1/postback | The body starts as an array but is not a JSON array of events. | Fix the request. The same request again gets the same answer. |
| 401 | unknown_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. |
| 401 | bad_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. |
| 401 | bad_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. |
| 403 | affiliate_inactive | GET /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. |
| 404 | unknown_affiliate | GET /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. |
| 413 | too_large | POST /v1/* | The body is larger than the limit of the endpoint. | Fix the request. The same request again gets the same answer. |
| 422 | invalid | POST /v1/*, GET /v1/reports | The event or the file cannot be used as it is. | Fix the request. The same request again gets the same answer. |
| 429 | rate_limited | GET /v1/* | More reads than the key's allowance. The answer carries Retry-After. | Wait the seconds in Retry-After, then send it again. |
| 500 | internal | /v1/* | Something went wrong on our side. | Send it again. |
- A batch with no events or more than 1,000 is refused with
422invalidand nofield. - A
401for a bad timestamp or signature with an operator key is listed in the inbound log asbad_signature. A request with an unknown key, or with an affiliate key, is not logged. - A read answers
422only for a report period that cannot be used (fromandto). Any other parameter with a value that is not allowed is ignored and its default is used. Check theby,pageandper_pagein the answer. - Retry on a timeout, on
429and on500. Do not retry any other4xxunchanged.
What field names in a 422
field | When |
|---|---|
body | The body is not valid JSON, or carries an unknown or malformed field (for example a ts that is not RFC 3339). |
type | type is missing or not one of the four. |
player_id | Missing or longer than 100 characters; a placeholder that was not filled in; for ngr, a player who is not registered. |
click_id | A placeholder that was not filled in. The message starts with misconfigured_macro. |
btag | A placeholder that was not filled in. The message starts with misconfigured_macro. |
promo_code | A placeholder that was not filled in. The message starts with misconfigured_macro. |
txn_id | Missing or longer than 200 characters. |
ref_txn_id | Missing; no deposit with that txn_id is known; or the deposit was already charged back. |
amount | Not a decimal the currency allows; for a deposit, not above zero. |
currency | Not a three-letter code. |
ggr | For ngr: not a decimal amount in the currency. |
bonuses | For ngr: not a decimal amount in the currency. |
fees | For ngr: not a decimal amount in the currency. |
taxes | For ngr: not a decimal amount in the currency. |
ts | More than a day in the future. |
period | Not a day or a month, or in the future. |
from | For GET /v1/reports: from or to is not YYYY-MM-DD, from is after to, or they are more than 366 days apart. |
file | For /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. |
chargeback | In 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.
status | Meaning |
|---|---|
accepted | Stored now. The answer says what it did. |
duplicate | Already 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. |
rejected | Only 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.effect | Meaning |
|---|---|
created | A commission row was created: a CPA, a revenue share, or an adjustment for a corrected figure. |
cancelled | A chargeback rejected a commission that was still on hold. |
reversed | A chargeback added a reversal row against a commission already approved or paid. |
none | No row was created. reason says why. |
Commission types
type | Meaning |
|---|---|
cpa | One per player when the deal’s minimum is met by the first deposit or cumulative deposits net of chargebacks, according to its CPA baseline. |
revshare | A share of a player's net gaming revenue for a period. Negative for a period the player won. |
adjustment | The difference a corrected revenue figure makes. |
reversal | The 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. |
tier | A 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:
status | Meaning |
|---|---|
hold | Waiting: for the hold period of the deal to end, or for what status_reason names. |
approved | The 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. |
paid | The payout item that contains the row was marked as paid. |
rejected | It 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_reason | With status | Meaning |
|---|---|---|
fx_rate_missing | hold | A 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_missing | hold, approved, paid | A 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_missing | hold | A 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_referral | hold | An open fraud flag: the player looks like the affiliate themselves. Waits for the operator to release or confirm the flag. |
click_velocity | hold | An 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_conversion | hold | An open fraud flag: a second "first" deposit was reported for the player. Waits for the operator. |
paid_traffic | hold | An open fraud flag: the click that brought the player carried an ad network's click ID. Waits for the operator. |
disposable_email | hold | An open fraud flag: the player signed up with a throwaway email domain. Waits for the operator. |
geo_mismatch | hold | An open fraud flag: the player registered in another country than the one of the click. Waits for the operator. |
fast_registration | hold | An open fraud flag: the player registered within the set number of seconds after the click. Waits for the operator. |
affiliate_pending | hold | The 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_rejected | hold | The affiliate's application was rejected. |
affiliate_suspended | hold | The affiliate was suspended when the commission was earned. When the affiliate is reinstated, the row waits only for its normal hold period. |
chargeback | rejected, approved | On a rejected row: a chargeback cancelled it while on hold. On a reversal row: it reverses a commission after a chargeback. |
below_threshold | rejected | Once rates were available, the qualifying first-deposit or cumulative net-deposit amount was below the deal’s minimum. |
fraud | rejected, approved | The operator confirmed the fraud flag that held the row, or took the commission back for fraud. |
base_rejected | rejected | A tier top-up whose commission was rejected while both were on hold. |
self_excluded | rejected, approved | The 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_account | rejected, approved | The operator took the commission back: the player has another account. |
bonus_abuse | rejected, approved | The operator took the commission back for bonus abuse. |
terms_breach | rejected, approved | The operator took the commission back for a breach of the program's terms. |
test_account | rejected, approved | The operator took the commission back: the player was a test account. |
other | rejected, approved | The 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 status | Meaning |
|---|---|
draft | The month was closed. The operator reviews the items. |
approved | Locked. With invoicing on, each payable item gets an invoice number. The bank file (CSV) can be downloaded and the items paid. |
paid | Every payable item was marked as paid. |
| Item status | Meaning |
|---|---|
payable | To be paid in this batch. |
carried | Below the minimum payout, or not above zero: carried forward into the next batch. |
held | The 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_details | The affiliate has not entered where to be paid. Not in the bank file. |
written_off | The 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. |
paid | Marked 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_dateorplayer_idis missing or cannot be read. - SALES:
activitydateorplayer_idis 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
| Status | Meaning |
|---|---|
accepted | Stored, with what it did. |
duplicate | Already stored; the first answer was returned. |
rejected | Refused; the detail says which field and why. |
bad_signature | The timestamp or the signature was wrong. |
test | A test event from the Integration page. It runs through the real path and stores nothing. |
States of a postback delivery
| Status | Meaning |
|---|---|
queued | Waiting for its first try, or for the next retry. The portal shows it as waiting or retrying. |
delivered | The tracker answered with a 2xx status. |
dead | Every retry failed. The portal shows it as given up; the affiliate can send it again by hand. |
Statuses of an affiliate
| Status | Meaning |
|---|---|
pending | Applied; the operator has not decided. |
approved | Active in the program. |
rejected | The application was declined. |
suspended | Stopped 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
| Parameter | Meaning |
|---|---|
sub1 … sub5 | The 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_content | Stored 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 wbraid | Ad-network click IDs. Stored with the click; available as postback macros. Not passed on. |
Parameters added to the landing URL
| Parameter | Meaning |
|---|---|
cid | The 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_content | The 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 andcidis not added. - A click on the link of an affiliate who is not approved is stored and flagged, and lands without
cidandbtag: it cannot be attributed. - Bots, link previews,
HEADrequests, 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
404and 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 |
|---|---|---|
registration | lead | A player was attributed to the affiliate. |
ftd | sale | A first deposit earned the affiliate a CPA commission. |
commission_approved | approved | A commission was approved when its hold period ended. |
commission_rejected | rejected | A commission on hold was rejected after a review. |
commission_paid | paid | A commission was paid in a payout batch. |
chargeback | rejected | A 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.
| Event | Keitaro | Binom | RedTrack | BeMob |
|---|---|---|---|---|
registration | lead | lead | other | new |
ftd | sale | sale | pending | approved |
commission_approved | sale | sale | approved | approved |
commission_rejected | rejected | reject | declined | declined |
commission_paid | sale | sale | approved | approved |
chargeback | rejected | reject | declined | chargeback |
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.
| Macro | Meaning |
|---|---|
{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.
| Keitaro | https://YOUR-TRACKER-DOMAIN/POSTBACK-KEY/postback?subid={sub1}&status={status}&payout={commission}¤cy={currency}&tid={conversion_id} |
| Binom | https://YOUR-TRACKER-DOMAIN/click?cnv_id={sub1}&payout={commission}&cnv_status={status}&cnv_currency={currency} |
| Voluum | https://YOUR-TRACKER-DOMAIN/postback?cid={sub1}&payout={commission}¤cy={currency}&txid={event_id}&et={event} |
| RedTrack | https://YOUR-TRACKER-DOMAIN/postback?clickid={sub1}&sum={commission}&status={status}&type={event}&rdtk_event_id={event_id} |
| BeMob | https://YOUR-TRACKER-DOMAIN/postback?cid={sub1}&payout={commission}&txid={conversion_id}&status={status} |
| PeerClick | https://YOUR-TRACKER-DOMAIN/postback?cid={sub1}&payout={commission}&txid={conversion_id}&status={status}¤cy={currency} |
| FunnelFlux Pro | https://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}¤cy={currency}&tid={conversion_id}&eid={event_id} GET https://tracker.example/postback?click_id=TRACKER_CLICK_ID&event=ftd&status=sale&payout=40.00¤cy=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
GETwith the user agentRevRake-Postback/1. It follows at most three redirects and gives up after 5 seconds. Certificates are verified. - A
2xxanswer 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://orhttps://address of at most 2,000 characters.
Why an attempt failed
timeout | No answer in 5 seconds. |
dns | The host name was not found. |
refused | The connection was refused. |
blocked_address | The address is private or local. |
tls | A certificate problem. |
too_many_redirects | More than three redirects. |
http_error | The tracker answered with a status other than 2xx. |
other | The 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.
| Event | Fields of data | When |
|---|---|---|
affiliate.applied | affiliate_number affiliate_name email promotes_on traffic | Someone 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.approved | affiliate_number affiliate_name email reason decided_by invited | You 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.rejected | affiliate_number affiliate_name email reason decided_by | You rejected an application. |
affiliate.suspended | affiliate_number affiliate_name email reason decided_by | You suspended an approved affiliate. |
player.registered | player_id occurred_at attributed affiliate_number affiliate_name attribution_reason | A player was registered, by a signed event or a row of a REG file, attributed or not. Once per player. |
commission.created | player_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currency | A 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.approved | player_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currency | A commission on hold was approved because its hold period ended, tier top-ups included. A row approved from the start sends none. |
commission.rejected | player_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currency | A 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.paid | player_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currency | A commission row was paid: one event per row of a payout item marked as paid, reversal and tier rows included. |
commission.reversed | player_id occurred_at attributed affiliate_number affiliate_name commission_id commission_type amount amount_currency commission commission_currency | A 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_closed | batch_id period reference items payable_by_currency closed_by_hand | A month was closed into a draft payout batch, by a person or by the scheduled close. |
payout.batch_approved | batch_id | A draft payout batch was approved and locked. |
payout.paid | batch_id item_id affiliate_number affiliate_name currency amount | An item of an approved batch was marked as paid. A batch whose items are all paid sends no event of its own. |
tracking.health_alert | brand kind since quiet_hours clicks_at_least | A 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_cleared | brand raised_at cleared_at | The first registration after a tracking health alert arrived, and the alert is cleared. |
flag.raised | kind detail affiliate_number affiliate_name | A 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_off | destination failures affiliate_number affiliate_name | An affiliate's postback destination was switched off after the failed deliveries in a row that the retry ladder allows. |
file.late | source date | A daily file fetched on a schedule had not arrived three hours after its fetch time. Once per file and day. |
webhook.test | message | Only 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:
| Field | Type | Meaning |
|---|---|---|
id | string | The ID of the event, a UUID. The same for every endpoint that receives the event and on every retry: use it to drop repeats. |
type | string | The event, as in the table above. |
created_at | string | When the event was queued, in RFC 3339 form in UTC, with fractions of a second. |
data | object | The 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
| Header | Value |
|---|---|
RevRake-Event | The event, the same as type in the body. |
RevRake-Delivery | The ID of this delivery to this endpoint, a UUID. The same on every retry, and when a delivery is sent again by hand. |
RevRake-Timestamp | When this attempt was sent, in Unix seconds. New on every attempt. |
RevRake-Signature | The 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
| Field | Type | Meaning |
|---|---|---|
affiliate_number | integer | The affiliate's number. |
affiliate_name | string | The affiliate's name. |
email | string | The email address the affiliate signs in with. |
promotes_on | string | What the applicant wrote about where they promote. |
traffic | array of string | The traffic sources the applicant ticked: seo, telegram, paid_social, youtube, email or offline. |
reason | string | The reason given to the affiliate. Always there for a rejection or a suspension; may be empty for an approval. |
decided_by | string | The email address of the team member who decided, or who added the affiliate. |
invited | boolean | true when you added and invited the affiliate yourself, without an application. Left out otherwise. |
player_id | string | Your ID for the player. |
occurred_at | string | When 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. |
attributed | boolean | Whether the player belongs to an affiliate. Always true for a commission. |
attribution_reason | string | Why the player was or was not attributed: one of the attribution reasons. |
commission_id | string | RevRake's ID of the commission row: the id of the row in GET /v1/commissions. The same in every event about that commission. |
commission_type | string | The 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. |
amount | string | For 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_currency | string | The ISO code of the currency of amount. |
commission | string | The 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_currency | string | The ISO code of the commission's currency: the deal's. |
batch_id | string | RevRake's ID of the payout batch, a UUID. |
period | string | The month the batch closes, YYYY-MM. |
reference | string | The reference of the batch, the one in the bank file, such as po_2026-09_demo. |
items | integer | How many items the batch has, whatever their status. |
payable_by_currency | object | Per currency, the sum of the amounts of all the items, as a decimal (1250.00), whatever their status, carried items included. |
closed_by_hand | boolean | true when a person closed the month, false for the scheduled close. |
item_id | string | RevRake's ID of the payout item, a UUID. |
currency | string | The currency of the item. |
message | string | A sentence saying that the test arrived. |
Verify a request
- Read the raw body as bytes, before any JSON parsing.
- Check that
RevRake-Timestampis within 5 minutes of your clock, and refuse the request otherwise, so a recorded request cannot be played again later. - Compute the hex HMAC-SHA256 of the timestamp, a dot and the raw body under the endpoint's secret, and compare it with
RevRake-Signaturein constant time. - Only then parse the body. If you have already handled its
id, answer2xxand do nothing. - Answer with any
2xxwithin 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_atoroccurred_at, not by the time they arrive. - A
2xxanswer 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
3xxanswer is a failure with the errorredirect, 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.testevent 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 a3xx. 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
idof 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_idto join the events about one commission, and reconcile withGET /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