Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sanbuk/sanbuk-php

Official PHP SDK for Sanbuk — CPA conversion tracking.

راهنمای فارسی →

composer require sanbuk/sanbuk-php
$sanbuk = new \Sanbuk\Client('YOUR_API_KEY');

$sanbuk->postback([
    'action'   => 'purchase',
    'event_id' => $orderId,   // your own id, never reused
    'click_id' => $clickId,   // the snbk_cid from the landing page
    'value'    => 25000000,   // Rial
]);

That is the whole integration. Everything below is detail.


How it works

Two channels report the same conversion, carrying the same event_id:

  • Postback (this SDK) is the financial source of truth. Only a postback can create a payable conversion.
  • Pixel (the browser) is verification and anti-fraud. It never bills on its own.

Sanbuk pairs them inside a 48-hour window. Getting that pairing right is the only thing the integration asks of you.

Requirements

PHP 7.4 or newer, with ext-curl, ext-json, and ext-mbstring. No package dependencies — so it drops into a WordPress or legacy Laravel install without fighting over a Guzzle version.

Capturing the click id

Sanbuk hands you ?snbk_cid=<uuid> on your landing page. Store it next to the order or user the moment you see it — without it a conversion is recorded but can never be attributed or paid.

use Sanbuk\ClickId;

// Query string first, then the cookie the pixel left behind.
$clickId = ClickId::capture();

// Or explicitly:
$clickId = ClickId::fromQuery($_GET) ?? ClickId::fromCookies($_COOKIE);

Malformed values return null rather than being passed through, so junk in a query string cannot turn into a 422 at checkout.

The browser pixel

use Sanbuk\Pixel;

$pixel = new Pixel('SNBK-PX-XXXXX');

// In <head>, on every page:
echo $pixel->loaderTag();

// On the thank-you page — same event id as the postback:
echo $pixel->trackTag('purchase', [
    'event_id' => $orderId,
    'value'    => 25000000,
]);

The pixel id is public and safe in page source. Output is escaped, so an order id containing </script> cannot break out of the block.

Rehearsing before you go live

$sanbuk->test()->postback([
    'action'   => 'purchase',
    'event_id' => 'ORD-1',
    'value'    => 1000,
]);

A test event is stored and verifies the action in the panel, but never spends your wallet. It is a separate event from its live twin, so rehearsing with a real order id never consumes it.

Check what actually happened rather than what you intended:

$result = $sanbuk->test()->postback(['action' => 'purchase', 'event_id' => 'ORD-1']);

$result->isTest();       // false means the sandbox header did not land
$result->isAccepted();   // first time
$result->isDuplicate();  // already seen — nothing changed
$result->getEventId();
$result->getMode();      // Mode::LIVE or Mode::TEST

Configuration

use Sanbuk\Client;
use Sanbuk\Config;
use Sanbuk\Mode;

$sanbuk = new Client('YOUR_API_KEY', new Config([
    'mode'                => Mode::LIVE,
    'timeout'             => 5.0,
    'connect_timeout'     => 2.0,
    'max_retries'         => 3,
    'retry_base_delay_ms' => 200,
    'retry_max_delay_ms'  => 5000,
]));

A misspelled option raises instead of being silently ignored.

Retries

Postbacks are idempotent per (action, event_id) within a mode: sending the same pair twice answers 200 and changes nothing. The SDK leans on that and retries network failures, 429, and 5xx with exponential backoff and full jitter.

Client errors — a wrong key, an undefined action — are never retried. They will be just as wrong in 200ms.

Errors

use Sanbuk\Exception;

try {
    $sanbuk->postback([
        'action'   => 'purchase',
        'event_id' => $orderId,
        'value'    => $total,
    ]);
} catch (Exception\ValidationFailedException $e) {
    $e->getErrors();             // ['params.order_id' => ['is required']]
} catch (Exception\ValueRequiredException $e) {
    // This action prices on value.
} catch (Exception\UnknownActionException $e) {
    // No such action code in your workspace.
} catch (Exception\InvalidApiKeyException $e) {
    // Wrong or missing key.
} catch (Exception\RateLimitException $e) {
    // Retries exhausted — queue it and send the same event_id later.
} catch (Exception\SanbukException $e) {
    // Anything else from this SDK.
}

Branch on $e->getErrorCode(), not on $e->getMessage(): the contract states the message is localised and may change, while the code is stable.

Bringing your own HTTP stack

use Sanbuk\Http\HttpClient;

final class MyHttpClient implements HttpClient { /* ... */ }

$sanbuk = new \Sanbuk\Client('YOUR_API_KEY', new Config(), new MyHttpClient());

Retries stay in the client, so every implementation retries identically.

Tests

composer install
composer test

License

Apache-2.0

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages