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.
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.
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.
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.
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.
$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::TESTuse 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.
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.
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.
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.
composer install
composer testApache-2.0