1C-Bitrix Integration with an Anti-Fraud System
Fraudulent orders on e-commerce sites are not only financial losses from chargebacks. They consume operator time, leave abandoned goods in the warehouse, and damage relationships with payment systems when dispute rates are high. The built-in tools of 1C-Bitrix address some of the problem (OTP phone verification, order limits), but they do not replace a dedicated fraud risk assessment system. Integration with an external risk scoring service is required.
Integration architecture
The anti-fraud check is embedded in the order checkout process. There are two points to invoke it:
Before order save — before writing to the database. If anti-fraud blocks the order, it is never created. Advantage: clean. Disadvantage: a synchronous call adds latency to order placement (200–500 ms per API call).
After order save — the order is created with a "Pending review" status, the anti-fraud system checks asynchronously. The result updates the status. Advantage: no latency. Disadvantage: requires a queue handler.
For most stores — synchronous check before saving the order with a 2–3 second timeout.
Anti-fraud providers
Seon — REST API, performs device fingerprinting, email/phone scoring, IP reputation. Popular in e-commerce.
IPQS (IPQualityScore) — comprehensive IP, email, phone, and device checks. Budget-friendly option.
Kount / Forter / Signifyd — enterprise solutions with ML models trained on store-specific data.
Custom rule-based model — if volume is under 200 orders/day, complex external systems are overkill. A set of PHP checks is sufficient.
Order check handler
// /local/lib/Fraud/FraudCheckHandler.php
namespace Local\Fraud;
AddEventHandler('sale', 'OnBeforeOrderFinalAction', [FraudCheckHandler::class, 'check']);
class FraudCheckHandler
{
public static function check(\Bitrix\Sale\Order $order): \Bitrix\Main\EventResult
{
if ($order->getId() > 0) {
// Existing order being updated — skip
return new \Bitrix\Main\EventResult(\Bitrix\Main\EventResult::SUCCESS);
}
try {
$checker = new FraudChecker();
$result = $checker->evaluate($order);
if ($result->isBlocked()) {
return new \Bitrix\Main\EventResult(
\Bitrix\Main\EventResult::ERROR,
new \Bitrix\Main\Error($result->getBlockReason())
);
}
if ($result->requiresReview()) {
// Flag order for manual review
$order->setField('COMMENTS', '[FRAUD_REVIEW] Score: ' . $result->getScore());
}
} catch (\Throwable $e) {
// Anti-fraud errors must not block the order
\Bitrix\Main\Diag\Debug::writeToFile(
['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()],
'Fraud check error',
'/local/logs/fraud.log'
);
}
return new \Bitrix\Main\EventResult(\Bitrix\Main\EventResult::SUCCESS);
}
}
Risk assessment class
namespace Local\Fraud;
class FraudChecker
{
private const BLOCK_THRESHOLD = 80;
private const REVIEW_THRESHOLD = 50;
public function evaluate(\Bitrix\Sale\Order $order): FraudResult
{
$score = 0;
$reasons = [];
$props = $order->getPropertyCollection();
$ip = $_SERVER['REMOTE_ADDR'];
$email = $props->getItemByOrderPropertyCode('EMAIL')?->getValue() ?? '';
$phone = $props->getItemByOrderPropertyCode('PHONE')?->getValue() ?? '';
// IP checks
$ipScore = $this->checkIp($ip);
$score += $ipScore['score'];
if ($ipScore['score'] > 20) $reasons[] = $ipScore['reason'];
// Email checks
$emailScore = $this->checkEmail($email);
$score += $emailScore['score'];
if ($emailScore['score'] > 10) $reasons[] = $emailScore['reason'];
// Order frequency
$freqScore = $this->checkOrderFrequency($ip, $email, $phone);
$score += $freqScore['score'];
if ($freqScore['score'] > 15) $reasons[] = $freqScore['reason'];
// Order amount
$amountScore = $this->checkOrderAmount($order);
$score += $amountScore['score'];
// External API check (if configured)
if (defined('FRAUD_API_KEY') && FRAUD_API_KEY) {
$apiScore = $this->checkExternalApi($ip, $email, $phone, $order);
$score += $apiScore['score'];
if ($apiScore['score'] > 20) $reasons[] = $apiScore['reason'];
}
$this->log($order->getId() ?: 0, $ip, $email, $score, $reasons);
return new FraudResult($score, $reasons, self::BLOCK_THRESHOLD, self::REVIEW_THRESHOLD);
}
private function checkIp(string $ip): array
{
// VPN / Tor / datacenter IP — high risk
$conn = \Bitrix\Main\Application::getConnection();
// IP in 1C-Bitrix stop list
$inStopList = $conn->query(
"SELECT ID FROM b_stop_list WHERE IP_ADDR = '{$ip}' AND ACTIVE = 'Y' LIMIT 1"
)->fetch();
if ($inStopList) return ['score' => 60, 'reason' => 'IP in stop list'];
// Number of orders from this IP in the last 24 hours
$orderCount = (int)$conn->query(
"SELECT COUNT(*) cnt FROM b_sale_order
WHERE CREATED_BY_IP = '{$ip}'
AND DATE_INSERT > DATE_SUB(NOW(), INTERVAL 24 HOUR)"
)->fetch()['cnt'];
if ($orderCount > 5) return ['score' => 40, 'reason' => "IP: {$orderCount} orders/24h"];
if ($orderCount > 2) return ['score' => 15, 'reason' => "IP: {$orderCount} orders/24h"];
return ['score' => 0, 'reason' => ''];
}
private function checkEmail(string $email): array
{
if (empty($email)) return ['score' => 20, 'reason' => 'No email'];
// Disposable domains
$tempDomains = ['guerrillamail.com', 'mailinator.com', 'tempmail.com', 'throwam.com', 'yopmail.com'];
$domain = strtolower(substr(strrchr($email, '@'), 1));
if (in_array($domain, $tempDomains)) return ['score' => 40, 'reason' => 'Disposable email'];
// Number of orders with this email
$conn = \Bitrix\Main\Application::getConnection();
$emailSafe = $conn->getSqlHelper()->forSql($email);
$orderCount = (int)$conn->query(
"SELECT COUNT(*) cnt
FROM b_sale_order_props_value pv
JOIN b_sale_order_props p ON p.ID = pv.ORDER_PROPS_ID
JOIN b_sale_order o ON o.ID = pv.ORDER_ID
WHERE p.CODE = 'EMAIL'
AND pv.VALUE = '{$emailSafe}'
AND o.DATE_INSERT > DATE_SUB(NOW(), INTERVAL 7 DAY)"
)->fetch()['cnt'];
if ($orderCount > 3) return ['score' => 25, 'reason' => "Email: {$orderCount} orders/week"];
return ['score' => 0, 'reason' => ''];
}
private function checkOrderAmount(\Bitrix\Sale\Order $order): array
{
$price = (float)$order->getPrice();
// Large order from a new customer — risk
$userId = (int)$order->getUserId();
if ($price > 100000 && $userId > 0) {
$conn = \Bitrix\Main\Application::getConnection();
$prevOrders = (int)$conn->query(
"SELECT COUNT(*) cnt FROM b_sale_order WHERE USER_ID = {$userId} AND STATUS_ID NOT IN ('C')"
)->fetch()['cnt'];
if ($prevOrders === 0) {
return ['score' => 30, 'reason' => 'High amount + new customer'];
}
}
return ['score' => 0, 'reason' => ''];
}
private function checkExternalApi(string $ip, string $email, string $phone, \Bitrix\Sale\Order $order): array
{
$http = new \Bitrix\Main\Web\HttpClient();
$http->setHeader('Authorization', 'Bearer ' . FRAUD_API_KEY);
$http->setTimeout(2); // hard timeout
$response = $http->post('https://api.fraudprovider.com/v1/check', json_encode([
'ip' => $ip,
'email' => $email,
'phone' => $phone,
'amount' => $order->getPrice(),
]));
if ($http->getStatus() !== 200) return ['score' => 0, 'reason' => ''];
$data = json_decode($response, true);
$risk = (int)($data['risk_score'] ?? 0);
return [
'score' => (int)($risk * 0.5), // normalize to our scale
'reason' => $risk > 70 ? "External API risk: {$risk}" : '',
];
}
private function log(int $orderId, string $ip, string $email, int $score, array $reasons): void
{
\Bitrix\Main\Diag\Debug::writeToFile(
compact('orderId', 'ip', 'email', 'score', 'reasons'),
'Fraud check',
'/local/logs/fraud.log'
);
}
}
Admin interface
In the admin panel — an "Anti-Fraud" section with:
- A table of suspicious orders (status "Pending review")
- "Approve" / "Reject" buttons
- History of blocked attempts with IPs and reasons
- Ability to add an IP or email to the allow/block list
Implementation timelines
| Configuration | Timeline |
|---|---|
| Basic anti-fraud (IP, email, frequency) | 4–5 days |
| + external API integration (Seon/IPQS) | +2–3 days |
| + admin interface, allow/block lists | +2–3 days |
| + ML scoring on proprietary data | +2–4 weeks |







