Search by

cashoutguard / cashoutguard-php

cashoutguard-team

PHP client for the CashoutGuard fraud-prevention API (rewards, GPT and offerwall sites).

Package info

github.com/cashoutguard/cashoutguard-php

Homepage

pkg:composer/cashoutguard/cashoutguard-php

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-09-29 11:38 UTC

This package is auto-updated.

Last update: 2026-09-29 12:36:08 UTC


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-curl by 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 allow result with $error set. 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