DNZ Digita
← Back to Push docs

Complete guide

Complete Push Notifications Integration Guide

This guide explains everything in order. If you follow the steps exactly, your app will receive notifications. Read the section that matches your platform (website, Android, or iOS).

Last updated: 10 July 2026

1. Before you start — the 3 things you need

DNZ Notifications works like a post office: you tell us who the user is (recipientId), you register their device once, then you send messages to that user.

Production API base URL (copy from console): https://dnzteam.online/notifications-api

Dashboard console: https://dnzteam.online/console/notifications

Important

Uploading Firebase service_account.json in the console does NOT register phones. It only lets DNZ send through Firebase. Your app must still call /v1/subscribers/register with the device FCM token.

What you needWhere to get itUsed for
API base URLConsole → API base URL boxAll API calls from app/server
API Key (dnz_live_…)Console → Create keyAuthentication header X-API-Key
recipientIdYou choose (e.g. user_12345)Same ID in register + send

2. Dashboard setup (5 minutes)

1Sign in to the console

Open https://dnzteam.online/console/notifications and sign in with your DNZ account.

  • Account created
  • Logged in to Push Notifications workspace

2Copy API base URL and create API key

Copy the API base URL shown at the top of the console.

Click Create key and save the full dnz_live_… key — it is shown only once.

3Mobile only: upload Firebase (Android) or APNs (iOS)

Android: upload service_account.json from Firebase Console → Project settings → Service accounts.

iOS: fill Key ID, Team ID, Bundle ID and upload the .p8 key from Apple Developer.

Website only: skip this — use VAPID keys instead (section 8).

Tip

FCM test in console shows OAuth OK — that means server credentials work. It does not mean your phone is registered yet.

3. Core concepts everyone must understand

recipientId — a stable ID for each user in your app (example: user_07811098146). Use the same value when registering the device and when sending notifications.

Register device — your app calls POST /v1/subscribers/register once per device (and again when FCM token changes).

Send — your server or the console calls POST /v1/send with the same recipientId.

queued — the API accepted your message; delivery happens in the background.

failed — delivery could not reach any registered device (see error table in section 12).

Note

VAPID is for browser Web Push only. Android apps do not need VAPID. An empty VAPID list in the console is normal for mobile-only projects.

4. Firebase setup — full checklist (Android)

Android push through DNZ uses your own Firebase project. DNZ does not provide Firebase for you — you create the project, connect your app, and upload server credentials to the DNZ console.

Note

After adding SHA fingerprints, uninstall the app from the test device, rebuild, reinstall, and call register again so a fresh FCM token is generated.

1Create one Firebase project for your app

Go to Firebase Console and create a project (or use an existing one).

Add an Android app with the exact package name from android/app/build.gradle (applicationId).

Download google-services.json and place it in android/app/.

  • Package name in Firebase matches applicationId in the app
  • google-services.json is in android/app/ (not android/ root)

2Same Firebase project for both files (critical)

Your app uses google-services.json — this links the phone to Firebase project A.

DNZ console uses service_account.json — this must come from the same Firebase project A.

If the app uses project A but you upload service_account from project B, FCM test may show OK but pushes will not reach the device.

Important

Open both JSON files and compare project_id — they must match exactly.

// google-services.json (inside your Android app)
"project_id": "my-shop-app"

// service_account.json (uploaded in DNZ console only)
"project_id": "my-shop-app"

// Both project_id values MUST be identical.

3Generate service_account.json for DNZ console

Firebase Console → Project settings → Service accounts → Generate new private key.

Upload the downloaded JSON in DNZ console (Push Notifications → Firebase / FCM).

Never put this file inside your mobile app or commit it to public Git.

4Enable Cloud Messaging

Firebase Cloud Messaging is enabled by default on new projects.

If you use Google Cloud Console separately, ensure Firebase Cloud Messaging API is enabled for the same project.

You do not need to create campaigns or topics in Firebase — DNZ sends directly to device tokens.

5Add SHA-1 and SHA-256 fingerprints (required for production)

Firebase needs your app signing fingerprints to issue valid FCM tokens, especially for release builds published to Google Play.

Add both SHA-1 and SHA-256 for debug (development) and release (production).

Firebase Console → Project settings → Your apps → Android app → Add fingerprint.

Tip

