Skip to content

RevoSurge Server Events API Deprecated ​

Deprecated on 18 October 2026

Server Events API v2 will be deprecated on 18 October 2026. Use Server Events API (v3) and see Migrating from v2.

Audience: Engineers, technical integrators, Admin teams

Overview ​

The Server Events API allows partners to securely send user events and user activities directly from their servers to the DataPulse platform.

This API is designed for high-throughput scenarios and requires strict authentication via API Keys.

Base URL ​

EnvironmentBase URL
Productionhttps://datapulse-api.revosurge.com/

Authentication ​

All requests to the Server Events API must include an API Key in the request header X-API-Key.

Required Headers ​

Header NameDescription
X-API-KeyAPI Key for authentication

Ingestion Endpoints ​

1. Single Ingest Event ​

PathMethodContent-Type
/v2/s2s/eventPOSTapplication/json

1.1 Request Body Schema ​

The request body accepts a JSON object with the following fields:

FieldTypeRequiredDescription
client_user_idStringYesUnique identifier of the user in your system.
click_idStringYesUnique ID for the ad click.
Suggested.
event_nameStringYesName of the event (e.g., "register", "login", "deposit").
timestampIntegerYesUnix timestamp (in seconds) when the event occurred.
ip_addressStringYesIP address of the user (IPv4/IPv6).
user_agentStringNoUser agent string of the browser or device.
game_typeStringNoType of the game (e.g., "slot", "casino").
game_providerStringNoProvider of the game (e.g., "EA", "GGP").
transaction_idStringYes*Unique ID for the transaction (e.g., purchase ID).
amountFloatYes*Monetary value of the transaction (e.g., deposit amount).
currencyStringYes*3-letter ISO currency code (e.g., USD, EUR) for fiat currency, or any specific code for crypto.
is_cryptoBooleanNoSet to true if the transaction is crypto-based, otherwise false.
<<any_prop>>StringNoAdditional custom properties as key-value pairs.

PS: The transaction_id, amount, currency is not required in non-financial events (eg. login).

1.2 Example Request (curl) ​

bash
curl -X POST "https://<<our-url>>/v2/s2s/event" 
    -H "Content-Type: application/json" 
    -H "X-API-KEY: dp_test_key_123"
    -d '{
        "client_user_id": "user_001", 
        "click_id": "clk_998877",
        "ip_address": "203.0.113.1", 
        "event_name":"deposit",      
        "transaction_id": "tx_554433",   
        "timestamp": 1702963200,   
        "amount": 50.00,          
        "currency": "USD"  
        }'

2. Batch Ingest Event ​

PathMethodContent-Type
/v2/s2s/batchPOSTapplication/json

2.1 Request Body Schema ​

The request body accepts a JSON object of array type. The array item is referenced as defined in section 1.1.

Responses ​

Status CodeDescription
200 OKEvent successfully queued for processing
400 Bad RequestMissing required fields
401 UnauthorizedInvalid API Key or missing auth headers
429 Too Many RequestsRate limit exceeded
500 Server ErrorInternal processing error. Please retry with backoff

Rate Limit ​

  • Limits are applied per X-API-KEY.
  • Standard Limit: 60 requests per minute (Default).
  • If you exceed the limit, you will receive a 429 response.
  • Retry Strategy: We recommend implementing an exponential backoff strategy when encountering 429 or 500 errors.

Dry run ​

Add ?dryrun=1 to either ingestion endpoint to check a request end to end without creating any data. We parse, authenticate, validate and enrich it exactly as we would in production, echo back the record we would have written, and then store nothing.

EndpointDry-run URL
SinglePOST /v2/s2s/event?dryrun=1
BatchPOST /v2/s2s/batch?dryrun=1

dryrun on its own and dryrun=true mean the same thing. dryrun=0, dryrun=false and dryrun=no turn it off.

Telling a dry run apart from a real write ​

CallStatus CodeBodyResponse Header
Real write200 OK{ "status": "success" }—
Dry run200 OKThe echoed recordX-Ingest-Mode: dryrun

WARNING

Both answer 200, so the status code alone does not tell you which one you got — read the body and the X-Ingest-Mode: dryrun response header. A dry run that passes is not a write. Nothing sent with dryrun=1 is ever stored.

A dry run is authenticated as usual and counts against your rate limit as usual — one batch request counts as one request. It is not a load-testing entry point.

The echoed record ​

bash
curl -X POST "https://<<our-url>>/v2/s2s/event?dryrun=1" \
    -H "Content-Type: application/json" \
    -H "X-API-KEY: dp_test_key_123" \
    -d '{
        "client_user_id": "u-ua-1",
        "click_id": "rs-ua-1",
        "event_name": "register",
        "timestamp": 1702963200,
        "ip_address": "203.0.113.1",
        "user_agent": "Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36"
        }'

The response is the record we would have stored:

{
  "requestId": "s2s.event-...",
  "apiKey": "dp_test_key_...",
  "clickId": "rs-ua-1",
  "country": "USA",
  "userAgentInfo": {
    "device_class": "Mobile",
    "device_name": "Samsung SM-G991B",
    "operating_system_name": "Android",
    "operating_system_version": "13",
    "agent_name": "Android",
    "agent_class": "Browser"
  },
  "eventData": {
    "client_user_id": "u-ua-1",
    "click_id": "rs-ua-1",
    "event_name": "register",
    "user_agent": "Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36",
    "recv_timestamp": 1789541015965,
    "request_id": "..."
  },
  "misspelledFields": []
}

What you send vs. what we record ​

Most of the difference between your payload and the echo is enrichment we add:

You sendWe record
ip_addressThe country and city resolved from that IP
user_agentuserAgentInfo — device class and name, OS name and version, agent name and class
amount + currencyThe amount converted with our published exchange rates
game_typeThe normalized game type
transaction_id, when you omit itA transaction_id we generate
—recv_timestamp — the time we received the event, in milliseconds
—request_id / requestId — the trace id for this call. Quote it when you ask us about it

The next two sections are the other side of that table — what we did not record the way you meant it.

The two spellings ​

WARNING

Never copy a field name out of the echo into your request. What you send is snake_case (client_user_id, click_id, ip_address, event_name, user_agent). The top level of the echo is camelCase (requestId, clickId, userAgentInfo), and the eventData nested inside it is snake_case again.

What a misspelled key costs you depends on whether the field is required — and the damage runs opposite to the noise:

  • A misspelled required field returns 400. Loud, and fixed in minutes.
  • A misspelled optional field returns 200, the event is stored, and the field silently disappears. Send userAgent instead of user_agent and userAgentInfo comes back empty — the whole device and operating-system dimension is gone, with no error anywhere.
  • click_id belongs to the second group. clickId gets you a 200, a stored event and something that looks entirely normal — except that conversion is never attributed.

misspelledFields ​

Every dry-run echo carries a misspelledFields list. It names the keys you sent that are recognizably ours but spelled differently — which is what makes the silent failure above visible, before you go live:

"misspelledFields": [{ "sent": "userAgent", "expected": "user_agent" }]

sent is the key exactly as you sent it; expected is the field we would have matched it to. Change your payload to use expected.

An empty array is the answer you want. A clean single-event dry run returns "misspelledFields": []. The key is always there, so [] means we ran the check and found nothing — not this endpoint has no such check. (Batch reports this differently — see below.)

We do not reject unknown fields ​

Custom properties are a supported feature, not a mistake. A key we do not recognize is kept verbatim in the event's properties — that is exactly what the <<any_prop>> row in section 1.1 is for.

So this check is deliberately narrow: it reports only keys that look like ours, spelled differently. Names are compared after folding to lower case and dropping separators, and a key is reported only if it then collides with a field we know.

You sendReported?Why
userAgent, User-AgentYesCollides with user_agent
CLIENTUSERIDYesCollides with client_user_id
user_agentNoSpelled correctly
bonus_round_id, vip_tier, table_idNoLegitimate custom properties — they collide with nothing of ours

IMPORTANT

Your own custom properties not showing up in this list is the correct outcome, not a sign that they were ignored. They are recorded exactly as you sent them. Do not delete a custom property you rely on because a dry run stayed quiet about it.

Batch dry run ​

POST /v2/s2s/batch?dryrun=1 answers synchronously with a per-item result:

{
  "requestId": "s2s.batch-...",
  "mode": "dryrun",
  "total": 3,
  "successCount": 2,
  "failureCount": 1,
  "successEvents": ["... the enriched events ..."],
  "errors": [
    {
      "index": 2,
      "violations": [
        { "field": "eventName", "rule": "required", "event": "deposit" },
        { "field": "clientUserId", "rule": "required", "event": "deposit" }
      ]
    }
  ],
  "misspelledFields": { "1": [{ "sent": "clickId", "expected": "click_id" }] }
}
  • errors[].index is the position of the item in the array you sent, counting from 0, and each violations[] entry points at the field at fault and the rule it broke — together they tell you which item, which field, which rule.
  • misspelledFields is a top-level object keyed by that same index. It is not attached to the individual events inside successEvents. Above, item 1 — the second one you sent — spelled click_id as clickId.
  • When nothing is misspelled the key is absent entirely — not an empty object, not an empty array. This differs from a single event, which always returns misspelledFields: [], so do not use the presence of the key as your signal.

NOTE

violations[].field tells you which field is at fault, but it does not always spell that field the way your request does. Fix it under its request-side name — request fields are snake_case (event_name, client_user_id).

Test mode ​

A dry run tells you whether one request is right, and keeps nothing. Test mode tells you what your integration has been sending: add the X-Test-Mode: true header to either ingestion endpoint and we process the request exactly as we would in production, then store the event as a test event that you can review for 14 days.

