Expose birthdays from Google Contacts as a live, subscribable ICS calendar feed.
This project:
- reads birthdays via the Google People API
- generates an iCalendar (ICS) feed
- serves it over HTTP
- works with personal Google accounts (Gmail) and Workspace
- is free (no billing, no paid APIs)
Typical use cases:
- subscribe to contact birthdays in Apple Calendar, Outlook, Nextcloud
- replace the unreliable internal Google “Birthdays” calendar
- keep full control over your data
- ✅ Live ICS feed (calendar subscription, not one-time export)
- ✅ No Google Workspace required
- ✅ Free Google APIs
- ✅ OAuth handled safely
- ✅ Designed for servers (including headless systems)
- ✅ Minimal dependencies
- ❌ No FastAPI / uvicorn based
- Python 3.9+
- A Google account
- Internet access
Install dependencies:
pip install -r requirements.txtor as an alternative use a python virtual environment
sudo apt install python3-venv
./create.venv.sh
source .venv/bin/activateThere are two different files involved:
| File | Purpose | Created by |
|---|---|---|
credentials.json |
OAuth client (app identity) | You, via Google Cloud Console |
token.json |
OAuth user token (login result) | Script, after first authorization |
The script cannot create credentials.json, but will create token.json automatically.
- Open Google Cloud Console
- Create a new project (any name)
No billing account needed.
- Go to APIs & Services → Library
- Search for People API
- Click Enable
- Go to APIs & Services → OAuth consent screen
- User type:
- External
- App name: anything (e.g. Contacts Birthdays)
- Scopes:
- leave empty (People API is added implicitly)
- Save
While still on the consent screen:
- Scroll to Test users
- Add your Google account email
- Save
This is required. Otherwise Google will block login with
“app is being tested”.
- Go to APIs & Services → Credentials
- Click Create credentials → OAuth client ID
- Application type:
- Desktop app
- Name: anything
- Create
- Download the JSON
- Rename it to:
credentials.json
- Place it next to the script.
Start the server:
python3 birthdays_server.pyOn first run only:
- Open stated URL in a browser window on the same machine
- Log in with your Google account
- Approve access
The script will:
- create
token.json - store it locally
- reuse it on future starts
- refresh it automatically
You should see log output like:
OAuth authorization completed (startup)
Saved token.json (startup)
Google People API service ready
- Run the script once on a machine with a browser
- Let it create
token.json - Copy both files to the server:
scp credentials.json token.json user@server:/path/to/app/- Start the server on the headless system:
python3 birthdays_server.pyAs long as token.json exists, no UI is needed anymore.
This is a recommended setup for long-running servers. Systemd service running as a dedicated users and files in the home folder of this user.
Adjust paths if needed:
/home/google-birthdays/
├── birthdays_server.py
├── credentials.json
├── token.json
├── requirements.txt
├── run_birthdays_server.sh
└── .venv/
sudo useradd --system --home /home/google-birthdays --shell /sbin/nologin googlebirthdays
sudo mkdir /home/google-birthdays
sudo chown -R googlebirthdays:googlebirthdays /home/google-birthdaysThe service must run as the same user that owns
token.json, otherwise token refresh may fail.
Create:
sudo nano /etc/systemd/system/google-birthdays.servicePaste:
[Unit]
Description=Google Contacts Birthdays ICS Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=googlebirthdays
Group=googlebirthdays
WorkingDirectory=/home/google-birthdays
ExecStart=bash -c /home/google-birthdays/run_birthdays_server.sh
Restart=on-failure
RestartSec=5
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/home/google-birthdays
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable google-birthdays
sudo systemctl start google-birthdaysCheck status:
sudo systemctl status google-birthdaysFollow logs:
journalctl -u google-birthdays -fsystemd cannot complete the first OAuth login. Do this once (on a machine with a browser),
or run it once manually as the service user if a browser is available:
cd /home/google-birthdays
sudo -u googlebirthdays .venv/bin/python birthdays_server.pyThis creates token.json. After that, start the systemd service normally.
Headless servers: create
token.jsonon a UI machine, then copytoken.json+credentials.jsonto the server.
Once the server is running:
http://<host>:8080/birthdays.ics
Add this URL to any calendar app as a subscribed calendar.
Calendar clients will refresh automatically (typically every 30–120 minutes).
You can expose the ICS feed safely via Caddy acting as a reverse proxy in front of the Python server.
This is recommended if you want:
- a stable URL
- optional HTTPS
- no direct exposure of the Python process
- The birthdays server listens on
localhost:8080 - Docker is installed
- Caddy runs on the same host
If you have a domain pointing to this server (for example birthdays.example.com):
docker run -d \
--name caddy-birthdays \
--restart unless-stopped \
-p 443:443 \
--add-host=host.docker.internal:host-gateway \
-v /home/google-birthdays/caddy/data:/data \
caddy:latest \
caddy reverse-proxy \
--from birthdays.example.com \
--to host.docker.internal:8080Caddy will:
- automatically obtain Let’s Encrypt certificates
- renew certificates automatically
- redirect HTTP to HTTPS
Most calendar clients strongly prefer HTTPS.
docker logs -f caddy-birthdaysTest the endpoint:
curl -I http://localhost/birthdays.icscredentials.jsonandtoken.jsongrant access to your contacts- Do not commit them to Git
- Add to
.gitignore:
credentials.json
token.json- The server exposes read-only calendar data
- Recommended: run behind a firewall or reverse proxy if exposed publicly
- Google People API: free
- OAuth: free
- No billing account
- No credit card
- No quotas issues for personal use
- Google does not provide an official birthdays export
- This workaround is currently the only reliable solution
- OAuth consent screen will remain in Testing (fine for personal use)
Apache 2.0
The APIs are supported. The use case is not officially “packaged” by Google, but fully allowed.
No. Standard Gmail accounts work perfectly.
Unlikely. This limitation has existed for over a decade.