cashoutguard / cashoutguard-php
PHP client for the CashoutGuard fraud-prevention API (rewards, GPT and offerwall sites).
Requires
- php: >=7.4
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^9.6
Suggests
- ext-curl: Needed by the default HTTP transport (you can inject your own transport instead).
Provides
None
Conflicts
None
Replaces
None
README
PHP client for CashoutGuard, the fraud-prevention API for rewards, GPT and offerwall sites.
- PHP 7.4 or newer. No Composer dependencies. Uses
ext-curlby default, and you can inject any other HTTP transport. - Fail-open by default. A timeout, network error, 5xx or any other API error returns an
allowresult with$errorset. It never throws, so a CashoutGuard outage can't stop your payouts. - 2 second total timeout by default.
5-minute quickstart
1. Install
composer require cashoutguard/cashoutguard-php
Get your public key (pk_...) and secret key (sk_...) from the CashoutGuard dashboard. The secret key belongs on your server only.
2. Add the browser agent to every page
<?= \CashoutGuard\Browser::snippet('pk_live_YOUR_PUBLIC_KEY') ?>
This prints the recommended loader stub and <script src="https://cashoutguard.com/v1/agent.js" data-key="pk_..." async>.
On the forms that matter (signup, login, cashout), collect a request_id and send it along with the form:
<form id="cashout-form" method="post" action="/cashout"> <input type="hidden" name="cg_request_id"> ... </form> <script> CashoutGuard.ready(async () => { const { request_id } = await CashoutGuard.collect({ event: 'cashout' }); document.querySelector('#cashout-form [name=cg_request_id]').value = request_id ?? ''; }); </script>
collect() never rejects. If it fails, request_id is null and you just send an empty value.
3. Ask for a decision on your server
Plain PHP
require __DIR__ . '/vendor/autoload.php'; $cg = new \CashoutGuard\Client(getenv('CASHOUTGUARD_SECRET')); $risk = $cg->evaluate([ 'event' => 'cashout', // signup | login | offer_click | conversion | cashout | custom 'account_id' => (string) $user->id, // your own 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' => 5.00, 'currency' => 'USD', ]); if ($risk->isBlocked()) { holdCashout($user, $risk->reasonCodes()); } elseif ($risk->needsReview()) { queueForManualReview($user, $risk->raw); } else { payNow($user, $amount); }
Laravel
config/services.php:
'cashoutguard' => [ 'secret' => env('CASHOUTGUARD_SECRET'), 'public' => env('CASHOUTGUARD_PUBLIC'), ],
app/Providers/AppServiceProvider.php:
use CashoutGuard\Client; public function register(): void { $this->app->singleton(Client::class, fn () => new Client((string) config('services.cashoutguard.secret'))); }
In your layout (resources/views/layouts/app.blade.php), inside <head>:
{!! \CashoutGuard\Browser::snippet(config('services.cashoutguard.public')) !!}
In a controller:
use CashoutGuard\Client; use Illuminate\Http\Request; public function store(Request $request, Client $cashoutguard) { $user = $request->user(); $risk = $cashoutguard->evaluate([ 'event' => 'cashout', 'account_id' => (string) $user->id, 'request_id' => $request->input('cg_request_id'), 'ip' => $request->ip(), 'email' => $user->email, 'payout_address' => $request->input('paypal_email'), 'payout_method' => 'paypal', 'amount' => $request->input('amount'), 'currency' => 'USD', ]); if ($risk->failed()) { logger()->warning('CashoutGuard unavailable, failing open', ['error' => $risk->error]); } if ($risk->isBlocked()) { return back()->withErrors(['amount' => 'This cashout is on hold for review.']); } $cashout = $user->cashouts()->create([ 'amount' => $request->input('amount'), 'status' => $risk->needsReview() ? 'pending_review' : 'approved', ]); return redirect()->route('cashouts.show', $cashout); }
Offerwall conversions (postbacks)
Score the conversion from your postback handler. Send the same click_id you used on offer_click; no request_id is needed because the conversion inherits the device of its click.
$risk = $cg->evaluate([ 'event' => 'conversion', 'account_id' => $userId, // networks: "publisherId:userId" 'offer_id' => $offerId, 'offer_name' => $offerTitle, // shown in the dashboard 'click_id' => $clickId, 'transaction_id' => $txId, 'amount' => $payout, 'ip' => $userIp, // if the offerwall sends it 'source' => 'torox', // offerwall, network or publisher 'meta' => ['sub1' => $sub1], // up to 20 extra strings ]);
The Result object
| Member | Meaning |
|---|---|
isBlocked() / needsReview() / isAllowed() |
Based on action. Use these. |
$action |
allow, review or block: what to do. In monitor mode it is always allow. |
$decision |
allow, review or block: what the rules concluded, even in monitor mode. |
$score |
0–100 for this event, or null when the call failed. |
$reasons / reasonCodes() / hasReason('ip_vpn') |
[{code, weight, detail?}], highest weight first. |
$raw |
The decoded JSON response: account, device, network, linked_accounts, cashout, and so on. |
$eventId, $mode, $account, accountStatus() |
Convenience accessors. |
ok() / failed(), $error, $errorMessage, $statusCode |
$error is null on success. Otherwise it is a short code such as timeout, network_error, http_500, temporarily_unavailable, missing_or_invalid_secret_key, validation_failed, not_found, invalid_argument, invalid_response or missing_secret_key. |
evaluate() sends only the documented fields. It also drops optional values that the API would reject, such as a malformed IP or request_id, or a negative amount, and it truncates over-long strings. A bad optional field therefore never turns a real verdict into a 422.
Accounts
$acc = $cg->account('user_48213'); // GET /v1/accounts/{id} if ($acc->error === 'not_found') { /* never evaluated */ } echo $acc->score, ' ', $acc->decision, ' ', $acc->accountStatus(); $cg->setAccountStatus('user_48213', 'blocked'); // active | allowlisted | blocked
Webhooks
CashoutGuard signs each webhook with X-CashoutGuard-Signature = hex HMAC-SHA256 of the raw body, keyed with your webhook secret.
$body = file_get_contents('php://input'); // raw bytes, NOT re-encoded JSON $sig = $_SERVER['HTTP_X_CASHOUTGUARD_SIGNATURE'] ?? ''; if (! $cg->verifyWebhook($body, $sig, getenv('CASHOUTGUARD_WEBHOOK_SECRET'))) { http_response_code(401); exit; } $event = json_decode($body, true); // ['type' => 'account.blocked', 'data' => ['account_id' => ..., 'score' => ..., 'event_id' => ...]]
In Laravel, use $request->getContent() and $request->header('X-CashoutGuard-Signature', ''). You can also call \CashoutGuard\Webhook::verify() without a client.
Options
new \CashoutGuard\Client('sk_live_...', [ 'baseUrl' => 'https://cashoutguard.com', // default 'timeout' => 2.0, // seconds, whole request 'failOpen' => true, // false = throw TransportException / ApiException / InvalidArgumentException 'transport' => $myTransport, // any \CashoutGuard\Http\TransportInterface ]);
Tests
composer install vendor/bin/phpunit
The suite uses an injected fake transport. It also runs a real curl round trip against php -S on localhost, covering JSON echo, a 503, connection refused and timeout.
License
MIT