If getToken() returns null on a release APK but works in debug, you almost always forgot the release SHA fingerprints in Firebase.

6Get debug SHA (development builds)

Run signingReport from the android folder of your Flutter/Android project.

Copy SHA-1 and SHA-256 from Variant: debug and paste them into Firebase.

# From your Flutter project root (Windows)
cd android
gradlew.bat signingReport

# Look under "Variant: debug" for SHA-1 and SHA-256 — add both to Firebase.
# For release builds, also check "Variant: release" if you already configured signing.

7Get release SHA (production / Play Store)

If you sign release builds locally: run keytool on your upload keystore (see command below).

If you use Google Play App Signing: open Play Console → Your app → Setup → App integrity → App signing key certificate — copy SHA-1 and SHA-256 there too.

Add every release fingerprint to Firebase before publishing or testing a signed release APK/AAB.

  • Release SHA-1 added in Firebase
  • Release SHA-256 added in Firebase
  • Play App Signing SHA added if Play re-signs your app
  • Re-download google-services.json only if Firebase prompts you (usually not required after adding SHA)
# Production / release keystore (replace paths and alias)
keytool -list -v -keystore C:\path\to\upload-keystore.jks -alias upload

# Copy SHA-1 and SHA-256 from the output.
# Password: your keystore password (not stored anywhere by DNZ).
PlatformFirebase required?What you configure
Android appYesgoogle-services.json in app + service_account.json in DNZ console
iOS app (APNs direct)NoAPNs .p8 in DNZ console (section 7)
Website (Web Push)NoVAPID keys in DNZ console (section 8)

5. Android app — Flutter (recommended)

1Add Firebase to your Flutter app

Complete section 4 first (Firebase project, google-services.json, SHA fingerprints, service_account in DNZ console).

Download google-services.json into android/app/.

Add firebase_messaging to pubspec.yaml.

2Add DNZ Flutter SDK (copy from this guide)

No download link — copy the code blocks below directly into your project.

1) Add the pubspec.yaml dependencies.

2) Create lib/dnz_notifications.dart and paste the full Dart source.

3) Add Android permissions in AndroidManifest.xml.

4) Run flutter pub get.

3Initialize and register after login

Run initialize once, then registerDevice with the same recipientId you will use when sending.

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:dnz_notifications/dnz_notifications.dart';

// 1) Once at app start
await DNZNotifications.instance.initialize(
  apiKey: 'dnz_live_YOUR_KEY',
  apiBase: 'https://dnzteam.online/notifications-api',
);

// 2) After user login — same recipientId everywhere
final fcmToken = await FirebaseMessaging.instance.getToken();
await DNZNotifications.instance.registerDevice(
  recipientId: 'user_12345',
  fcmToken: fcmToken,
);
await DNZNotifications.instance.startBackground();

// 3) When FCM token refreshes
FirebaseMessaging.instance.onTokenRefresh.listen((token) async {
  await DNZNotifications.instance.registerDevice(
    recipientId: 'user_12345',
    fcmToken: token,
  );
});

4Send a test from console

Open the app on a real device, log in, and confirm registerDevice succeeded.

In the console Test send box, use the exact same recipientId and your API key.

Success tip

If registration worked, delivery log should show sent instead of failed.

pubspec.yaml (dependencies)
# Add under dependencies: in pubspec.yaml
  firebase_messaging: ^15.0.0
  http: ^1.2.0
  web_socket_channel: ^2.4.0
  flutter_local_notifications: ^17.0.0
  shared_preferences: ^2.2.0
lib/dnz_notifications.dart (full file — copy all)
/// DNZ Notifications — Flutter SDK
/// ----------------------------------------
/// مكتبة Flutter لربط تطبيقك بمنصة DNZ:
///   * تسجيل الجهاز مع connectionId فريد
///   * فتح WebSocket في الخلفية لاستقبال الإشعارات حتى عند إغلاق التطبيق
///   * عرض إشعارات النظام محلياً عند وصول رسالة
///   * heartbeat تلقائي كل 30 ثانية
///
/// المتطلبات (في pubspec.yaml):
///   http, web_socket_channel, flutter_local_notifications,
///   flutter_background_service, shared_preferences
///
/// استخدام أساسي في main.dart:
///   await DNZNotifications.instance.initialize(
///     apiKey: 'dnz_live_...',
///     apiBase: 'https://dnzteam.online/notifications-api',
///   );
///   await DNZNotifications.instance.registerDevice(recipientId: userId);
///   await DNZNotifications.instance.startBackground();
///
/// المصدر الكامل مضمّن في الدليل:
///   https://dnzteam.online/push-notifications/docs/complete-guide (القسم 4)
library dnz_notifications;

