CashoutGuard integrations: PHP, Laravel, Node.js, Next.js, Python, WordPress, Android and offerwalls
Every integration is the same two steps, whatever you build with. This page shows them for each stack, with the exact code from our client libraries.
Short answer
Load the browser agent (or the Android SDK) to get a request_id on the pages that matter, then have your server call POST /v1/evaluate at signup, offer click, conversion and cashout and act on the action it returns. Ready-made clients exist for PHP, Node.js, WordPress and Android; any other language uses one HTTPS call with JSON.
- PHP and Laravel
- composer require cashoutguard/cashoutguard-php (no dependencies, PHP 7.4+).
- Node.js and Next.js
- npm install @cashoutguard/node (Node 18+, TypeScript types included).
- WordPress
- Plugin: scores signups and logins, adds a Risk column to Users.
- Android apps
- Native SDK (.aar): emulators, rooted phones, hooks, VPN on the device.
- Anything else
- POST /v1/evaluate with a secret key and a JSON body.
The two steps every integration has
- In the browser or app: load the agent with your public key (pk_live_...) and call collect() when a user signs up, logs in, opens an offer or asks for a cashout. It returns a request_id; send it to your server with the form.
- On your server: call POST /v1/evaluate with your secret key (sk_live_...), the event, your own user id as account_id and the request_id. Act on action: allow, review or block.
Conversions from offerwalls arrive by postback, server to server, with no browser: send them as a conversion with the same account_id and the click_id, and CashoutGuard links them to the click. Every official client fails open after two seconds, so an outage never stops your users or your payouts.
What to send at each moment
Send the events you have. More events give the engine more to link, but each one is useful on its own. The fields in the right column are the ones that catch the most fraud at that moment.
| Event | When to send it | Fields that matter most |
|---|---|---|
| signup | Right after the account is created, before any welcome bonus | account_id, request_id, ip, email |
| login | After a successful login | account_id, request_id, ip |
| offer_click | When the user opens an offer or an offerwall link | account_id, request_id, offer_id, offer_name, click_id |
| conversion | In your postback handler, before crediting the user | account_id, click_id, transaction_id, amount, ip, offer_country |
| cashout | When the user requests a withdrawal, before you pay | account_id, request_id, amount, currency, payout_address, payout_method |
The payout address is the single most useful field on a cashout: one person running twenty accounts usually cashes out to one or two wallets or PayPal emails. It is hashed before it is stored, and it is only compared with other accounts of the same site.
PHP and Laravel
composer require cashoutguard/cashoutguard-php<?= \CashoutGuard\Browser::snippet('pk_live_YOUR_PUBLIC_KEY') ?> // in your layout: loads the agent$cg = new \CashoutGuard\Client(getenv('CASHOUTGUARD_SECRET'));
$risk = $cg->evaluate([
'event' => 'cashout',
'account_id' => (string) $user->id,
'request_id' => $_POST['cg_request_id'] ?? null,
'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
'email' => $user->email,
'payout_address' => $user->paypal_email,
'payout_method' => 'paypal',
'amount' => (float) $_POST['amount'],
]);
if ($risk->isBlocked()) {
// hold the cashout for review
}In Laravel, put the secret in .env, build the client once in a service provider and call it from the controllers that create accounts, credit conversions and pay cashouts. There is also a single-file download for sites without Composer (see the documentation).
Ready-to-copy middleware for the cashout route. It asks CashoutGuard before your controller runs, stops blocked requests and passes the result on, so a cashout that needs review can be held. Nothing is blocked while the site is in shadow mode.
// app/Http/Middleware/CashoutGuardCheck.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CashoutGuardCheck
{
public function handle(Request $request, Closure $next, string $event = 'cashout')
{
$user = $request->user();
$risk = app(\CashoutGuard\Client::class)->evaluate(array_filter([
'event' => $event,
'account_id' => (string) $user->id,
'request_id' => $request->input('cg_request_id'),
'ip' => $request->ip(),
'email' => $user->email,
'amount' => $request->input('amount'),
'payout_address' => $request->input('wallet'),
], fn ($v) => $v !== null && $v !== ''));
if ($risk->isBlocked()) {
return back()->withErrors(['cashout' => 'We could not process this request. Please contact support.']);
}
$request->attributes->set('cg_risk', $risk); // needsReview(): hold it for manual approval
return $next($request);
}
}
// AppServiceProvider::register()
$this->app->singleton(\CashoutGuard\Client::class, fn () => new \CashoutGuard\Client(config('services.cashoutguard.secret')));
// routes/web.php
Route::post('/cashout', [CashoutController::class, 'store'])->middleware(['auth', \App\Http\Middleware\CashoutGuardCheck::class.':cashout']);For signups, call evaluate() in your registration controller right after the user is created, with the new user id, and suspend the account when it is blocked. For offerwall postbacks, see "Stop fraudulent postbacks" below.
Node.js, Express and Next.js
npm install @cashoutguard/nodeimport { CashoutGuard } from '@cashoutguard/node';
const cg = new CashoutGuard(process.env.CASHOUTGUARD_SECRET);
const risk = await cg.evaluate({
event: 'cashout',
account_id: user.id,
request_id: body.cg_request_id,
ip: clientIp,
amount: Number(body.amount),
currency: 'USD',
});
if (risk.isBlocked()) return holdForReview();// Express: guard any route with one line
import { CashoutGuard } from '@cashoutguard/node';
const cg = new CashoutGuard(process.env.CASHOUTGUARD_SECRET);
export const guard = (event) => async (req, res, next) => {
const risk = await cg.evaluate({
event,
account_id: String(req.user.id),
request_id: req.body.cg_request_id,
ip: req.ip, // behind a proxy: app.set('trust proxy', 1)
email: req.user.email,
amount: Number(req.body.amount) || undefined,
payout_address: req.body.wallet,
});
if (risk.isBlocked()) return res.status(403).json({ error: 'Request could not be processed' });
req.cgRisk = risk; // risk.needsReview(): hold it for manual approval
next();
};
app.post('/cashout', requireLogin, guard('cashout'), cashoutHandler);In Next.js, call it from a route handler or server action, never from client components: the secret key must stay on the server. Load the agent with a Script tag in your root layout.
Python and any other language
The server API is one HTTPS call with a JSON body and a bearer token, so any stack works: Python, Go, Ruby, Java, .NET or a no-code backend.
For Python there is a single-file client with no dependencies: cashoutguard.py. It adds a short timeout and fails open, so an outage on our side never blocks your users.
from cashoutguard import CashoutGuard
cg = CashoutGuard(os.environ['CASHOUTGUARD_SECRET'])
risk = cg.evaluate({'event': 'cashout', 'account_id': str(user.id), 'request_id': form.get('cg_request_id'),
'ip': client_ip, 'amount': 5.0, 'currency': 'USD', 'payout_address': wallet})
if risk.is_blocked():
hold_for_review()import os, requests
r = requests.post(
'https://cashoutguard.com/v1/evaluate',
headers={'Authorization': 'Bearer ' + os.environ['CASHOUTGUARD_SECRET']},
json={'event': 'signup', 'account_id': str(user.id), 'request_id': form.get('cg_request_id'), 'ip': client_ip},
timeout=2,
)
action = r.json().get('action', 'allow') if r.ok else 'allow' # fail openWordPress
The WordPress plugin loads the agent on every page and on the login page, scores every signup and login, and adds a Risk column to the Users screen. Rewards and offerwall plugins can score conversions and cashouts through its hooks. Download it from the documentation, upload it in Plugins, then paste your two keys in its settings page.
Android apps
The native Android SDK checks for emulators, rooted and hooked phones (Frida, Xposed), a VPN running on the device and location spoofing, and gives each phone an ID that survives reinstalls, so one person cannot come back as a new user. The app gets a request_id; your server calls /v1/evaluate exactly like the web flow.
// Application.onCreate()
CashoutGuard.init(this, "pk_live_YOUR_PUBLIC_KEY")
// on signup, offer click or cashout
CashoutGuard.collect("cashout") { requestId ->
api.requestCashout(amount, cgRequestId = requestId)
}Apps that show your site in a WebView can use the browser agent instead: add null to the site's allowed origins, because WebViews send the origin "null".
Offerwall postbacks and networks
- When the user opens an offer, collect in the browser and send an offer_click with the offer and your own click id.
- When the postback arrives, send a conversion from your postback handler with the same account_id and click_id, the network's transaction id, the reward as amount and the user IP if the network passes it. No request_id is needed: the conversion inherits the device of its click.
Keep verifying the network's signature and IP allowlist in your handler as you do today; CashoutGuard adds the fraud check on top, it does not replace postback security. See how to secure your offerwall postbacks.
If you run an offerwall or ad network with many publishers, send account ids as publisher:user and turn on offerwall network mode in Sites & keys. Accounts are then only linked inside the same publisher, and the dashboard shows fraud per publisher.
Stop fraudulent postbacks before they are paid
This is the setup most rewards sites and offerwall networks want: every conversion is checked before anyone is paid, fraudulent ones are stopped and kept for review, and nobody is banned. It works the same for a GPT site that receives postbacks from offerwalls and for a network that forwards postbacks to its publishers. No developer? The postback relay does it without code.
- Call /v1/evaluate in your postback handler, before crediting. Send the conversion with account_id, click_id, transaction_id, amount, offer_id and the user IP. Use a short timeout (1.5 to 2 seconds) and pay as usual if there is no answer, so an outage never stops your payouts.
- If action is block, do not pay it. A GPT site does not credit the user. A network does not credit the publisher and does not fire the publisher postback. Keep the conversion in your own list of blocked postbacks with the reasons, so you can review it.
- Switch the site to Enforce and choose "Only fraudulent conversions (postbacks)". Until then action is always allow: you can deploy the code in shadow mode and nothing changes. Leave "Also block the user's account" off, so the user keeps their account and only the fraudulent conversion is stopped.
- Review and release. When you approve a blocked conversion by hand, credit it and fire its postback without calling /v1/evaluate again, so it is not counted twice.
// GPT site: your offerwall postback handler (signature and IP checks first, as today)
$risk = $cg->evaluate([
'event' => 'conversion',
'account_id' => (string) $userId,
'click_id' => $clickId, // the id you put in the offer link
'transaction_id' => $transactionId,
'amount' => (float) $reward,
'offer_id' => (string) $offerId,
'ip' => $userIp, // if the offerwall sends it
]);
if ($risk->isBlocked()) {
saveBlockedPostback($transactionId, $risk->reasonCodes()); // your own table, to review
exit('0'); // answer the way you answer other rejected postbacks
}
creditUser($userId, $reward);
exit('1');// Offerwall network: the advertiser postback, before paying the publisher
$risk = $cg->evaluate([
'event' => 'conversion',
'account_id' => $publisherId . ':' . $subId, // network mode: publisher:user
'click_id' => $clickToken,
'transaction_id' => $transactionId,
'amount' => (float) $payout,
'offer_id' => (string) $offerId,
'ip' => $clickIp,
'source' => $publisherName, // fraud per publisher in the dashboard
]);
if ($risk->isBlocked()) {
holdConversion($conversion, 'CashoutGuard', $risk->reasonCodes()); // never reaches the publisher
return;
}
payPublisher($conversion);
firePublisherPostback($conversion);Testing your integration
- Load a page with the agent and open your browser console: the agent explains there if the public key is wrong or the domain is not in the allowed origins.
- Sign up a test user and open Events in the dashboard. The signup should appear within seconds with its device, network and score. If it shows "No device data", the request_id did not reach your server call.
- Open an offer and fake a postback with your network's test tool. The conversion should appear linked to the click, with the same device.
- Check Install in the dashboard: its checklist turns green when the first browser collect and the first server evaluate arrive.
Common integration mistakes
- The secret key in the browser. Only the public key belongs in pages and apps. If a secret key was ever shipped to a browser, rotate it in Sites & keys.
- The proxy IP instead of the user IP. Behind Cloudflare or a load balancer, pass the real client IP from the header your proxy sets, not REMOTE_ADDR.
- Checking after paying. Call evaluate before you credit a conversion or send a cashout, otherwise a block comes too late.
- Reading decision instead of action. In shadow mode decision can say block while action says allow. Act on action; read decision to see what would happen.
- No click_id on conversions. Without it a conversion cannot inherit the device of its click, and completed-too-fast checks lose their start time.
- Reusing one request_id. Collect a fresh one for each important moment. A request_id used by a second account is itself a fraud signal.
Webhooks, Zapier and Make
Set a webhook URL on the site and CashoutGuard posts every blocked event (event.blocked) and every account that crosses the block score (account.blocked, with the action to take on the user). Each request is signed: X-CashoutGuard-Signature is the hex HMAC-SHA256 of the raw body with your webhook secret.
No backend changes needed to get alerts: point the webhook at a Zapier "Catch Hook" or a Make custom webhook and forward it to Slack, Discord, Telegram, email or a spreadsheet.