A Python CLI tool for sending Firebase Cloud Messaging (FCM) notifications using the Firebase Admin SDK.
This project uses Poetry for dependency management and packaging.
Add to your project:
poetry add fcm-sendInstall globally:
pipx install fcm-sendpip install fcm-sendpipx installs the CLI in an isolated environment, avoiding dependency conflicts:
# Install pipx if you don't have it
pip install pipx
# macOS: Using brew
brew install pipx
pipx ensurepath
# Install fcm-send
pipx install fcm-sendgit clone https://github.com/mfdeveloper/firebase_cloud_messaging_cli.git
cd firebase_cloud_messaging_cli
# Install with Poetry (recommended)
poetry install
# Run the CLI
poetry run fcm-send --version- Send push notifications with title, body, and optional custom data
- Send data-only messages (silent notifications for background processing)
- Display service account info and retrieve the access token
- Dry-run mode to validate messages without sending
- Image support for rich notifications
1. Install Poetry
If you don't have Poetry installed:
# macOS / Linux
curl -sSL https://install.python-poetry.org | python3 -
# Alternatively, create and activate virtual environment
python3 -m venv .venv
source .venv/bin/activate
# And using pipx
pipx install poetryNote: After installation, ensure Poetry is in your PATH. See Poetry Installation.
cd <project-root>
# Install dependencies (creates .venv automatically)
poetry install
# Activate the virtual environment
poetry shellNote: Poetry automatically creates a
.venvfolder in your project (configured inpoetry.toml).
To authenticate a service account and authorize it to access Firebase services, you must generate a
service-account.json private key file in JSON format.
To generate and obtain one: a private key file for your service account:
- Go to Firebase Console
- Select your project
- Open Project Settings > Service Accounts.
- Click Generate New Private Key, then confirm by clicking Generate Key.
- Securely store the
.jsonfile containing the key.
For more details, see the Firebase documentation page: Initialize the SDK in non-Google environments
Set the credentials environment variable pointing to your service account JSON file:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your-service-account.jsonTip: Add this to your
~/.zshrcor~/.bashrcfor persistence.
| Variable | Description |
|---|---|
GOOGLE_APPLICATION_CREDENTIALS |
Environment variable pointing to the service account .json file |
ACCESS_TOKEN |
Temporary Firebase access token (retrieved automatically by the SDK) |
FCM_TOKEN |
Device FCM registration token (obtained from mobile app) |
SERVICE_ACCOUNT_EMAIL |
The client_email field from the credentials JSON |
PROJECT_ID |
The project_id field from the credentials JSON |
Before running commands, ensure you're in the Poetry environment:
cd <project-root>
# Option 1: Activate shell
poetry shell
fcm-send --info
# Option 2: Run directly with "poetry run"
poetry run fcm-send --infoNote: Using a code editor (such as VSCode or Cursor) a Python Virtual Environment can be loaded automatically using .vscode/settings.json configurations. Open this folder as
${workspaceFolder}andsettings.jsonfile mentioned above would be loaded!
# Using CLI argument (takes precedence)
fcm-send --credentials-key-file ~/my-service-account.json --info
# Using environment variable
export GOOGLE_APPLICATION_CREDENTIALS=~/my-service-account.json
fcm-send --info
# Combining with other commands
fcm-send --credentials-key-file ~/sa.json --token FCM_TOKEN --title "Hi" --body "Test"Display project details and retrieve the current Firebase Admin SDK access token:
fcm-send --info
# Or use --access-token alias
fcm-send --access-tokenOutput:
============================================================
Firebase Service Account Information
============================================================
PROJECT_ID: your-project-id
SERVICE_ACCOUNT_EMAIL: firebase-adminsdk@your-project.iam.gserviceaccount.com
CREDENTIALS_FILE: /path/to/service-account.json
============================================================
ACCESS_TOKEN (first 50 chars): ya29.c.c0ASRK0GYQ...
ACCESS_TOKEN (full):
ya29.c.c0ASRK0GYQ...
============================================================Alternatively, display project details with current Google OAuth2 access token for using with FCM HTTP API access token. You can use it to perform a curl or any other way for HTTP requests on FCM API:
fcm-send --info-http
# Or use --access-token-http alias
fcm-send --access-token-httpfcm-send --token YOUR_FCM_TOKEN --title "Hello" --body "World"fcm-send --token YOUR_FCM_TOKEN \
--title "Order Update" \
--body "Your order has been shipped!" \
--data '{"order_id": "12345", "action": "open_order"}'fcm-send --token YOUR_FCM_TOKEN \
--title "Check this out" \
--body "New feature available!" \
--image "https://example.com/image.png"Data-only messages don't show a visible notification but are delivered to your app for background processing:
fcm-send --token YOUR_FCM_TOKEN \
--data-only '{"action": "sync", "resource_id": "123"}'Test your message configuration without actually sending it:
fcm-send --token YOUR_FCM_TOKEN \
--title "Test" \
--body "This is a test" \
--dry-runYou can also use fcm-send as a library in your Python code:
from fcm_send import FCMClient
# Initialize client with credentials file
client = FCMClient("/path/to/service-account.json")
# Send a notification
response = client.send_notification(
fcm_token="device_fcm_token",
title="Hello",
body="World",
data={"key": "value"} # optional
)
print(f"Message ID: {response}")
# Send a data-only message
response = client.send_data_message(
fcm_token="device_fcm_token",
data={"action": "sync", "id": "123"}
)| Option | Description |
|---|---|
--credentials-key-file |
Path to Firebase service account .json file (CLI argument, takes over $GOOGLE_APPLICATION_CREDENTIALS) |
--info |
Display service account info and access token |
--info-http |
Display service account info and Google OAuth2 access token for FCM HTTP API |
--access-token |
Alias for --info |
--access-token-http |
Alias for --info-http |
--token <FCM_TOKEN> |
FCM registration token of the target device |
--title TEXT |
Notification title |
--body TEXT |
Notification body |
--data <JSON> |
Custom data payload as JSON string (e.g --data '{"action": "sync" }') |
--data-only <JSON> |
Send data-only message (no visible notification) |
--image URL |
Image URL for rich notifications |
--dry-run |
Validate message without sending |
To send notifications, you need the FCM registration token from your mobile app. In Android:
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
Log.d("FCM", "Token: $token")
}
}Reference: FCM: Retrieve the current registration token
Make sure you've exported the environment variable:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.jsonReference: Firebase Admin SDK: Initialize in non-Google environments
This error occurs when:
- The app has been uninstalled from the device
- The token has expired or been rotated
- The token was generated for a different Firebase project
The FCM token was generated for a different Firebase project. Make sure you're using:
- The correct service account JSON for your project
- An FCM token generated by an app configured with the same Firebase project
The script is organized into two main classes:
Handles all Firebase operations:
credentials_path- Get credentials file path from environmentcredentials_info- Load/cache credentials JSONproject_id- Get project ID from credentialsservice_account_email- Get service account emailinitialize()- Initialize Firebase Admin SDKget_access_token()- Retrieve current access tokenshow_info()- Display service account informationsend_notification()- Send FCM notificationsend_data_message()- Send data-only message
Handles command-line interface:
create_parser()- Configure argument parserhandle_info()- Process--infocommandhandle_data_only()- Process--data-onlycommandhandle_notification()- Process notification commandrun()- Main CLI execution logic
firebase-cloud-messaging/
├── src/
│ └── fcm_send/
│ ├── __init__.py # Package exports (FCMClient, main, etc.)
│ ├── __main__.py # Enables `python -m fcm_send`
│ ├── __version__.py # Version from pyproject.toml ✨
│ ├── client.py # FCMClient class
│ ├── cli.py # CLIHandler and main() entry point
│ └── py.typed # PEP 561 marker for type checking
├── tests/ # Test suite (58 tests)
├── pyproject.toml # Poetry configuration
├── poetry.toml # Poetry settings (in-project venv)
├── poetry.lock # Locked dependencies
├── LICENSE # MIT License
└── README.md # This file
git clone https://github.com/mfdeveloper/firebase_cloud_messaging_cli.git
cd firebase_cloud_messaging_cli
# Install all dependencies (creates .venv automatically)
poetry install
# Activate the virtual environment
poetry shell
# Or run commands directly
poetry run fcm-send --version
poetry run pytestgit clone https://github.com/mfdeveloper/firebase_cloud_messaging_cli.git
cd firebase_cloud_messaging_cli
# Create and activate virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install from requirements.txt
pip install -r requirements.txt
# Or install the package in editable mode
pip install -e .The project includes a comprehensive test suite with 58 unit tests achieving 97% code coverage.
# Run all tests
poetry run pytest
# Run with coverage terminal report
poetry run pytest --cov=src/fcm_send --cov-report=term-missing
# Run tests with coverage (HTML report)
poetry run pytest --cov=src/fcm_send --cov-report=htmlFor detailed testing documentation, including test breakdown, fixtures, and mocking strategy, see:
This project uses Pylint for static code analysis. The configuration is defined in .pylintrc.
# Lint source package only
poetry run pylint src/fcm_send/
# Lint source package and tests
poetry run pylint src/fcm_send/ tests/
# Lint tests only
poetry run pylint tests/The .pylintrc file includes project-specific settings:
- Max line length: 120 characters
- Extended test names: Supports long descriptive test method names
- Disabled rules: Common pytest patterns (unused fixtures, redefined outer names) and CLI patterns (broad exception catching)
To check your current score:
# Output: Your code has been rated at X.XX/10
poetry run pylint src/fcm_send/ tests/This project uses Poetry for building and publishing packages.
Update the version in pyproject.toml:
[tool.poetry]
version = "0.2.0"Or use Poetry's version command:
# Bump patch version (0.1.0 → 0.1.1)
poetry version patch
# Bump minor version (0.1.0 → 0.2.0)
poetry version minor
# Bump major version (0.1.0 → 1.0.0)
poetry version major# Build both sdist and wheel
poetry buildThis creates distribution files in the dist/ directory:
dist/fcm_send-0.1.0.tar.gz(source distribution)dist/fcm_send-0.1.0-py3-none-any.whl(wheel)
Test your package on Test PyPI before publishing to the real PyPI.
This project includes a GitHub Actions workflow for automated publishing to TestPyPI.
Triggers:
- Manual: Go to Actions → "Publish to TestPyPI" → "Run workflow"
- Automatic: Push a version tag (e.g.,
git tag v0.1.0 && git push --tags)
One-time Setup:
- Create a TestPyPI account
- Generate an API token at TestPyPI API Tokens
- Add the token as a GitHub secret:
- Go to your repository Settings → Secrets and variables → Actions
- Create a new secret named
TEST_PYPI_API_TOKENwith your token
# Configure TestPyPI repository
poetry config repositories.testpypi https://test.pypi.org/legacy/
# Set your TestPyPI token
poetry config pypi-token.testpypi pypi-XXXXXXXXXXXX
# Publish to TestPyPI
poetry publish --repository testpypi
# Alternatively, you can use regular "Twine" for publishing
twine upload --repository testpypi dist/*
# Test installation from TestPyPI
pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
fcm-sendNote: The
--extra-index-urlis needed because TestPyPI doesn't have all dependencies (likefirebase-admin), so it falls back to the real PyPI for those.
Once verified on TestPyPI, publish to the official PyPI.
This project includes a GitHub Actions workflow for automated publishing to PyPI.
Trigger:
- Push a
pypi-*tag:git tag -a pypi-0.1.0 -m "Release version 0.1.0" git push origin pypi-0.1.0
One-time Setup:
- Create a PyPI account
- Generate an API token at PyPI API Tokens
- Add the token as a GitHub secret:
- Go to your repository Settings → Secrets and variables → Actions
- Create a new secret named
PYPI_API_TOKENwith your token
# Set your PyPI token
poetry config pypi-token.pypi pypi-XXXXXXXXXXXX
# Publish to PyPI
poetry publish
# Alternatively, you can use regular "Twine" for publishing
twine upload dist/*Users can install your package with:
poetry add fcm-send
# or
pip install fcm-send| Command | Description |
|---|---|
poetry install |
Install all dependencies |
poetry install --extras dev |
Install with dev dependencies |
poetry shell |
Activate the virtual environment |
poetry run <cmd> |
Run a command in the virtual environment |
poetry add <pkg> |
Add a dependency |
poetry add --group dev <pkg> |
Add a dev dependency |
poetry remove <pkg> |
Remove a dependency |
poetry update |
Update dependencies to latest versions |
poetry lock |
Update poetry.lock without installing |
poetry build |
Build sdist and wheel |
poetry publish |
Publish to PyPI |
poetry version <rule> |
Bump version (patch, minor, major) |
poetry show |
Show installed packages |
poetry env info |
Show virtualenv info |