Utiliy tool to upload GPX files from Adidas Running (ex Runtastic) to Strava with automatic activity type detection.
- 🚀 Batch upload - Upload entire directories of GPX files
- 🔍 Auto-detection - Automatically infers activity type from Runtastic JSON metadata
- 🔄 Smart retry - Exponential backoff and automatic retry on failures
- 📊 Progress tracking - Real-time progress with detailed logging
- 🎯 Year filtering - Upload only specific years
- ⚙️ Configurable - Extensive options for customization
- Python 3.9+
requestslibrary
- Clone or download this repository
- Install dependencies:
pip install requests- Make the script executable (optional):
chmod +x upload_file.py- Go to https://www.strava.com/settings/api
- Create an application (if you haven't already)
- Note your Client ID and Client Secret
- Use the following URL to authorize (replace
YOUR_CLIENT_ID):
https://www.strava.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost&response_type=code&scope=activity:write
- Authorize the app and copy the
codefrom the redirect URL - Exchange the code for an access token:
curl -X POST https://www.strava.com/oauth/token \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d code=AUTHORIZATION_CODE \
-d grant_type=authorization_code- Use the
access_tokenfrom the response
# Upload a single GPX file
python3 upload_file.py /path/to/file.gpx --access-token YOUR_TOKEN
# Upload all GPX files in a directory
python3 upload_file.py /path/to/directory --access-token YOUR_TOKEN# Upload only 2015 activities
python3 upload_file.py ./Sport-sessions/GPS-data --year 2015 --access-token YOUR_TOKEN# Automatically detect activity type from Runtastic JSON files
python3 upload_file.py ./Sport-sessions/GPS-data \
--year 2015 \
--json-dir ./Sport-sessions \
--access-token YOUR_TOKEN# Custom name, description, and skip gear update
python3 upload_file.py file.gpx \
--name "Morning Run" \
--description "Great weather!" \
--skip-metadata-update \
--access-token YOUR_TOKEN# Enable verbose logging to see all API responses
python3 upload_file.py ./Sport-sessions/GPS-data \
--year 2015 \
--access-token YOUR_TOKEN \
--verbose# Set the access token as an environment variable
export STRAVA_ACCESS_TOKEN=your_token_here
# Now you don't need to specify --access-token
python3 upload_file.py ./Sport-sessions/GPS-data --year 2015| Option | Description | Default |
|---|---|---|
gpx_path |
Path to GPX file or directory | ./Sport-sessions/GPS-data |
--access-token |
Strava access token | $STRAVA_ACCESS_TOKEN |
--activity-type |
Activity type (Run, Ride, Walk, etc.) | Auto-inferred |
--json-dir |
Directory with Runtastic JSON metadata | None |
--name |
Activity name | Auto-generated |
--description |
Activity description | Auto-generated |
--private |
Make activities private | True |
--trainer |
Mark as trainer activity | False |
--commute |
Mark as commute | False |
--poll-timeout |
Upload processing timeout (seconds) | 60 |
--year |
Upload only files with this year | None |
--skip-metadata-update |
Skip gear clearing (faster) | False |
--wait-timeout |
Wait for activity availability (seconds) | 120 |
--verbose, -v |
Enable debug logging | False |
The script automatically maps Runtastic activity types to Strava:
| Runtastic ID | Strava Type |
|---|---|
| 1 | Run |
| 2 | Walk |
| 3, 4, 15, 22 | Ride |
| 7, 19 | Walk |
| 13 | Hike |
| 18 | Swim |
| 82 | Trail Run |
- Upload GPX - File is uploaded to Strava
- Poll Status - Wait for Strava to process the upload
- Get Activity ID - Extract the created activity ID
- Wait for Availability - Wait for activity to be available in API (with exponential backoff)
- Clear Gear - Update activity to remove default gear assignment
When you upload a running activity, Strava automatically assigns your default running shoes and privacy visibility you set for your profile. For imported historical activities, you might prefer differently. Even though the script clears this automatic assignment of gears by setting gear_id=None, Strava overwrites it anyway, same for privacy. I suggest to set temporary the preferences you need in Strava settings and go back as you're done with importing.
The script uses smart retry logic:
- First check: 2 seconds
- Second check: 4 seconds
- Third check: 8 seconds
- Subsequent: 10 seconds (maximum)
This reduces API calls while ensuring activities become available.
If you hit Strava's rate limit (429 error), the script automatically:
- Waits with exponential backoff
- Retries the request
- Maximum backoff: 30 seconds
If you try to upload a file that already exists on Strava:
- The script detects the duplicate
- Extracts the existing activity ID
- Optionally updates metadata on the existing activity
If activities don't become available within the timeout:
- The activity is still created on Strava
- Gear clearing is skipped
- You can manually remove gear later if needed
Solutions:
# Increase timeout to 3 minutes
python3 upload_file.py ./Sport-sessions/GPS-data \
--wait-timeout 180 \
--access-token YOUR_TOKEN
# Or skip gear update entirely (faster)
python3 upload_file.py ./Sport-sessions/GPS-data \
--skip-metadata-update \
--access-token YOUR_TOKENSport-sessions/
├── GPS-data/
│ ├── 2015-01-01_12-00-00.gpx
│ ├── 2015-01-02_12-00-00.gpx
│ └── ...
├── 2015-01-01_12-00-00.json # Activity metadata
├── 2015-01-02_12-00-00.json
└── ...
The script looks for matching JSON files to infer activity type:
- In
--json-dir(if specified) - In parent directory of GPX file
- Next to the GPX file
python3 upload_file.py ./Sport-sessions/GPS-data \
--year 2024 \
--json-dir ./Sport-sessions \
--access-token cc23f3f735ddb4454c9f979ccea953e3071697d3Output:
2024-01-15 10:30:45 - INFO - Found 45 GPX file(s) to upload
[1/45] Processing 2024-01-01_08-30-00.gpx
2024-01-15 10:30:46 - INFO - Uploading 2024-01-01_08-30-00.gpx...
2024-01-15 10:30:46 - INFO - Inferred activity type 'Run' from 2024-01-01_08-30-00.json
2024-01-15 10:30:47 - INFO - Upload submitted, ID: 19139903659
2024-01-15 10:30:52 - INFO - Waiting for activity 18038693567 to become available...
2024-01-15 10:30:54 - INFO - Activity 18038693567 available after 1 attempts
2024-01-15 10:30:54 - INFO - Activity 18038693567 is ready. Clearing default gear...
2024-01-15 10:30:57 - INFO - ✓ Successfully uploaded 2024-01-01_08-30-00.gpx (Activity ID: 18038693567)
2024-01-15 10:30:57 - INFO - Default gear cleared successfully
[2/45] Processing 2024-01-02_09-15-00.gpx
...
============================================================
Upload Summary:
Successful: 45/45
Failed: 0/45
============================================================
python3 upload_file.py my-run.gpx \
--name "Morning 10K" \
--description "Personal best!" \
--activity-type Run \
--access-token YOUR_TOKEN# Upload 500 activities without waiting for gear update
python3 upload_file.py ./Sport-sessions/GPS-data \
--skip-metadata-update \
--access-token YOUR_TOKEN# Enable verbose mode to see all API responses
python3 upload_file.py problematic-file.gpx \
--verbose \
--access-token YOUR_TOKENProblem: Strava's API is slow to make the activity available.
Solutions:
- Increase timeout:
--wait-timeout 300 - Skip metadata update:
--skip-metadata-update - Wait and manually remove gear later on Strava
Problem: You're making too many API requests.
Solution: The script automatically handles this with exponential backoff. Just wait.
Problem: No access token provided.
Solutions:
- Use
--access-token YOUR_TOKEN - Set environment variable:
export STRAVA_ACCESS_TOKEN=YOUR_TOKEN
Problem: Directory is empty or wrong year filter.
Solutions:
- Check the directory path
- Remove
--yearfilter to see all files - Ensure files have
.gpxextension
Problem: Invalid or expired access token.
Solution: Generate a new access token (see "Getting a Strava Access Token" section).
- Use
--skip-metadata-updatefor bulk uploads if you don't care about adding additional metadata (ex: gear) - Increase
--wait-timeoutif you frequently see timeout warnings - Use
--yearfilter to upload in batches by year - Run at off-peak hours to avoid Strava API rate limits
- Use environment variable for access token to avoid typing it repeatedly
| Code | Meaning |
|---|---|
| 0 | Success (all files uploaded) |
| 1 | Error (some or all files failed) |
| 130 | Interrupted by user (Ctrl+C) |
- Even though sctivities are set as private by default (
--privateis True), don't forget to check your Strava account default options for visibility that might be overrided. - Your access token is never logged or stored by the script
- Use environment variables for tokens to avoid command history exposure
The repo has evident limits. Feel free to make the use you want :)
MIT - This script is provided as-is for personal use. Strava API usage must comply with Strava's API Agreement.
Special credits to @maxschaffelder repository: https://github.com/maxschaffelder/runtastic-to-strava.git
Happy running! 🚴♂️🏃♀️