import 'dart:async';
import 'dart:convert';
import 'dart:io' show Platform;
import 'dart:math';

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

typedef DNZMessageHandler = void Function(Map<String, dynamic> message);

class DNZException implements Exception {
  final String message;
  final int? statusCode;
  DNZException(this.message, [this.statusCode]);
  @override
  String toString() => 'DNZException($statusCode): $message';
}

class DNZNotifications {
  DNZNotifications._();
  static final DNZNotifications instance = DNZNotifications._();

  late String _apiKey;
  late String _apiBase;
  String? _connectionId;
  WebSocketChannel? _channel;
  StreamSubscription? _wsSub;
  Timer? _heartbeat;
  bool _shouldReconnect = true;
  final List<DNZMessageHandler> _handlers = [];
  final FlutterLocalNotificationsPlugin _local = FlutterLocalNotificationsPlugin();

  bool _initialized = false;

  static const String _androidChannelId = 'dnz_channel_v3';
  // صوت إشعار النظام الافتراضي على أندرويد
  static const UriAndroidNotificationSound _defaultAndroidSound =
      UriAndroidNotificationSound('content://settings/system/notification_sound');

  Future<void> initialize({
    required String apiKey,
    required String apiBase,
  }) async {
    if (_initialized) return;
    if (apiKey.isEmpty) throw ArgumentError('apiKey مطلوب');
    _apiKey = apiKey;
    _apiBase = apiBase.endsWith('/')
        ? apiBase.substring(0, apiBase.length - 1)
        : apiBase;

    const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
    const iosInit = DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
    );
    await _local.initialize(
      const InitializationSettings(android: androidInit, iOS: iosInit),
    );

    final androidPlugin = _local.resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>();
    await androidPlugin?.createNotificationChannel(
      const AndroidNotificationChannel(
        _androidChannelId,
        'DNZ Notifications',
        description: 'إشعارات DNZ بصوت النظام الافتراضي',
        importance: Importance.high,
        playSound: true,
        enableVibration: true,
        sound: _defaultAndroidSound,
      ),
    );

