Skip to content

Instantly share code, notes, and snippets.

@michaellihs
Last active January 3, 2026 18:39
Show Gist options
  • Select an option

  • Save michaellihs/bb262e2c6ee93093485361de282c242d to your computer and use it in GitHub Desktop.

Select an option

Save michaellihs/bb262e2c6ee93093485361de282c242d to your computer and use it in GitHub Desktop.
Upload tracks to Strava from the Command Line

Upload GPS Tracks to Strava from your Command Line

This short tutorial describes how to upload GPS tracks to Strava using your command line interface / shell. It requires no special tools or any 3rd party code.

1. Generate an API Key

Run the following steps with your user logged in to Strava in your browser!

Strava uses OAuth to authenticate against 3rd party applications. In order to authenticate to your Strava account from your command line, you first have to generate an API key. Therefore go to this page https://strava.github.io/api/v3/oauth/ and create a new API. The settings are as follows:

  • Application Name chose whatever name you like (does not matter for our use case)
  • Website chose whatever website you want to use (needs to be a valid url, e.g. [http://google.com] (does not matter for our use case)
  • Callback Domain any domain name that should be used as a callback (does not matter for our use case)

After you saved your API, you need to upload a image for it.

Open the https://strava.github.io/api/v3/oauth/ page again and copy the following values to a text editor

  • Client ID - an ID for your application, used later on to identify your App
  • Secret - a secret token generated for you (not your OAuth Token!)

2. Generate an OAuth Token

For the purpose of generating the OAuth token, this documentation helps a lot https://strava.github.io/api/v3/oauth/.

  1. Open the following URL (replace CLIENT_ID with the ID from above):

    https://www.strava.com/oauth/authorize?client_id=CLIENT_ID&response_type=code&redirect_uri=http%3A%2F%2Flocalhost&scope=activity:write&state=mystate&approval_prompt=force
    

    make sure to properly quote the redirect URL

  2. Click "Authorise"

  3. Cope the Code from the URL that is given as the URL code parameter

  4. Run the following CURL request, to get the final OAuth token (replace CLIENT_ID, CLIENT_SECRET and CODE accordingly):

    curl -X POST https://www.strava.com/oauth/token \
      -F client_id=CLIENT_ID \
      -F client_secret=CLIENT_SECRET \
      -F code=CODE
  5. Copy the access_token from the JSON response - this is your OAuth token

3. Upload your tracks

Now comes the funny part. Use the following command to upload a track to Strava:

curl -X POST https://www.strava.com/api/v3/uploads \
    -H "Authorization: Bearer OAUTH_TOKEN" 
    -F file=@"PATH_TO_FILE" -F data_type="tcx"

other data_types can be fit, fit.gz, tcx, tcx.gz, gpx, gpx.gz

To check the status of your update, use the id from the JSON response and run

curl https://www.strava.com/api/v3/uploads/ID -H "Authorization: Bearer OAUTH_TOKEN"

If you want to upload a directory with files, use the following command

 for i in `ls /path/to/files/*.tcx`
   do curl -X POST https://www.strava.com/api/v3/uploads -H "Authorization: Bearer OAUTH_TOKEN" -F file=@"$i" -F data_type="tcx"
 done

Further References

@oliviertassinari
Copy link

With Strava rate limiting https://developers.strava.com/docs/rate-limits/.

The default overall rate limit allows 200 requests every 15 minutes, with up to 2,000 requests per day. The default “non-upload” rate limit allows 100 requests every 15 minutes, with up to 1,000 requests per day.

I had to add sleep 6

@SaturnXIII
Copy link

Thanks i have made a automate script in py

import os
import time
import requests

CONFIG_FILE = "strava_config.txt"
API_BASE = "https://www.strava.com"

# ----------------- Load and save config -----------------
def load_config():
    config = {}
    if not os.path.exists(CONFIG_FILE):
        return config
    with open(CONFIG_FILE, "r") as f:
        for line in f:
            if "=" in line:
                k, v = line.strip().split("=", 1)
                config[k] = v
    return config

def save_config(config):
    with open(CONFIG_FILE, "w") as f:
        for k, v in config.items():
            f.write(f"{k}={v}\n")

# ----------------- Tutorial setup -----------------
def tutorial_setup():
    print("=== Strava Configuration (one-time setup) ===")
    client_id = input("Client ID: ").strip()
    client_secret = input("Client Secret: ").strip()

    print("\nOpen this URL in your browser:\n")
    print(
        f"https://www.strava.com/oauth/authorize?"
        f"client_id={client_id}"
        f"&response_type=code"
        f"&redirect_uri=http://localhost"
        f"&scope=activity:write"
        f"&approval_prompt=force"
    )

    code = input("\nPaste the OAuth code here: ").strip()

    # --- Robust extraction of the code from URL if user pasted the full URL ---
    if "code=" in code:
        code = code.split("code=")[1]
    code = code.split("&")[0]
    # -----------------------------------------------------------------------

    r = requests.post(
        f"{API_BASE}/oauth/token",
        data={
            "client_id": client_id,
            "client_secret": client_secret,
            "code": code,
            "grant_type": "authorization_code",
        },
    )

    data = r.json()

    # --- Handle errors gracefully ---
    if "access_token" not in data:
        print("\n❌ OAuth error received from Strava:")
        print(data)
        print("\n👉 Make sure you pasted the correct OAuth code, not the access token.")
        exit(1)

    config = {
        "client_id": client_id,
        "client_secret": client_secret,
        "access_token": data["access_token"],
        "refresh_token": data["refresh_token"],
        "expires_at": str(data["expires_at"]),
    }

    save_config(config)
    print("\n✅ Configuration saved to strava_config.txt")
    return config

# ----------------- Refresh token -----------------
def refresh_token(config):
    print("🔄 Refreshing access token...")
    r = requests.post(
        f"{API_BASE}/oauth/token",
        data={
            "client_id": config["client_id"],
            "client_secret": config["client_secret"],
            "refresh_token": config["refresh_token"],
            "grant_type": "refresh_token",
        },
    )
    data = r.json()
    config["access_token"] = data["access_token"]
    config["refresh_token"] = data["refresh_token"]
    config["expires_at"] = str(data["expires_at"])
    save_config(config)
    print("✅ Token refreshed successfully")

# ----------------- Check token expiry -----------------
def check_token(config):
    if time.time() > int(config["expires_at"]):
        refresh_token(config)

# ----------------- Upload file -----------------
def upload_file(config):
    path = input("Path to file (gpx/tcx/fit): ").strip()
    data_type = input("Data type (gpx, tcx, fit, etc.): ").strip()

    check_token(config)

    with open(path, "rb") as f:
        r = requests.post(
            f"{API_BASE}/api/v3/uploads",
            headers={"Authorization": f"Bearer {config['access_token']}"},
            files={"file": f},
            data={"data_type": data_type},
        )

    print("📤 Upload response:")
    print(r.json())

# ----------------- Check upload status -----------------
def check_upload_status(config):
    upload_id = input("Upload ID: ").strip()
    check_token(config)

    r = requests.get(
        f"{API_BASE}/api/v3/uploads/{upload_id}",
        headers={"Authorization": f"Bearer {config['access_token']}"},
    )

    print("📊 Upload status:")
    print(r.json())

# ----------------- Main menu -----------------
def menu(config):
    while True:
        print("\n=== STRAVA CLI ===")
        print("1) Upload a file")
        print("2) Check upload status")
        print("3) Refresh token")
        print("4) Quit")

        choice = input("> ").strip()

        if choice == "1":
            upload_file(config)
        elif choice == "2":
            check_upload_status(config)
        elif choice == "3":
            refresh_token(config)
        elif choice == "4":
            break
        else:
            print("❌ Invalid choice")

# ----------------- Main -----------------
def main():
    config = load_config()

    required_keys = [
        "client_id",
        "client_secret",
        "access_token",
        "refresh_token",
        "expires_at",
    ]

    if not all(k in config and config[k] for k in required_keys):
        config = tutorial_setup()

    menu(config)

if __name__ == "__main__":
    main()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment