Telegram Mini App Development with Bitrix24 Integration
A manager in the field opens Telegram, sees a lead, and moves it to the next status in 30 seconds. Instead of 5 minutes searching in a browser. This is a typical scenario we cover with a Telegram Mini App + Bitrix24 REST API combination. The Mini App works as a CRM dashboard right in the chat: deal list, stages, contact info, ability to comment. We develop turnkey with full OAuth routing, token storage, and webhook configuration.
Problems the Integration Solves
Authorization without a browser. The Bitrix24 REST API requires an OAuth access_token, but a Mini App is a web page in Telegram WebView where redirect is impossible. The solution is a mediator server that validates the Telegram initData and issues an OAuth link or a ready-made token. We've applied this approach in 15+ projects.
Delays in lead processing. Managers spend up to 5 minutes opening a browser, logging into the CRM, and searching for a card. The Mini App reduces this to 30 seconds: the employee sees the lead immediately and changes the status with one tap. According to our measurements, the speed of reaction to an incoming lead increases by 60–70% — the Telegram Mini App processes leads 3 times faster than through the web interface.
Real-time notifications. Without integration, a manager learns about a new deal via email digests with minutes of delay. Bitrix24 event webhooks send a POST request to the server, which then sends a message to Telegram. The notification arrives in 1–2 seconds.
Use Cases
| Scenario | Description | Target Audience |
|---|---|---|
| Mobile CRM Dashboard | View leads and deals, change status, add comments | Field managers, sales departments |
| Corporate Ordering | Place requests by dealers/agents via Mini App | Network companies, distributors |
| Self-Service Portal | Client sees request status and communication history | B2C services, tech support |
| Task Manager | Task list, status change, file attachment | Teams using Bitrix24 |
Architecture: OAuth and Server Proxy
Authorization process:
- User opens the Mini App. The frontend sends initData to the server.
- Server validates initData hash, extracts user_id, and looks up a record in the
tg_bx24_tokenstable. - If a token is found and not expired — returns a JWT for the Mini App. If not — returns an OAuth authorization link for Bitrix24.
- After confirmation, the server exchanges the
codefor anaccess_tokenand saves it linked to the Telegram user_id.
| Step | Action | Participant |
|---|---|---|
| 1 | Open Mini App, send initData | User -> Mini App |
| 2 | Validate initData, search for token | Server |
| 3 | Return JWT or OAuth link | Server -> Mini App |
| 4 | Confirm OAuth (if needed) | User -> Bitrix24 |
| 5 | Exchange code for token, save | Server |
The OAuth callback handling code is standard for all our integrations:
class Bx24OAuthController
{
public function callback(Request $request): Response
{
$code = $request->get('code');
$tgUserId = $request->session()->get('pending_tg_user_id');
$tokenData = $this->exchangeCode($code);
\Local\TgBx24\TokenStorage::save($tgUserId, [
'access_token' => $tokenData['access_token'],
'refresh_token' => $tokenData['refresh_token'],
'expires_at' => time() + $tokenData['expires_in'],
'domain' => $tokenData['domain'],
'user_id' => $tokenData['user_id'],
]);
$this->bot->sendMessage($tgUserId, 'Bitrix24 authorization successful.');
return redirect('/auth/success');
}
private function exchangeCode(string $code): array
{
$response = Http::post('https://oauth.bitrix.info/oauth/token/', [
'grant_type' => 'authorization_code',
'client_id' => config('bx24.client_id'),
'client_secret' => config('bx24.client_secret'),
'code' => $code,
]);
return $response->json();
}
}
Tokens are stored in a separate table or Redis. We use automatic refresh 5 minutes before expiration to ensure users don't lose access. All secrets are encrypted.
class TokenStorage
{
private const TABLE = 'tg_bx24_tokens';
public static function save(int $tgUserId, array $tokenData): void
{
$encrypted = \Local\Crypto::encrypt(json_encode($tokenData));
\Bitrix\Main\Application::getConnection()->queryExecute(
"INSERT INTO " . self::TABLE . " (tg_user_id, token_data, updated_at)
VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE token_data = ?, updated_at = NOW()",
[$tgUserId, $encrypted, $encrypted]
);
}
public static function getValidToken(int $tgUserId): ?array
{
$result = \Bitrix\Main\Application::getConnection()->query(
"SELECT token_data FROM " . self::TABLE . " WHERE tg_user_id = ?",
[$tgUserId]
);
$row = $result->fetch();
if (!$row) return null;
$data = json_decode(\Local\Crypto::decrypt($row['token_data']), true);
if ($data['expires_at'] < time() + 300) {
$data = self::refreshToken($data);
}
return $data;
}
private static function refreshToken(array $data): array
{
$response = \Bitrix\Main\Web\HttpClient::post(
'https://oauth.bitrix.info/oauth/token/',
[
'grant_type' => 'refresh_token',
'client_id' => \Bitrix\Main\Config\Option::get('local.tg_bx24', 'client_id'),
'client_secret' => \Bitrix\Main\Config\Option::get('local.tg_bx24', 'client_secret'),
'refresh_token' => $data['refresh_token'],
]
);
return $newData;
}
}
Why a Mediator Server?
A direct request from the Mini App to the Bitrix24 API is impossible due to CORS and the lack of secure token storage on the client. The mediator server acts as a cryptographic switch: all requests to the REST API go through it, tokens never leave the server. This is the security standard for OAuth in mobile applications.
Real-Time Data Exchange with Webhooks
Bitrix24 allows setting up event webhooks — for example, ONCRMDEALUPDATE. When an event triggers, Bitrix24 sends a POST to our server. The server identifies the responsible person (ASSIGNED_BY_ID), finds their Telegram user_id via the token table, and sends a message via sendMessage. Delay is no more than 2 seconds.
How automatic token refresh works
We store tokens in the `tg_bx24_tokens` table with an `expires_at` field. 5 minutes before expiration, the server automatically requests a new token via refresh_token. If the refresh_token is also expired, the user is prompted to go through OAuth again. All secrets are encrypted.React Mini App: Employee CRM Dashboard
The frontend is built with React using the Telegram WebApp SDK. This allows embedding the interface into a bot button and using native controls (e.g., haptic feedback).
import { useEffect, useState } from 'react';
const tg = window.Telegram.WebApp;
interface Deal {
ID: string;
TITLE: string;
OPPORTUNITY: string;
STAGE_ID: string;
CONTACT_NAME: string;
}
function CrmDashboard() {
const [deals, setDeals] = useState<Deal[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
tg.ready();
tg.expand();
loadDeals();
}, []);
async function loadDeals() {
const res = await fetch('/tg-api/crm/deals', {
headers: { 'X-Tg-Init-Data': tg.initData },
});
const data = await res.json();
setDeals(data.deals);
setLoading(false);
}
async function updateStage(dealId: string, stageId: string) {
await fetch(`/tg-api/crm/deals/${dealId}/stage`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Tg-Init-Data': tg.initData,
},
body: JSON.stringify({ stage_id: stageId }),
});
tg.HapticFeedback.notificationOccurred('success');
loadDeals();
}
// ... render
}
On the server, a proxy class encapsulates calls to the REST API. It automatically fetches the token from storage and handles authorization errors:
class CrmDealsProxy
{
public function getDeals(int $tgUserId): array
{
$tokenData = TokenStorage::getValidToken($tgUserId);
if (!$tokenData) {
throw new \RuntimeException('Not authorized', 401);
}
$bx24 = new \Local\TgBx24\Bx24Client($tokenData['access_token'], $tokenData['domain']);
$result = $bx24->call('crm.deal.list', [
'filter' => ['ASSIGNED_BY_ID' => $tokenData['user_id'], 'CLOSED' => 'N'],
'select' => ['ID', 'TITLE', 'OPPORTUNITY', 'STAGE_ID', 'CONTACT_ID'],
'order' => ['DATE_MODIFY' => 'DESC'],
'start' => 0,
]);
return $result['result'] ?? [];
}
public function updateDealStage(int $tgUserId, int $dealId, string $stageId): bool
{
$tokenData = TokenStorage::getValidToken($tgUserId);
$bx24 = new \Local\TgBx24\Bx24Client($tokenData['access_token'], $tokenData['domain']);
$result = $bx24->call('crm.deal.update', [
'id' => $dealId,
'fields' => ['STAGE_ID' => $stageId],
]);
return !empty($result['result']);
}
}
What's Included in the Work
- Bitrix24 application registration (OAuth), Mini App setup in @BotFather
- Mediator server: initData validation, OAuth token storage and renewal (PHP 8.1, MariaDB)
- React Mini App for the chosen scenario (deal dashboard, requests, tasks)
- REST proxy to Bitrix24 API with automatic token renewal
- Bitrix24 event webhooks for Telegram notifications
- User onboarding: account linking and test launch
- Installation and support documentation
Process and Timelines
We follow a structured approach: data collection → audit/analysis → design → estimation → development → testing → launch.
- MVP for one scenario: from 3 to 5 weeks
- Full-featured application with multiple sections: from 8 to 14 weeks
Cost is determined individually after analyzing the technical specifications. We provide a 30-day guarantee on the integration's functionality. Experience: 10+ years, over 50 integrations with external services. Certified Bitrix developers.
Contact us for a consultation — we'll conduct a free audit of your CRM and propose an architecture that will reduce your sales team's reaction time.