    final prefs = await SharedPreferences.getInstance();
    _connectionId = prefs.getString('dnz_connection_id') ?? _generateConnectionId();
    await prefs.setString('dnz_connection_id', _connectionId!);
    _initialized = true;
  }

  String get connectionId => _connectionId ?? '';

  void onMessage(DNZMessageHandler handler) => _handlers.add(handler);

  String _generateConnectionId() {
    final rand = Random.secure();
    final bytes = List<int>.generate(16, (_) => rand.nextInt(256));
    return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
  }

  /// [fcmToken] من FirebaseMessaging (أندرويد) — [apnsToken] من FirebaseMessaging أو APNs (آيفون).
  /// الموجّه الهجين يستخدمها عند إغلاق التطبيق بدون استهلاك بطارية إضافية.
  Future<Map<String, dynamic>> registerDevice({
    required String recipientId,
    String? fcmToken,
    String? apnsToken,
  }) async {
    _ensureInit();
    final platform = Platform.isIOS ? 'ios' : 'android';
    final body = <String, dynamic>{
      'recipientId': recipientId,
      'platform': platform,
      'deviceTransport': 'mobile_background',
      'connectionId': _connectionId,
    };
    if (fcmToken != null && fcmToken.isNotEmpty) body['nativeFcmToken'] = fcmToken;
    if (apnsToken != null && apnsToken.isNotEmpty) body['nativeApnsToken'] = apnsToken;
    final res = await http.post(
      Uri.parse('$_apiBase/v1/subscribers/register'),
      headers: _headers(),
      body: jsonEncode(body),
    );
    return _decode(res);
  }

  Future<void> startBackground() async {
    _ensureInit();
    _shouldReconnect = true;
    await _connectWs();
    _heartbeat?.cancel();
    _heartbeat = Timer.periodic(const Duration(seconds: 30), (_) => _sendHeartbeat());
  }

  Future<void> stopBackground() async {
    _shouldReconnect = false;
    _heartbeat?.cancel();
    _heartbeat = null;
    await _wsSub?.cancel();
    _wsSub = null;
    await _channel?.sink.close();
    _channel = null;
  }

  final Set<String> _seenMessageIds = {};

  Future<void> _connectWs() async {
    await _wsSub?.cancel();
    _wsSub = null;
    try {
      await _channel?.sink.close();
    } catch (_) {}
    _channel = null;

    final wsBase = _apiBase
        .replaceFirst('https://', 'wss://')
        .replaceFirst('http://', 'ws://');
    final url = Uri.parse(
      '$wsBase/v1/ws/background'
      '?connectionId=$_connectionId&apiKey=$_apiKey',
    );
    try {
      _channel = WebSocketChannel.connect(url);
      _wsSub = _channel!.stream.listen(
        _onWsData,
        onError: (_) => _scheduleReconnect(),
        onDone: _scheduleReconnect,
        cancelOnError: true,
      );
    } catch (_) {
      _scheduleReconnect();
    }
  }

  void _onWsData(dynamic data) {
    if (data is! String) return;
    try {
      final msg = jsonDecode(data) as Map<String, dynamic>;
      final msgId = msg['id']?.toString();
      if (msgId != null && msgId.isNotEmpty) {
        if (_seenMessageIds.contains(msgId)) return;
        _seenMessageIds.add(msgId);
        if (_seenMessageIds.length > 200) {
          _seenMessageIds.remove(_seenMessageIds.first);
        }
      }
      _showLocal(msg);
      for (final h in _handlers) {
        try { h(msg); } catch (_) {}
      }
    } catch (_) {}
  }

  void _scheduleReconnect() {
    _wsSub = null;
    _channel = null;
    if (!_shouldReconnect) return;
    Future.delayed(const Duration(seconds: 3), _connectWs);
  }

  Future<void> _sendHeartbeat() async {
    try {
      await http.post(
        Uri.parse('$_apiBase/v1/devices/heartbeat'),
        headers: _headers(),
        body: jsonEncode({'connectionId': _connectionId}),
      );
    } catch (_) {}
  }

  Future<void> _showLocal(Map<String, dynamic> msg) async {
    const channel = AndroidNotificationDetails(
      _androidChannelId,
      'DNZ Notifications',
      channelDescription: 'إشعارات DNZ بصوت النظام الافتراضي',
      importance: Importance.high,
      priority: Priority.high,
      playSound: true,
      enableVibration: true,
      sound: _defaultAndroidSound,
    );
    const ios = DarwinNotificationDetails(
      presentAlert: true,
      presentBadge: true,
      presentSound: true,
    );
    await _local.show(
      DateTime.now().millisecondsSinceEpoch.remainder(1 << 31),
      msg['title']?.toString() ?? 'DNZ',
      msg['body']?.toString() ?? '',
      const NotificationDetails(android: channel, iOS: ios),
      payload: jsonEncode(msg['data'] ?? {}),
    );
  }

  Map<String, String> _headers() => {
        'Content-Type': 'application/json',
        'X-API-Key': _apiKey,
      };

  Future<Map<String, dynamic>> _decode(http.Response res) async {
    Map<String, dynamic> body = {};
    try { body = jsonDecode(res.body) as Map<String, dynamic>; } catch (_) {}
    if (res.statusCode >= 400) {
      throw DNZException(body['detail']?.toString() ?? 'HTTP ${res.statusCode}', res.statusCode);
    }
    return body;
  }

  void _ensureInit() {
    if (!_initialized) {
      throw StateError('DNZNotifications: استدعِ initialize() أولاً');
    }
  }
}
AndroidManifest.xml
<!-- android/app/src/main/AndroidManifest.xml (before <application>) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
API example
POST https://dnzteam.online/notifications-api/v1/subscribers/register
Headers:
  Content-Type: application/json
  X-API-Key: dnz_live_YOUR_KEY

Body:
{
  "recipientId": "user_12345",
  "platform": "android",
  "deviceTransport": "mobile_background",
  "connectionId": "unique-device-id-abc",
  "nativeFcmToken": "fcm-token-from-firebase-on-device"
}

