PHP SDK
Card and crypto payments in PHP with one Composer package. The SDK handles wallets and payment links, prices and QR codes. It also verifies the signed callback and decodes the payout amounts exactly. It works with any PHP 7.4+ application and has no dependencies.
Install
PHP 7.4 or newer with the json and openssl extensions. The SDK uses curl when it is installed and has no other dependencies.
composer require paygate-to/paygate-phprequire 'vendor/autoload.php';
$paygate = new PayGate\Client();
// Production: your free dedicated endpoint and, optionally, your white-label checkout domain
$paygate = new PayGate\Client([
'api_domain' => 'api-yourshop-1a2b3c.paygate.to',
'checkout_domain' => 'pay.yourshop.com',
]);Get your dedicated endpoint from the custom domain guide. Source code: github.com/paygate-to/paygate-php.
Card payments
There are three steps for each order. First, create a wallet tied to the order's callback URL. Then send the customer to the payment link. Finally, credit the order when the signed callback arrives. Payouts arrive in USDC on Polygon.
use PayGate\Callback\CallbackToken;
$tokens = new CallbackToken(getenv('PAYGATE_CALLBACK_SECRET')); // 64 random hex chars, kept on your server
// The callback carries the order id and a token so the id cannot be swapped.
$callback = $tokens->callbackUrl('https://shop.example/paygate/callback.php', ['order' => $order->id]);
$wallet = $paygate->card()->createWallet('0xYourPolygonUsdcWallet', $callback);
// What you expect to receive, in USD: the callback compares the amount that arrived with this.
$order->expectedUsd = $paygate->card()->convertToUsd($order->total, 'EUR')['value'];
header('Location: ' . $paygate->card()->paymentUrl($wallet, $order->total, $order->email, 'EUR'));paymentUrl() opens the hosted page, which lists the providers available for the customer's country, amount and currency. Pass a provider id as the fifth argument to send the customer straight to one provider. providers() lists the providers with their status and minimum order:
foreach ($paygate->card()->providers() as $provider) {
if ($provider->isActive()) {
echo $provider->getId(), ' min ', $provider->getMinimumAmount(), ' ', $provider->getMinimumCurrency(), "\n";
}
}
$url = $paygate->card()->paymentUrl($wallet, '49.90', '[email protected]', 'USD', 'wert');The same callback URL always returns the same wallet, so rebuilding the link for an order is safe.
Crypto on your page
Show the address, the amount and a QR code on your own checkout page:
$crypto = $paygate->crypto();
$wallet = $crypto->createWallet('btc', 'bc1qYourBitcoinWallet', $callback);
$quote = $crypto->convert('btc', $order->total, 'USD'); // ['value' => '0.00041512', 'exchange_rate' => ...]
$qr = $crypto->qrCode('btc', $wallet->getAddressIn(), $quote['value']);
echo 'Send ', $quote['value'], ' BTC to ', $wallet->getAddressIn();
echo '<img src="', $qr['data_uri'], '" alt="Payment QR code">';Coins are written as btc, erc20_usdc or erc20/usdc. Use a payout wallet on the coin's network, for example an EVM address for ERC20, BEP20 and Polygon tokens. $crypto->coins() lists every coin with its logo and minimum.
$crypto->createWallet('ltc', $myLtcWallet, $callback, [
'pending' => true, // also send a callback (pending=1) when the transaction is first seen
'confirmations' => 3, // confirmations required before the confirmed callback
]);Hosted multi-coin page
The customer picks the coin on the hosted page. Give one wallet per network you accept: evm, btc, bitcoincash, ltc, doge, solana, trc20, xmr, zec, ton.
$hosted = $paygate->crypto()->createHostedWallet([
'btc' => 'bc1qYourBitcoinWallet',
'evm' => '0xYourEvmWallet', // ETH, USDT, USDC... on ERC20, BEP20, Polygon, Arbitrum, Base...
'xmr' => 'YourMoneroWallet',
], $order->total, 'USD', $callback);
$url = $paygate->crypto()->hostedPaymentUrl($hosted, true /* customer pays the network fee */, [
'theme' => '#7c3aed', // hex codes or CSS colour names
]);
header('Location: ' . $url);Handling the callback
When a payment arrives, PayGate.to requests your callback URL with your parameters plus the payment data, and signs the request. Verify the signature first and fail closed:
use PayGate\Callback\CallbackData;
use PayGate\Crypto\Coin;
$result = $paygate->callbacks()->verifyRequest('https://shop.example/paygate/callback.php');
if (!$result->isValid()) {
http_response_code(403); // $result->getReason(): signature_mismatch, signature_absent...
exit;
}
$data = CallbackData::fromRequest();
if (!$tokens->verify($data->get('order'), $data->get('token'))) {
http_response_code(403);
exit;
}
if ($data->isPending()) {
exit('ok'); // seen, not yet confirmed: do not ship
}
$order = findOrder($data->get('order'));
if ($order->hasTransaction($data->getTxidIn())) {
exit('ok'); // callbacks can repeat: credit each payment once
}
$order->addPayment($data->getTxidIn(), $data->getCoin(), $data->getValueCoin());
// Card payments arrive in USDC after the provider's fee: the official plugins accept 60% of the USD total or more.
if ($order->method === 'card') {
$paid = (float) $order->receivedTotal() >= 0.60 * (float) $order->expectedUsd;
} else {
$paid = Coin::id($data->getCoin()) === Coin::id($order->coin) && (float) $order->receivedTotal() >= 0.98 * (float) $order->expectedCoin;
}
$order->setStatus($paid ? 'paid' : 'underpaid');
echo 'ok';Each check matters:
- Signature: proves PayGate.to sent the request and that nothing in it was changed.
- Token: the callback URL is chosen when the wallet is created, so anyone can create a wallet whose callback names one of your orders and pay it a few cents. The token proves your server issued that order reference. Without tokens, compare the callback's
address_inwith the order's deposit address (the Polygon address for cards); the hosted page has no address in advance, so use tokens there. - Transaction id: callbacks are repeated until you answer
ok. - Coin and amount: the payment link is visible to the customer, and any token can be sent to a crypto address.
- Payout address: callback URLs end up in web server logs. With a leaked URL, someone could create a wallet that pays them and still call your callback. The payout helper returns what reached your wallet; expect it to be at least 90% of the amount forwarded.
Never use PayGate.to's parameter names (value_coin, coin, txid_in, txid_out, address_in, value_forwarded_coin, address_out, pending) in your callback URL; the SDK refuses them. If a name appears twice, the SDK uses the last value, which is PayGate.to's.
Pass verifyRequest() the callback URL you registered, written as a constant in your code. The signed string is that URL's scheme, host and path followed by the query string exactly as it arrived; the SDK never rebuilds it. In Symfony or Laravel, pass the raw query yourself. $request->getQueryString() re-encodes the query and breaks the signature, so don't use it:
$result = $paygate->callbacks()->verify(
'https://shop.example/paygate/callback.php',
$request->server->get('QUERY_STRING'),
$request->headers->get('X-PayGate-Signature'),
$request->headers->get('X-PayGate-Key-Id')
);Payment data
| Method | Value |
|---|---|
getValueCoin() | Amount received. Card payments arrive in USDC (or USDT) on Polygon after the provider's fee. Crypto payments are in the coin paid. |
getCoin() | polygon_usdc, btc, erc20_usdt… |
getTxidIn() | Transaction that paid the order's address. Credit each one once. |
getTxidOut() | Transaction that forwarded the funds to you. |
getAddressIn() | The order's deposit address. |
getValueForwardedCoin() | Total forwarded to you and to any affiliates. |
getAddressOut() | Share received by each payout address. |
getPayouts() | Exact amount sent to each address, computed without floats. |
isPending() | True for an unconfirmed crypto notice. |
get('order') | Any of your own parameters. |
Laravel
The paygate-laravel package wires the SDK into Laravel 10, 11 and 12: a PayGate facade, a paygate.callback middleware that refuses any callback not signed by PayGate.to, and a helper that adds the order token to your callback URL.
composer require paygate-to/paygate-laravel
php artisan vendor:publish --tag=paygate-config # optionalPut your wallets in .env (PAYGATE_USDC_WALLET for cards, PAYGATE_BTC_WALLET, PAYGATE_EVM_WALLET, PAYGATE_SOLANA_WALLET… for crypto) and keep APP_URL set to your public https address: signatures are checked against it, never against the Host header.
use PayGate\Laravel\Facades\PayGate;
Route::get('/paygate/callback', PayGateCallbackController::class)
->name('paygate.callback')
->middleware('paygate.callback:order'); // signature + order token, else 403
$callback = paygate_callback_url('paygate.callback', ['order' => $order->id]);
$wallet = PayGate::card()->createWallet(config('paygate.wallets.usdc_polygon'), $callback);
return redirect()->away(PayGate::card()->paymentUrl($wallet, $order->total, $order->email, 'USD'));In the controller, $request->attributes->get('paygate') holds the verified payment data. Credit each txid_in once and check the coin and the amount, as above. The package README has a complete controller.
White label and affiliates
You can earn a commission on the sales of merchants you onboard. The shares must add up to 0.99, because PayGate.to keeps 1%. You can also split each payment across up to 19 wallets. See the white-label program.
// Commission: 2% to you, 97% to the merchant
$paygate->card()->createAffiliateWallet($merchantWallet, $callback, $yourWallet, 0.02, 0.97);
$paygate->crypto()->createAffiliateWallet('trc20_usdt', $merchantWallet, $callback, $yourWallet, 0.02, 0.97);
// Split payouts
$paygate->card()->createMassPayoutWallet($callback, ['0xPartnerA' => 0.50, '0xPartnerB' => 0.49]);
// Branding on the hosted pages
$paygate->card()->paymentUrl($wallet, '25', $email, 'USD', null, [
'logo' => 'https://yourshop.com/logo.png',
'background' => '#ffffff',
'theme' => '#7c3aed',
'button' => '#111827',
]);Coin data and status
$crypto = $paygate->crypto();
$crypto->coins(); // every coin with name, logo and minimum
$crypto->info('erc20_usdc'); // name, minimum, prices in fiat currencies
$crypto->price('btc', 'EUR');
$crypto->fees('btc'); // estimated network fee, in BTC and in fiat
$crypto->convert('sol_sol', '20', 'GBP');
// Occasional checks only: these endpoints are rate limited. Update orders from the callback.
$paygate->card()->paymentStatus($ipnToken);
$crypto->paymentStatus($ipnToken, 'bep20_usdt');Amounts are plain decimal strings (0.00001, never 1.0E-5), so they keep their full precision.
Configuration and errors
new PayGate\Client([
'api_domain' => 'api.paygate.to',
'checkout_domain' => 'checkout.paygate.to',
'timeout' => 30,
'connect_timeout' => 10,
'retries' => 1, // GET only; POST and swaps are never retried
'http_client' => $client, // implements PayGate\Http\HttpClientInterface
]);| Exception | When |
|---|---|
InvalidArgumentException | Input rejected before any request: a callback without an order parameter, shares that do not add up to 0.99, a bad currency… |
ApiException | PayGate.to answered with an error, a non-2xx status or a body that is not JSON. |
TransportException | The request never completed (DNS, TLS, timeout). |
All three extend PayGate\Exception\PayGateException. Messages never include query strings, because they contain wallets and callback tokens.
Examples and testing
The repository's examples/ folder is a small working shop: a card checkout, crypto on your own page, the hosted page, and a callback handler that checks the signature, the token, repeats, the coin and the amount.
composer install
composer test # unit tests, no network
PAYGATE_INTEGRATION=1 composer test:integration # read-only calls to the live APIIn your own tests, pass a fake http_client. To test a callback handler, use your own key pair: new CallbackVerifier(['v1' => $testPublicPem]), and sign the URL with openssl_sign().
To try a running site (your own handler or any official plugin), vendor/bin/paygate-callback-tester sends signed test callbacks with a key pair of your own. Trust its public key on a test site only, and remove it afterwards:
vendor/bin/paygate-callback-tester keygen
vendor/bin/paygate-callback-tester send 'https://shop.test/paygate/callback.php?order=1001&token=…' \
--coin=polygon_usdc --value=49.90 --payout=0xYourPolygonWallet
vendor/bin/paygate-callback-tester send '…' --tamper # must be refused
vendor/bin/paygate-callback-tester send '…' --times=4 # credited onceSecurity checklist
- Always verify the signature, and answer 403 when it fails. A callback that has not been verified proves nothing; the
User-Agentheader can be set by anyone. - Keep the callback secret on your server only. It stops a valid payment for one order from being replayed against another.
- Credit each incoming transaction (
txid_in) once. Callbacks are retried and can arrive more than once. - Compare the coin and the amount received with the order before releasing goods. Customers can see and edit the card payment link.
- Use a self-custodial wallet you control for payouts.
The signing key is published at /docs/paygate_callback_public.pem and bundled in the SDK as v1. The full scheme is described in the Callback Event reference.
