Crypto Payment Integration in 1C-Bitrix
1C-Bitrix is not the most convenient platform for custom payment integrations. The system of handlers is documented partially, events in CEvent trigger unpredictably, and the table structure changed between versions. Nevertheless, it's a real use case: Bitrix shops whose audience pays in crypto are not rare, especially in certain niches.
If you're in one of these use cases — this document is about doing it right.
Payment Handler Architecture
In Bitrix, payment systems are implemented via a class inheriting from \Bitrix\Sale\PaySystem\ServiceHandler. This is the correct approach — not a hack via awkward redirects, but official API.
Module structure:
/local/modules/mypay.crypto/
├── install/
│ ├── index.php # Module installer
│ └── handler/
│ └── crypto.php # Handler for Bitrix
├── lib/
│ ├── CryptoGateway.php # Business logic
│ └── WebhookHandler.php # Callback processing
├── include.php
└── .settings.php
Handler Class
<?php
namespace MyCrypto\CryptoPay;
use Bitrix\Sale\PaySystem\ServiceHandler;
use Bitrix\Sale\Payment;
use Bitrix\Main\Request;
class Handler extends ServiceHandler
{
// Handler type: REDIRECT — user goes to external URL
const RETURN_URL = true;
public function initiatePay(Payment $payment, Request $request = null)
{
$orderId = $payment->getOrderId();
$amount = $payment->getSum();
$currency = $payment->getCurrencyCode(); // usually RUB or USD
// Convert to crypto via rate
$cryptoAmount = $this->convertToCrypto($amount, $currency, 'USDT');
// Create payment in external system
$paymentData = $this->gateway->createInvoice([
'order_id' => $orderId,
'amount' => $cryptoAmount,
'currency' => 'USDT',
'callback_url'=> $this->getCallbackUrl($payment),
'return_url' => $this->getSuccessUrl($payment),
]);
// Save external ID for reconciliation
$this->setExtraParams([
'crypto_invoice_id' => $paymentData['invoice_id'],
]);
$this->setInitiatePayRedirect($paymentData['payment_url']);
return ServiceResult::createSuccess();
}
protected function getCallbackUrl(Payment $payment): string
{
return \Bitrix\Main\Engine\UrlManager::getInstance()->getHostUrl()
. '/bitrix/tools/sale_ps_interact.php?'
. http_build_query([
'TYPE' => 'BACK_URL_NOTIFY',
'PAYMENT_ID' => $payment->getId(),
]);
}
}
Webhook Processing
sale_ps_interact.php is the standard Bitrix endpoint for payment notifications. The handler receives control via processRequest:
public function processRequest(Payment $payment, Request $request)
{
$invoiceId = $request->get('invoice_id');
$status = $request->get('status');
$signature = $request->get('signature');
// 1. Verify signature
if (!$this->verifyWebhookSignature($request->toArray(), $signature)) {
$logger->error('Invalid webhook signature', ['invoice' => $invoiceId]);
return ServiceResult::createError('INVALID_SIGNATURE');
}
// 2. Check amount via API (don't trust webhook data)
$invoiceData = $this->gateway->getInvoice($invoiceId);
if ($invoiceData['status'] !== 'confirmed') {
return ServiceResult::createSuccess(); // wait
}
// 3. Verify amount matches
$expectedSum = $payment->getSum();
if (!$this->isAmountSufficient($invoiceData['received_amount'], $expectedSum)) {
return ServiceResult::createError('UNDERPAYMENT');
}
// 4. Confirm payment in Bitrix
$result = $payment->setField('PAID', 'Y');
if ($result->isSuccess()) {
$payment->save();
// Trigger event — send email, update order status
\Bitrix\Sale\Order::load($payment->getOrderId())->save();
}
return ServiceResult::createSuccess();
}
Exchange Rates
Can't show rate from arbitrary source without fixing. Use official rate with timestamp and store it:
class ExchangeRateService
{
private const CACHE_TTL = 300; // 5 minutes
public function getRate(string $from, string $to): array
{
$cacheKey = "crypto_rate_{$from}_{$to}";
$cached = \Bitrix\Main\Data\Cache::createInstance();
if ($cached->initCache(self::CACHE_TTL, $cacheKey)) {
return $cached->getVars();
}
// CoinGecko API or Binance
$rate = $this->fetchRateFromAPI($from, $to);
$data = ['rate' => $rate, 'fetched_at' => time(), 'expires_at' => time() + 900];
$cached->startDataCache();
$cached->endDataCache($data);
return $data;
}
}
Fix rate for 15 minutes. If user pays after expiration — create new invoice with current rate.
Saving Transaction Data
Bitrix doesn't have native storage for custom payment data. Options:
\Bitrix\Sale\Internals\PaymentTable — standard table, via setExtraParams you can save arbitrary fields (serialized in PS_PARAMS).
Separate table — preferred for large data and future analytics:
class CryptoTransactionTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'my_crypto_transactions'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('PAYMENT_ID'),
new StringField('INVOICE_ID'),
new StringField('CURRENCY'), // USDT, BTC, ETH
new StringField('NETWORK'), // ethereum, tron, bitcoin
new FloatField('CRYPTO_AMOUNT'),
new FloatField('FIAT_AMOUNT'),
new StringField('EXCHANGE_RATE'),
new StringField('TX_HASH'),
new StringField('STATUS'),
new DatetimeField('CREATED_AT'),
];
}
}
Display Payment Details
Component bitrix:sale.payment.pay renders payment page. For crypto payments you need custom page with QR code and timer:
// In template /local/templates/.../pay.php
$invoiceData = $arResult['PAY_SYSTEM_PARAMS'];
?>
<div class="crypto-payment">
<p>Transfer <strong><?= $invoiceData['amount'] ?> <?= $invoiceData['currency'] ?></strong></p>
<div class="qr-code">
<img src="https://api.qrserver.com/v1/create-qr-code/?data=<?=
urlencode($invoiceData['payment_uri']) ?>&size=200x200">
</div>
<code class="address"><?= $invoiceData['address'] ?></code>
<div class="timer" data-expires="<?= $invoiceData['expires_at'] ?>">
Pay within <span id="countdown"></span>
</div>
</div>
Payment URI for EVM: ethereum:0xADDRESS/transfer?address=0xTO&uint256=AMOUNT (EIP-681). For BTC: bitcoin:ADDRESS?amount=0.001&label=Order123.
Testing
Bitrix has no sandbox for payment systems. Test scenario: create test payment system, send TYPE=BACK_URL_NOTIFY manually via curl with test data, check statuses.
curl -X POST https://yourshop.ru/bitrix/tools/sale_ps_interact.php \
-d "TYPE=BACK_URL_NOTIFY&PAYMENT_ID=123&invoice_id=test_inv_001&status=confirmed&signature=..."
Typical Issues
Event order. Bitrix saves PAID=Y only if Order::save() is called correctly — Payment::save() is not enough. Test full flow with real order.
CSRF on webhook URL. sale_ps_interact.php bypasses CSRF protection, but your custom webhook endpoint doesn't. If making separate endpoint, add define('STOP_STATISTICS', true) and define('NO_KEEP_STATISTIC', 'Y') at file beginning.
Timezone. Bitrix stores dates in UTC but displays with site settings. When comparing expires_at use \Bitrix\Main\Type\DateTime not native PHP time().
Workflow
Analyze Bitrix version and current payment architecture → module development → gateway integration (NOWPayments/CoinPayments or custom) → staging testing → production installation.
Timeline 2-3 days: 1 day on module + webhook, 1 day on testing edge cases, 1 day on production deploy and monitoring first transactions.