6. Android app — Kotlin / Java / React Native

1Get FCM token on the device

Complete Firebase setup in section 4 (same project_id, SHA fingerprints for production).

Use Firebase Messaging on the device. Never put service_account.json inside the mobile app.

2Register with DNZ API

POST /v1/subscribers/register with platform android, deviceTransport mobile_background, connectionId, and nativeFcmToken.

val apiBase = "https://dnzteam.online/notifications-api"
val apiKey = "dnz_live_YOUR_KEY"
val recipientId = "user_12345"
val fcmToken = FirebaseMessaging.getInstance().token.await()
val connectionId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)

val json = JSONObject()
  .put("recipientId", recipientId)
  .put("platform", "android")
  .put("deviceTransport", "mobile_background")
  .put("connectionId", connectionId)
  .put("nativeFcmToken", fcmToken)

// POST to $apiBase/v1/subscribers/register with header X-API-Key
API example
POST https://dnzteam.online/notifications-api/v1/subscribers/register
Headers:
  Content-Type: application/json
  X-API-Key: dnz_live_YOUR_KEY

Body:
{
  "recipientId": "user_12345",
  "platform": "android",
  "deviceTransport": "mobile_background",
  "connectionId": "unique-device-id-abc",
  "nativeFcmToken": "fcm-token-from-firebase-on-device"
}

7. iOS app — APNs

1Configure APNs in Apple Developer

Create an APNs Auth Key (.p8), note Key ID and Team ID.

Use your app Bundle ID from Xcode.

2Upload credentials in DNZ console

Fill Key ID, Team ID, Bundle ID, upload .p8, enable Sandbox for development builds.

3Register device from the app

Get APNs/FCM device token on iOS, then call /v1/subscribers/register with platform ios and nativeApnsToken.

API example
POST https://dnzteam.online/notifications-api/v1/subscribers/register
Headers:
  Content-Type: application/json
  X-API-Key: dnz_live_YOUR_KEY

Body:
{
  "recipientId": "user_12345",
  "platform": "ios",
  "deviceTransport": "mobile_background",
  "connectionId": "unique-device-id-abc",
  "nativeApnsToken": "apns-token-from-device"
}

8. Website — Web Push (browser)

1Create VAPID key in console

Click Create VAPID in the console. Put the public key in your frontend JavaScript.

2Service worker + permission

Register a service worker on your domain.

Ask notification permission after a user action (not on first page load).

Subscribe with pushManager.subscribe using the VAPID public key.

3Register browser subscription

Send the subscription JSON to /v1/subscribers/register with deviceTransport web_push.

API example
POST https://dnzteam.online/notifications-api/v1/subscribers/register
Headers:
  Content-Type: application/json
  X-API-Key: dnz_live_YOUR_KEY

Body:
{
  "recipientId": "user_12345",
  "platform": "web",
  "deviceTransport": "web_push",
  "webPush": {
    "endpoint": "https://fcm.googleapis.com/fcm/send/...",
    "keys": {
      "p256dh": "BASE64_PUBLIC_KEY",
      "auth": "BASE64_AUTH_SECRET"
    }
  }
}

9. Sending from your backend (production)

Never expose dnz_live_… keys in mobile apps if you can avoid it — for register calls a client key is acceptable; for mass sending use your server.

Always send from backend in production so you control who receives notifications.

When the Android app is fully closed, the system shows the FCM notification payload. To play your custom channel sound, pass: - androidChannelId → mapped to android.notification.channel_id (must match the channel created in the app) - androidSound → mapped to android.notification.sound (res/raw file name without extension, or "default") You can also put the same keys inside data (androidChannelId / androidSound, or channel_id / sound). Those meta keys are used for FCM Android config and are not forwarded as normal data fields. Important (Android 8+): once a NotificationChannel exists, its sound is fixed at creation time. FCM cannot override a silent channel. If the channel was created without sound, create a NEW channel id (e.g. vcenter_push_v3) with DEFAULT notification sound, update Manifest + androidChannelId, then reinstall or clear app data.

API example
POST https://dnzteam.online/notifications-api/v1/send
Headers:
  Content-Type: application/json
  X-API-Key: dnz_live_YOUR_KEY

