1C-Bitrix Integration with Firebase Cloud Messaging
Firebase Cloud Messaging (FCM) is Google's infrastructure for push notifications on Android, iOS, and in the browser. Unlike OneSignal, FCM is a low-level transport with no built-in UI or segmentation. The integration with 1C-Bitrix is built entirely on custom code: the frontend SDK registers a device token, and the PHP backend sends notifications via the FCM HTTP v1 API.
FCM HTTP v1 API vs Legacy API
Google shut down the Legacy HTTP API (the server key) in June 2024. All new integrations use the HTTP v1 API with OAuth 2.0 Service Account authorization. If your project still has an old integration via https://fcm.googleapis.com/fcm/send — it is no longer functional.
HTTP v1 endpoint: POST https://fcm.googleapis.com/v1/projects/{project_id}/messages:send
Authorization — a Bearer token obtained from the Service Account JSON via the Google Auth Library.
Service Account and Authorization
In Firebase Console → Project Settings → Service Accounts → Generate new private key. Download the JSON file and place it outside the webroot:
/var/www/site/storage/firebase/service-account.json
Obtain the token via JWT:
use Google\Auth\Credentials\ServiceAccountCredentials;
class FcmAuthService
{
private ServiceAccountCredentials $credentials;
public function __construct(string $serviceAccountPath)
{
$this->credentials = new ServiceAccountCredentials(
'https://www.googleapis.com/auth/firebase.messaging',
json_decode(file_get_contents($serviceAccountPath), true)
);
}
public function getAccessToken(): string
{
$token = $this->credentials->fetchAuthToken();
return $token['access_token'];
}
}
Install the dependency: composer require google/auth. Cache the token — it is valid for 1 hour. Regenerating it on every request is wasteful.
public function getCachedToken(): string
{
$cacheKey = 'fcm_access_token';
$cached = \Bitrix\Main\Data\Cache::createInstance();
if ($cached->initCache(3500, $cacheKey, '/fcm/')) {
return $cached->getVars()['token'];
}
$token = $this->getAccessToken();
$cached->startDataCache();
$cached->endDataCache(['token' => $token]);
return $token;
}
Registering the FCM Token on the Frontend
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';
const app = initializeApp({
apiKey: "...",
authDomain: "project.firebaseapp.com",
projectId: "project-id",
messagingSenderId: "123456789",
appId: "1:123456789:web:abc"
});
const messaging = getMessaging(app);
// Request permission and register token
async function initPush() {
try {
const token = await getToken(messaging, {
vapidKey: 'YOUR_VAPID_KEY'
});
if (token) {
await fetch('/local/api/fcm/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Bitrix-Csrf-Token': BX.bitrix_sessid()
},
body: JSON.stringify({ fcm_token: token, platform: 'web' })
});
}
} catch (err) {
console.warn('Push permission denied:', err);
}
}
// Foreground notification handler (when the tab is open)
onMessage(messaging, (payload) => {
new Notification(payload.notification.title, {
body: payload.notification.body,
icon: '/local/templates/main/images/push-icon.png'
});
});
For background notifications, a Service Worker /firebase-messaging-sw.js is required in the site root:
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js');
firebase.initializeApp({ /* config */ });
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
self.registration.showNotification(payload.notification.title, {
body: payload.notification.body,
data: payload.data,
});
});
Storing FCM Tokens
class FcmTokenTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'local_fcm_tokens'; }
public static function getMap(): array
{
return [
new \Bitrix\Main\ORM\Fields\IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new \Bitrix\Main\ORM\Fields\IntegerField('USER_ID'),
new \Bitrix\Main\ORM\Fields\StringField('TOKEN', ['required' => true]),
new \Bitrix\Main\ORM\Fields\StringField('PLATFORM'), // web, android, ios
new \Bitrix\Main\ORM\Fields\DatetimeField('CREATED_AT'),
new \Bitrix\Main\ORM\Fields\DatetimeField('LAST_USED_AT'),
new \Bitrix\Main\ORM\Fields\StringField('ACTIVE'),
];
}
}
When a token is refreshed (FCM rotates tokens upon app reinstallation) — locate the old token and replace it rather than creating a duplicate record.
Sending Notifications
class FcmService
{
private FcmAuthService $auth;
private string $projectId;
public function sendToUser(int $userId, string $title, string $body, array $data = []): void
{
$tokens = FcmTokenTable::getList([
'filter' => ['USER_ID' => $userId, 'ACTIVE' => 'Y'],
'select' => ['TOKEN', 'PLATFORM'],
])->fetchAll();
foreach ($tokens as $tokenRow) {
$this->sendToToken($tokenRow['TOKEN'], $title, $body, $data, $tokenRow['PLATFORM']);
}
}
private function sendToToken(string $token, string $title, string $body, array $data, string $platform): void
{
$message = [
'token' => $token,
'notification' => ['title' => $title, 'body' => $body],
'data' => array_map('strval', $data), // FCM requires strings
];
// Platform-specific settings
if ($platform === 'android') {
$message['android'] = [
'priority' => 'high',
'notification' => ['channel_id' => 'orders', 'icon' => 'ic_notification'],
];
} elseif ($platform === 'ios') {
$message['apns'] = [
'headers' => ['apns-priority' => '10'],
'payload' => ['aps' => ['sound' => 'default', 'badge' => 1]],
];
}
$accessToken = $this->auth->getCachedToken();
$projectId = $this->projectId;
$url = "https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['message' => $message]),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"Authorization: Bearer {$accessToken}",
],
]);
$response = json_decode(curl_exec($ch), true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 404 || ($response['error']['code'] ?? 0) === 404) {
// Token is stale — deactivate
FcmTokenTable::updateByToken($token, ['ACTIVE' => 'N']);
}
}
}
Topics vs Individual Tokens
FCM supports sending to topics (/topics/promo_electronics) — convenient for bulk messaging without storing tokens. Subscribing to a topic:
POST https://iid.googleapis.com/iid/v1:batchAdd
{
"to": "/topics/promo_electronics",
"registration_tokens": ["TOKEN1", "TOKEN2"]
}
For transactional notifications (orders belonging to a specific user) — individual tokens only.
Timeline
| Task | Duration |
|---|---|
| Service Account, FCM HTTP v1 client, token caching | 2–3 days |
| Token registration (web + android/ios) | 3–4 days |
| Sending on order events + error handling | 2–3 days |
| Service Worker, foreground/background notifications | 2–3 days |
| Subscription management from account, topics | 3–5 days |
| Full scope | 3–4 weeks |