bash
curl -i -X POST "https://<<our-url>>/v2/s2s/event" \
    -H "Content-Type: application/json" \
    -H "X-API-KEY: dp_test_key_123" \
    -H "X-Test-Mode: true" \
    -d '{
        "client_user_id": "u-test-1",
        "click_id": "rs-test-1",
        "event_name": "register",
        "timestamp": 1702963200,
        "ip_address": "203.0.113.1"
        }'
CallStatus CodeBodyResponse HeaderStored
Real write200 OK{ "status": "success" }—Yes, in production
Test mode200 OKThe echoed record, or the per-item batch result with "mode": "test"X-Ingest-Mode: testAs a test event, for 14 days
Dry run200 OKThe echoed recordX-Ingest-Mode: dryrunNo
  • Test events never reach production. They are not in any report, never attributed to a click, never bid on and never billed. They are deleted after 14 days.
  • If a request carries both ?dryrun=1 and X-Test-Mode: true, dry run wins — nothing is stored.
  • A test event that fails validation gets the same error as in production and is not stored. In a batch, only the items that pass are stored.
  • Test-mode requests count against your rate limit as usual.

To view your test events, and for the limits that apply, see Test mode in v3 — test events sent through v2 and v3 are listed together.

Use Case Scenarios on Request Body Schema ​

User Register ​

{
  "client_user_id": "<<THE UNIQUE USER ID>>",
  "click_id": "<<THE UNIQUE CLICK ID>>",
  "event_name": "register",
  "timestamp": UTC seconds,
  "ip_address": "<<THE END USER IP>>",
  "user_agent": "<<THE USER AGENT STRING>>"
}

User Login ​

{
  "client_user_id": "<<THE UNIQUE USER ID>>",
  "click_id": "<<THE UNIQUE CLICK ID>>",
  "event_name": "login",
  "timestamp": UTC seconds,
  "ip_address": "<<THE END USER IP>>",
  "user_agent": "<<THE USER AGENT STRING>>"
}

User Deposit ​

{
  "client_user_id": "<<THE UNIQUE USER ID>>",
  "click_id": "<<THE UNIQUE CLICK ID>>",
  "event_name": "deposit",
  "currency": "<<THE CURRENCY, eg: USD | EUR | BTC>>",
  "amount": 5.00,
  "transaction_id": "<<THE UNIQUE TRANSACTION ID>>",
  "timestamp": UTC seconds,
  "ip_address": "<<THE END USER IP>>",
  "is_crypto": true | false
}

User Bet & Win/Loss ​

The bet stake (bet) and its settlement outcome (win / loss) must both be reported. Depending on when the game settles, choose one of the two patterns below — do not use both, or the result will be double-counted.

  • Pattern A — Combined (for instantly-settled games such as slots or roulette): include bet_result and bet_result_amount directly in the bet event so the stake and the outcome are reported in a single call.
  • Pattern B — Separate (for delayed-settlement games such as sports betting or poker): first send the bet event without the result fields, then send a standalone win or loss event after settlement, linking it back to the original bet via parent_transaction_id.
Pattern A: bet event with embedded result ​
{
  "client_user_id": "<<THE UNIQUE USER ID>>",
  "event_name": "bet",
  "game_type": "<<THE GAME TYPE>>",
  "game_provider": "<<THE GAME PROVIDER>>",
  "currency": "<<THE CURRENCY, eg: USD | EUR | BTC>>",
  "amount": 5.00,
  "transaction_id": "<<THE UNIQUE TRANSACTION ID>>",
  "timestamp": UTC seconds,
  "is_crypto": true | false,

  "bet_result": "win | loss",
  "bet_result_amount": 1.00
}

Note: amount is the stake placed by the player; bet_result_amount represents the player's net result. Send a positive value when bet_result is win, and a negative value when bet_result is loss (e.g. -5.00).

Pattern B: standalone win / loss event ​

Send a bet event without bet_result / bet_result_amount first (recording only the stake), then send the corresponding win or loss event once the bet settles:

{
  "client_user_id": "<<THE UNIQUE USER ID>>",
  "event_name": "win | loss",
  "game_type": "<<THE GAME TYPE>>",
  "game_provider": "<<THE GAME PROVIDER>>",
  "currency": "<<THE CURRENCY, eg: USD | EUR | BTC>>",
  "amount": 5.00,
  "transaction_id": "<<THE UNIQUE TRANSACTION ID>>",
  "parent_transaction_id": "<<THE PARENT BET TRANSACTION ID>>",
  "timestamp": UTC seconds,
  "is_crypto": true | false
}

Note: amount represents the player's net result. Send a positive value when event_name is win, and a negative value when event_name is loss (e.g. -5.00). parent_transaction_id must match the transaction_id of the original bet event so the two can be linked.

User Withdraw ​

{
  "client_user_id": "<<THE UNIQUE USER ID>>",
  "event_name": "withdraw",
  "currency": "<<THE CURRENCY, eg: USD | EUR | BTC>>",
  "amount": 5.00,
  "transaction_id": "<<THE UNIQUE TRANSACTION ID>>",
  "timestamp": UTC seconds,
  "ip_address": "<<THE END USER IP>>",
  "is_crypto": true | false
}