Body:
{
  "recipientId": "user_12345",
  "channel": "push",
  "title": "Hello",
  "body": "Your notification text",
  "androidChannelId": "vcenter_push_v1",
  "androidSound": "vcenter_notify",
  "data": {
    "orderId": "123"
  }
}

10. Console test send — checklist

Important

Common mistake: uploading Firebase JSON and sending from console without ever calling register from the app. Error: recipient_not_found.

1Prerequisites

If any item is missing, you will see queued then failed in the delivery log.

  • Device registered via /v1/subscribers/register (check your app logs)
  • recipientId in test box matches register exactly (including user_ prefix)
  • API key pasted in test box
  • For Android: FCM configured in console + nativeFcmToken sent at register
  • For Android custom sound when app is closed: set androidChannelId + androidSound in Test send (or /v1/send)
  • For Android release: SHA-1 and SHA-256 added in Firebase (section 4)
  • google-services.json and service_account.json share the same project_id

11. All scenarios — what happens

ScenarioWhat to configureHow delivery works
Android app closedFCM + register + androidChannelId/androidSoundSystem FCM notification with your channel sound
Android app openregister + startBackground WebSocketInstant via WebSocket; FCM when offline
iOS app closedAPNs in console + register with nativeApnsTokenDNZ sends via Apple APNs
Website Chrome/FirefoxVAPID + web_push registerWeb Push via browser push service
Same user, phone + laptopSame recipientId on both devicesOne send reaches all registered devices
Console test onlyApp must register firstTest send targets recipientId you registered

12. Errors and fixes

SymptomLikely causeFix
queued then failedNo subscriber for recipientIdCall /v1/subscribers/register from app with same recipientId
recipient_not_foundDevice never registeredRun registerDevice after login; check app network logs
no_active_devicesSubscriber exists but no deviceRegister again with valid FCM/APNs token
fcm_not_configuredAndroid token but no Firebase on serverUpload service_account.json in console
FCM OK but push failsMismatched Firebase projectsgoogle-services.json and service_account.json must have same project_id
FCM token is nullMissing SHA or wrong package nameAdd SHA-1/SHA-256 in Firebase; match applicationId
Works in debug, not releaseRelease SHA not in FirebaseAdd release + Play App Signing SHA fingerprints (section 4)
Push arrives closed-app but no custom soundMissing androidChannelId / androidSound on sendPass channel id + raw sound name in /v1/send or Test send
Push arrives closed-app, fields present, still silentAndroid channel created without sound (Oreo+ locks channel sound)Create a NEW channel id with setSound(DEFAULT); uninstall/reinstall or clear app data
No active VAPID keysWeb push without VAPIDCreate VAPID key in console
FCM OK but push failsWrong recipientId in testMatch user_ prefix exactly
Success rate 0%All sends failed deliveryFix registration first, then retest

13. Security rules

Keep service_account.json and .p8 keys only on DNZ console — never inside the mobile app.

Rotate API keys if leaked. Use one key per app/environment when possible.

recipientId should map to your real user ID — do not use random IDs that change every session.

Quick FAQ

Is uploading Firebase enough?

No. Firebase JSON configures the server. Your app must still register the device FCM token with /v1/subscribers/register.

Does /v1/send support androidChannelId and androidSound?

Yes. Pass androidChannelId and androidSound (or the same keys inside data). DNZ maps them to FCM android.notification.channel_id and android.notification.sound. Required for custom sound when the Android app is fully closed.

Why does console show queued but log says failed?

queued means the API accepted the job. failed means no registered device matched that recipientId or push delivery failed.

Do I need VAPID for Android?

No. VAPID is only for browser Web Push.

Can I use the same API key in the app?

Yes for register calls. For sending to many users, use your backend with the API key on the server.

Where is dnz_notifications.dart?

The full Flutter SDK source is embedded in section 5 of this guide (pubspec.yaml, lib/dnz_notifications.dart, AndroidManifest). Copy the code — there is no separate download file.

Must google-services.json and service_account.json be from the same project?

Yes. Both files must have the same project_id. The app gets FCM tokens from one Firebase project; DNZ must send using credentials from that exact project.

Why add SHA-1 and SHA-256 in Firebase?

Firebase uses your app signing fingerprints to trust your app. Without release SHA values, getToken() may fail on production builds even if debug works.

Next step

Open the console, create an API key, then register the device from your app before any test send.

Start in console
Support