Turn the Ride Receipt emails Citi Bike sends to your Gmail into Strava activities — with the real route map, correct distance, and proper timestamps. Backfill your whole history, sync new rides automatically, and import from any Lyft bikeshare — from a small, auditable Python CLI you run yourself.
One command processes every receipt that hasn't been uploaded yet —
or backfill a date window. Preview first with
--dry-run (parses only, no writes).
Gmail → parse receipt → build GPX → upload to Strava → tag & label. Your tokens never leave your machine.
Searches Gmail for
from:updates.citibikenyc.com subject:"Ride Receipt", skipping anything already uploaded.
Pulls start/end stations and times, the e-bike flag, the receipt number, and the Google-encoded polyline of the route — the source of truth for coordinates.
Computes distance from the polyline and interpolates each point's timestamp across the ride window. Distance and elapsed time are exact; the speed profile is smoothed.
Uploads the GPX to Strava, sets the sport type to E-Bike Ride where applicable, and labels the email so it's never uploaded twice.
Requires Python 3.11+. You register your own Google and Strava apps —
there's no shared server and no third party in the loop. Strava's API
needs a Strava subscription (no extra fee for subscribers). Prefer not
to connect Gmail? Feed a saved .eml with
process-file instead.
# 1. Clone & install git clone https://github.com/erikleon/citibike2strava cd citibike2strava python3 -m venv .venv && source .venv/bin/activate pip install -e . # 2. Register your own Google + Strava apps and add credentials. # Full walkthrough: docs/OAUTH_SETUP.md cp .env.example .env $EDITOR .env # 3. Authorize (opens your browser for each service). citibike2strava login # 4. Preview what would be uploaded — parses only, no writes. citibike2strava run --dry-run # 5. Do it for real (or backfill your history: run --since 2024-01-01). citibike2strava run # 6. Optional: print a cron/launchd/Task Scheduler recipe to auto-sync. citibike2strava schedule
Accurate, private, and idempotent by design — built for your whole history, not just today's ride.
The GPX is decoded from the receipt's polyline, so Strava shows the actual streets you rode.
Electric rides are tagged as E-Bike Ride on Strava automatically.
Uploaded emails get labelled, and Strava rejects a duplicate
external_id — so a ride is only ever imported once.
You bring your own OAuth apps. Tokens are stored locally with
0600 permissions and never sent to anyone but Google
and Strava.
Import every past ride in one run. It paces itself under Strava's
rate limit, retries on
429, and resumes cleanly — one bad receipt never
aborts the batch.
Citi Bike, plus experimental Divvy, Bay Wheels, Bluebikes, and Capital Bikeshare. The parser fails closed on anything it doesn't recognize, so it never invents a route.
Feed a saved or forwarded .eml (or paste the receipt)
with process-file and skip the Gmail connection
entirely — Strava is the only account you need.
schedule prints a cron / launchd / Task Scheduler
recipe, or run watch for a single foreground command —
either way new rides sync automatically.