Integrating the Cashback System with 1C in 1C-Bitrix
A retail chain operates in two environments: offline sales via 1C:Retail and an online store on 1C-Bitrix. A customer makes a purchase in a physical store — the cashback accrual must appear in their personal account on the website within minutes. Conversely, cashback redeemed online must be reflected on the next in-store visit. Without balance synchronization between systems, the loyalty program effectively operates as two separate programs, one per channel.
Bidirectional Synchronization Architecture
Choosing the master system is the first architectural question. Options:
Option A: 1C as master. 1C-Bitrix stores only a cached balance; all operations are written to 1C; 1C-Bitrix regularly fetches the current balance. Easier to maintain consistency, but online redemption is impossible when 1C is unavailable.
Option B: 1C-Bitrix as master. 1C fetches the balance from 1C-Bitrix via API. Operations from 1C are sent to a 1C-Bitrix queue. Vulnerability — an offline register cannot redeem cashback when the network is unavailable.
Option C: synchronous exchange with a queue. Each system writes operations to its own queue; a background agent synchronizes them. The most reliable, but requires a conflict resolution mechanism for simultaneous operations.
For most projects, Option A with balance caching on the 1C-Bitrix side is appropriate.
API on the 1C-Bitrix Side
Create a REST endpoint in 1C-Bitrix to receive and send operations from 1C:
// /local/api/cashback/v1/
// Routing via urlrewrite.php or a dedicated file
class CashbackApiController
{
/**
* GET /local/api/cashback/v1/balance?user_phone=79001234567
* Used by 1C to check the balance at the register
*/
public function getBalance(): void
{
$this->requireApiKey();
$phone = $_GET['user_phone'] ?? '';
$userId = $this->getUserIdByPhone($phone);
if (!$userId) {
$this->respond(['error' => 'user_not_found'], 404);
return;
}
$balance = CashbackBalanceTable::getBalance($userId);
$this->respond([
'user_id' => $userId,
'balance' => $balance,
'updated_at' => CashbackBalanceTable::getLastUpdated($userId),
]);
}
/**
* POST /local/api/cashback/v1/transactions
* 1C sends operations (accrual/deduction for offline purchases)
*/
public function addTransaction(): void
{
$this->requireApiKey();
$body = json_decode(file_get_contents('php://input'), true);
$this->validateTransaction($body); // type, amount, external_id, user_phone
// Idempotency: external_id is unique on the 1C side
if (CashbackTransactionTable::existsByExternalId($body['external_id'])) {
$this->respond(['status' => 'already_exists', 'idempotent' => true]);
return;
}
$userId = $this->getUserIdByPhone($body['user_phone']);
\Bitrix\Main\Application::getConnection()->startTransaction();
try {
CashbackTransactionTable::add([
'USER_ID' => $userId,
'TYPE' => $body['type'], // accrual|debit
'AMOUNT' => $body['amount'],
'DESCRIPTION' => $body['description'] ?? '',
'EXTERNAL_ID' => $body['external_id'], // 1C document ID
'SOURCE' => '1c_retail',
'CREATED_AT' => new \Bitrix\Main\Type\DateTime($body['created_at']),
]);
if ($body['type'] === 'accrual') {
CashbackBalanceTable::credit($userId, $body['amount']);
} else {
CashbackBalanceTable::debit($userId, $body['amount']);
}
\Bitrix\Main\Application::getConnection()->commitTransaction();
$this->respond(['status' => 'ok']);
} catch (\Exception $e) {
\Bitrix\Main\Application::getConnection()->rollbackTransaction();
$this->respond(['error' => $e->getMessage()], 500);
}
}
}
External ID and Idempotency
The EXTERNAL_ID field in the transaction table is the unique identifier of the document in 1C. 1C generates it as {OperationType}_{DocumentNumber}_{Date}. When the same document is sent again (network failure, request retry), 1C-Bitrix responds with already_exists without re-crediting — this protects against duplicate balances.
Handler on the 1C Side
In 1C:Retail or 1C:Trade Management, an external data processor or extension is created that:
- When processing a receipt for accrual — sends a POST to the 1C-Bitrix API
- When redeeming cashback at the register — first requests the balance (
GET /balance), then POSTs an operation of typedebit - When voiding a receipt — sends a
releaseoperation (cancellation of deduction) or a negative accrual
Example HTTP request from 1C (built-in HTTP client):
Запрос = Новый HTTPЗапрос("/local/api/cashback/v1/transactions");
Запрос.Заголовки.Вставить("Content-Type", "application/json");
Запрос.Заголовки.Вставить("X-API-Key", Константы.КешбекAPIКлюч.Получить());
Запрос.УстановитьТелоИзСтроки(ЗаписатьJSON(ТелоЗапроса));
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
Synchronization During Offline Register Operation
The register may operate without network connectivity. In this case, operations accumulate in the local 1C database and are sent as a batch when the connection is restored. The 1C-Bitrix API accepts an array of transactions via POST /transactions/batch. Each transaction is processed independently; the response contains an array with the result for each (success/error/duplicate).
Conflict scenario: the user redeemed 500 online while the register was offline. The offline register attempted to deduct another 300, but the balance was 500. When synchronizing, 1C-Bitrix will detect that after the first deduction the balance is 0 and will reject the offline operation with insufficient_balance. 1C must handle this case: void the discount or request an additional payment.
User Lookup
Customer identification offline — by phone number. Lookup in 1C-Bitrix:
private function getUserIdByPhone(string $phone): ?int
{
$phone = preg_replace('/\D/', '', $phone);
$result = \Bitrix\Main\UserTable::getList([
'filter' => ['PERSONAL_PHONE' => $phone],
'select' => ['ID'],
'limit' => 1,
]);
if ($row = $result->fetch()) {
return (int)$row['ID'];
}
// Search in additional field UF_PHONE_VERIFIED
$result = \Bitrix\Main\UserTable::getList([
'filter' => ['UF_PHONE_VERIFIED' => $phone],
'select' => ['ID'],
'limit' => 1,
]);
return ($row = $result->fetch()) ? (int)$row['ID'] : null;
}
Phone numbers are stored in various formats — normalization to 11 digits (without +, leading 7 or 8) is mandatory on input.
Displaying Offline Operations in the Personal Account
Transactions with SOURCE = '1c_retail' are displayed in the history with the label "In-store purchase" instead of a link to an online order. The DESCRIPTION field receives the store address or register number from 1C — this is shown to the user.
Synchronization Monitoring
The local_cashback_sync_log table records all incoming requests from 1C: timestamp, external_id, response status. If no operations are received from a specific store within N hours — a trigger notifies the administrator (possibly a processing failure on the 1C side).
| Metric | Target |
|---|---|
| Time for an offline operation to appear in 1C-Bitrix | < 5 minutes with online register |
| Delay for batch synchronization after offline period | < 10 minutes after connection is restored |
| Transaction duplication | 0 (idempotency via external_id) |
Scope of Work
- REST API on the 1C-Bitrix side: balance, transactions, batch
- Transaction tables with
EXTERNAL_IDandSOURCE - Idempotency logic, conflict handling for insufficient balance
- Phone normalization, user lookup
- 1C external data processor (coordinated with the 1C developer)
- Synchronization monitoring, failure alerts
Timeline: 4–6 weeks with a 1C developer on the project. 6–10 weeks if the 1C handler needs to be developed from scratch.







