How Reward Validation Works: Server-Side Architecture for Rewarded Monetization
Why This Article Exists
If you integrate a rewarded monetization platform and get reward validation wrong, you lose money. Either you credit users who didn’t earn rewards (revenue leak), or you fail to credit users who did (trust damage).
There is no middle ground.
This article explains how server-side reward validation works — the architecture, the data flow, the security measures, and the edge cases every developer should understand before integrating any offerwall SDK.
The Core Problem
A rewarded monetization platform pays users for completing actions. The developer needs to know, with certainty, that a user actually completed the action before granting a reward.
The challenge: the user’s device is untrusted. Anything that happens client-side can be spoofed, replayed, or manipulated.
Client-side validation cannot defend against any of these. The only reliable approach is server-side validation, where the reward is granted based on data that originates outside the user’s device.
The Validation Flow
Step 1: User opens offerwall
└── SDK presents offers from the marketplace
Step 2: User clicks an offer
└── Tracking engine records the click
└── A unique click_id is generated
Step 3: User completes the required action
└── (e.g., installs the advertised app, completes a survey)
Step 4: Advertiser reports the conversion
└── Advertiser sends conversion event to the tracking engine
Step 5: Tracking engine validates the conversion
└── Checks: click happened? Within conversion window? Duplicate? Fraud signals?
Step 6: Postback sent to publisher server
└── HTTP GET to the publisher's postback URL
└── Contains: {player_id}, {click_id}, {offer_id}, {reward_amount}, {status}
Step 7: Publisher server validates the postback
└── Checks: legitimate source? Duplicate click_id? Status is 'approved'?
Step 8: Publisher server credits the user
└── Updates user balance in database
└── Returns HTTP 200
The user’s device is involved in steps 1–3. Everything from step 4 onward happens server-to-server. The user’s app does not participate in the validation or credit process.
The fundamental principle: the app triggers the offer, but the server grants the reward.
Why SDK Callbacks Aren’t Enough
Every offerwall SDK provides client-side callbacks. Developers sometimes use these to grant rewards directly:
// ❌ DO NOT DO THIS
offerwall.onReward = { reward ->
val amount = reward["amount"] as Double
user.balance += amount // Granting reward client-side
saveUserBalance(user)
}
This fails in four scenarios:
1. App closed before callback fires. The user opens the offerwall, clicks an offer that requires installing another app, and switches away. Your app gets suspended. The offer is completed, the postback is sent to your server, but the SDK callback never fires.
2. Client-side manipulation. A rooted device can hook into the SDK’s reward callback and fire it with arbitrary amounts.
3. Duplicate callbacks. The SDK may fire onReward multiple times with different statuses (pending, then approved). If you credit on the first callback, you might credit for a pending status that later becomes rejected.
4. Network conditions. The callback depends on real-time connectivity. The postback doesn’t.
The Correct Pattern
// ✅ Correct: SDK callback for UI feedback only
offerwall.onReward = { reward ->
val amount = reward["amount"] as Double
val status = reward["status"] as String
// UI feedback only
showRewardNotification("Reward pending: $amount")
// Do NOT credit the user here.
// The actual credit happens on your server via postback.
}
Postback Architecture in Detail
The Postback Request
When a conversion is validated, Perkox sends an HTTP request to the postback URL:
GET https://yourdomain.com/perkox/postback
?user_id={player_id}
&click_id={click_id}
&offer_id={offer_id}
&reward={reward_amount}
&payout={payout}
&status={status}
| Placeholder | What it contains | Example |
|---|---|---|
{player_id} |
Player ID passed to the SDK | player123 |
{click_id} |
Unique click identifier | d4246e2ca2894efc79df7b4b4 |
{offer_id} |
Completed offer ID | 10101 |
{reward_amount} |
Reward amount to credit | 50.00 |
{payout} |
Publisher payout amount | 10.00 |
{status} |
Conversion status | approved |
Example Implementation (Node.js)
app.get('/perkox/postback', async (req, res) => {
const { user_id: userId, click_id: clickId, offer_id: offerId,
reward: rewardAmount, payout: payoutAmount, status } = req.query;
// 1. Validate required fields
if (!userId || !clickId || !status) {
return res.status(400).send('Missing required fields');
}
// 2. Only process approved rewards
if (status === 'pending') return res.status(200).send('OK');
if (status === 'rejected') return res.status(200).send('OK');
// 3. Handle reversals
if (status === 'reversed') {
const existing = await db.query(
'SELECT * FROM rewards WHERE click_id = $1', [clickId]
);
if (existing.rows.length > 0) {
const originalAmount = existing.rows[0].amount;
await db.query('UPDATE users SET balance = balance - $1 WHERE id = $2',
[originalAmount, userId]);
await db.query('UPDATE rewards SET status = $1 WHERE click_id = $2',
['reversed', clickId]);
}
return res.status(200).send('OK');
}
// 4. Check for duplicates
const existing = await db.query(
'SELECT * FROM rewards WHERE click_id = $1', [clickId]
);
if (existing.rows.length > 0) return res.status(200).send('OK');
// 5. Credit the user
const reward = parseFloat(rewardAmount) || 0;
const payout = parseFloat(payoutAmount) || 0;
await db.query(
'INSERT INTO rewards (user_id, click_id, offer_id, amount, payout, status) VALUES ($1,$2,$3,$4,$5,$6)',
[userId, clickId, offerId, reward, payout, status]
);
await db.query('UPDATE users SET balance = balance + $1 WHERE id = $2',
[reward, userId]);
// 6. Always return 200
res.status(200).send('OK');
});
Reward Status Lifecycle
┌─────────┐
│ PENDING │ → Under review. Do NOT credit.
└────┬─────┘
│
▼
┌──────────┐
│ APPROVED │ → Validated. Credit the user.
└────┬─────┘
│
├─── (normal flow ends)
│
▼
┌──────────┐
│ REVERSED │ → Remove the credit (fraud, chargeback)
└──────────┘
| Status | Meaning | Action |
|---|---|---|
pending |
Reward under review | Do not credit yet |
approved |
Reward validated | Credit the user |
rejected |
Reward not approved | Do not credit |
reversed |
Previously approved, now reversed | Remove or adjust credit |
Security Considerations
IP Allowlisting: Restrict your postback endpoint to Perkox server IPs.
Parameter Validation: Validate reward amounts are reasonable and player_id matches expected format.
Idempotency: Use click_id as a UNIQUE constraint in your database to prevent double-credits.
CREATE TABLE rewards (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
click_id VARCHAR(255) UNIQUE NOT NULL,
offer_id VARCHAR(255),
amount DECIMAL(10, 2) NOT NULL,
payout DECIMAL(10, 2),
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Logging: Log every postback for audit and debugging.
Testing Checklist
Before going live, test all four scenarios:
Manual test:
curl "https://yourdomain.com/perkox/postback?user_id=test_user&click_id=test_001&offer_id=999&reward=50.00&payout=10.00&status=approved"
Duplicate test: Send the same request again. Verify no double credit.
Reversal test: Send status=reversed for the same click_id. Verify balance reduced.
Rejected test: Send status=rejected. Verify no credit granted.
Frequently Asked Questions
What’s the difference between the SDK callback and the postback?
The SDK callback fires inside the app for UI feedback. The postback is an HTTP request sent from Perkox’s servers to your server after validation. The postback is the source of truth.
Can I use SDK callbacks instead of postbacks?
No. SDK callbacks only fire while the offerwall is open. If the user closes the app, the callback won’t fire, but the postback will still be sent.
What HTTP status should my endpoint return?
Always return HTTP 200, even for duplicates or rejected statuses. Non-200 responses trigger retry attempts.
What if my server is down when the postback arrives?
Perkox will retry. Your endpoint must be idempotent — use click_id for deduplication.
Key Takeaways
- The app triggers the offer; the server grants the reward.
- SDK callbacks are for UI feedback only. Never credit users based on client-side events.
- Postbacks are the source of truth. Your server credits the user based on validated conversion data.
- Handle all four reward statuses. approved, pending, rejected, reversed.
- Make your handler idempotent. Use
click_idfor deduplication. - Always return HTTP 200.
- Log everything.
Further Reading
- What is Rewarded Monetization? The Definitive Guide →
- Offerwall SDK Integration: Flutter Tutorial →
- Offerwall SDK Integration: React Native Tutorial →
- Tapjoy Alternatives: 5 Platforms Compared →
- Perkox Postback URL Configuration →
Perkox provides rewarded monetization infrastructure — SDKs, tracking, analytics, and reward validation — for mobile apps and games across Android, iOS, Unity, Flutter, and React Native. Get started →
