Strava API reference
Strava linking, sync and bulk export import. See Integrations and Importing data.
Routes
import_bikes_from_strava_export
import_bikes_from_strava_export(request, file, token_user_id, db)
Create the bikes listed in an uploaded Strava bikes.csv.
Source code in backend/app/modules/strava/router.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | |
import_shoes_from_strava_export
import_shoes_from_strava_export(request, file, token_user_id, db)
Create the shoes listed in an uploaded Strava shoes.csv.
Source code in backend/app/modules/strava/router.py
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | |
strava_retrieve_activities_days
strava_retrieve_activities_days(request, start_date, end_date, _validate_access_token, _check_scopes, token_user_id, db, storage)
Queue a Strava sync of an explicit date range.
Returns 202 with a job handle; poll GET /activities/ingestion-jobs/{job_id}
for the outcome. The range is fetched by a background job, never on this
request or an API worker thread.
Source code in backend/app/modules/strava/router.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
Activity sync
fetch_and_process_activities
fetch_and_process_activities(strava_client, start_date, end_date, user_id, user_integrations)
Fetch a user's Strava activities in a window and store the new ones.
Runs on a worker thread. The Strava round trips happen with no database session open; each activity then opens a short session of its own for the duplicate check, the lookups and the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
strava_client
|
Client
|
A client authenticated as the user. |
required |
start_date
|
datetime
|
Start of the window. |
required |
end_date
|
datetime
|
End of the window. |
required |
user_id
|
int
|
The user the activities belong to. |
required |
user_integrations
|
UsersIntegrationsRead
|
The user's integration settings. |
required |
Returns:
| Type | Description |
|---|---|
list[ActivityRead | None] | None
|
One entry per activity Strava returned ( |
list[ActivityRead | None] | None
|
or |
Raises:
| Type | Description |
|---|---|
HTTPException
|
When Strava rejects the request or the user is missing. |
Source code in backend/app/modules/strava/activity_utils.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
get_user_strava_activities_by_dates
get_user_strava_activities_by_dates(start_date, end_date, user_id)
Fetch and store one user's Strava activities recorded in a window.
Refreshes the user's access token first when it is about to expire, so the scheduled per-user refresh also keeps tokens current and no separate pass over every user is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_date
|
datetime
|
Start of the window. |
required |
end_date
|
datetime
|
End of the window. |
required |
user_id
|
int
|
The user to sync. |
required |
Returns:
| Type | Description |
|---|---|
list[ActivityRead | None] | None
|
The activities Strava returned, or |
list[ActivityRead | None] | None
|
nothing came back. |
Raises:
| Type | Description |
|---|---|
HTTPException
|
When Strava rejects the request or the account is missing. |
Source code in backend/app/modules/strava/activity_utils.py
697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 | |
process_activity
process_activity(activity, user_id, user_privacy_settings, strava_client, user_integrations)
Parse one Strava activity and store it, unless it is already stored.
The Strava calls inside :func:parse_activity run with no session open. The
duplicate check and the store each use a short session, and the store
publishes activity.created on this worker thread, where its subscribers
run inline without touching the request path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
activity
|
SummaryActivity
|
The Strava summary activity. |
required |
user_id
|
int
|
The user the activity belongs to. |
required |
user_privacy_settings
|
UsersPrivacySettingsRead
|
The user's privacy defaults. |
required |
strava_client
|
Client
|
A client authenticated as the user. |
required |
user_integrations
|
UsersIntegrationsRead
|
The user's integration settings. |
required |
Returns:
| Type | Description |
|---|---|
ActivityRead | None
|
The stored activity, or |
Source code in backend/app/modules/strava/activity_utils.py
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 | |
save_activity_streams_laps
save_activity_streams_laps(activity, stream_data, laps, db)
Persist a Strava-parsed activity through the canonical ingestion seam.
Strava is a thin adapter: it builds the
format-agnostic :class:~modules.activities.activity.schema.ParsedActivity
from the parse result and delegates persistence + activity.created
publication to
:func:~modules.activities.activity.activity_integration.store_parsed_activity,
instead of calling create_activity and the stream/lap CRUD directly. The
notification, thumbnail, and HR-zone subscribers react to the published event.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
activity
|
ActivityCore
|
The parsed Strava activity to persist. |
required |
stream_data
|
list
|
|
required |
laps
|
list[dict] | None
|
Parsed lap dicts, or |
required |
db
|
Session
|
Database session. |
required |
Returns:
| Type | Description |
|---|---|
ActivityRead
|
The created activity schema. |
Source code in backend/app/modules/strava/activity_utils.py
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 | |
Gear sync
iterate_over_bikes_csv
iterate_over_bikes_csv(bikes_file_path)
Parses a Strava bikes CSV file and returns its contents as a dictionary.
Constructs a dictionary where each key is the "Bike Name" from the CSV, and the value is a dictionary of the bike's attributes as provided in the CSV row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bikes_file_path
|
str
|
Path of the uploaded bikes CSV. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict
|
A dictionary mapping bike names to their corresponding data from the CSV file. |
Raises:
| Type | Description |
|---|---|
HTTPException
|
If the CSV file is missing, has invalid headers, or cannot be parsed. |
Source code in backend/app/modules/strava/gear_utils.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | |
iterate_over_shoes_csv
iterate_over_shoes_csv(shoes_file_path)
Parses a Strava shoes CSV file and returns its contents as a list. Why a list? Strava shoes files do not have anything that can be used as a key (unlike Strava bikes files, where nickname is required and unique)
Constructs a list where each item a dictionary of the shoe's attributes as provided in the CSV row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shoes_file_path
|
str
|
Path of the uploaded shoes CSV. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
list |
list
|
A list of shoe data obtained from the CSV file. |
Raises:
| Type | Description |
|---|---|
HTTPException
|
If the CSV file is missing, has invalid headers, or cannot be parsed. |
Source code in backend/app/modules/strava/gear_utils.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | |
match_gear_for_activity
match_gear_for_activity(activity, gears)
Return the local gear ID matching this activity's Strava gear, else None.
Source code in backend/app/modules/strava/gear_utils.py
106 107 108 109 110 111 112 113 114 115 116 | |
transform_csv_bike_gear_to_schema_gear
transform_csv_bike_gear_to_schema_gear(bikes_dict, token_user_id)
Transforms a dictionary of bike data (parsed from CSV) into a list of Gear schema objects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bikes_dict
|
dict
|
A dictionary where each key is a bike nickname and each value is a dictionary containing bike attributes such as "Bike Brand" and "Bike Model". |
required |
token_user_id
|
int
|
The user ID to associate with each Gear object. |
required |
Returns:
| Type | Description |
|---|---|
list[GearCreate]
|
list[gears_schema.GearCreate]: A list of Gear schema objects created from the input bike data. |
Source code in backend/app/modules/strava/gear_utils.py
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | |
transform_csv_shoe_gear_to_schema_gear
transform_csv_shoe_gear_to_schema_gear(shoes_list, token_user_id, db)
Transforms a list of shoe data (parsed from CSV) into a list of Gear schema objects. Renames any name-less shoes to a default name plus a unique number, as Strava allows name-less shoes, but Endurain does not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shoes_list
|
list
|
A list where each row is a single shoe's data, parsed from the strava shoe csv |
required |
token_user_id
|
int
|
The user ID to associate with each Gear object. |
required |
db
|
session
|
Database session |
required |
Returns:
| Type | Description |
|---|---|
list[GearCreate]
|
list[gears_schema.GearCreate]: A list of Gear schema objects created from the input shoe data. |
Source code in backend/app/modules/strava/gear_utils.py
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | |
Athlete
Bulk import source
The Strava-export specialisation of the bulk-import source, and its archive layout.
A Strava export is a folder of activity files plus an activities.csv manifest
and a media directory. Three things follow from that, and all three are facts
about Strava rather than about bulk import:
- the manifest supplies names, descriptions and gear the exported files omit,
- it lists a multi-activity
.fitonce per activity inside it, so importing every activity from every listing multiplies them (a 5-activity file becomes 25), - the photos ship next to the files rather than inside them.
Activities knows nothing about Strava: this module registers a
:class:BulkImportFormat that says where the activity files are, where the user
drops them, and how to build the source that answers those three questions.
StravaBulkImportSource
dataclass
Bases: BulkImportSource
A file from a Strava bulk export.
Attributes:
| Name | Type | Description |
|---|---|---|
strava_activities |
dict | None
|
Parsed Strava |
gear_nickname_to_id |
dict | None
|
Gear nickname -> internal gear id, used to resolve the gear named in the CSV. |
read_member |
ArchiveMemberReader | None
|
Reads a member of the archive by name, to reach the photos. |
Source code in backend/app/modules/strava/bulk_import_source.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
import_side_artifacts
import_side_artifacts(created_activities, file_base_name, db)
Attach the photos the export ships alongside the activity file.
Even a multi-activity .fit is handled correctly by attaching to the
last created activity — a Strava export's activity directory holds one
imported activity per file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
created_activities
|
list[ActivityRead]
|
Activities persisted from the file, in order. |
required |
file_base_name
|
str
|
The file's original base name (the |
required |
db
|
Session
|
Database session. |
required |
Source code in backend/app/modules/strava/bulk_import_source.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
metadata_for
metadata_for(file_base_name)
Build the supplemental metadata from the matching activities.csv row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_base_name
|
str
|
The original (pre-decompression) base filename, which
is the key into the |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The metadata dict, empty when the import carries no start timestamp. |
Source code in backend/app/modules/strava/bulk_import_source.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
should_import
should_import(activity, activity_metadata, *, activities_in_file)
Whether one activity parsed from a multi-activity file should be imported.
The activities.csv row identifies which activity inside the file this
listing refers to, by start time; the others are duplicates of listings
handled elsewhere in the run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
activity
|
ActivityCore
|
The parsed activity under consideration. |
required |
activity_metadata
|
dict
|
Metadata from :meth: |
required |
activities_in_file
|
int
|
How many activities the file yielded. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in backend/app/modules/strava/bulk_import_source.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
register_bulk_import_format
register_bulk_import_format()
Register the Strava export layout with activity ingestion.
Source code in backend/app/modules/strava/bulk_import_source.py
217 218 219 220 221 222 223 224 225 226 227 | |
Bulk import
build_import_dictionary
build_import_dictionary(file_base_name, import_initiated_time, is_strava_bulk_import=False, strava_activities=None)
Creates the "import_info" dictionary that is added to all activities that are imported from files.
Functions both for Strava imports and generic bulk imports, depending on whether the is_strava_bulk_import variable is set or not.
Returns: Dictionary that contains the import_dict values for the activity (which is then added to the activity as a dictionary in the "import_info" field of the activity).
Source code in backend/app/modules/strava/bulk_import_utils.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | |
build_metadata_dict
build_metadata_dict(file_base_name, strava_activities, import_initiated_time, users_existing_gear_nickname_to_id=None)
Creates a dictionary with metadata information pulled from a Strava activities file.
The field strava_activity_metadata["metadata_found_in_csv"] identifies whether there was an entry for this activity in the activities.csv file or not.
Returns: Dictionary that contains the gear name as a key to look up the gear ID.
Source code in backend/app/modules/strava/bulk_import_utils.py
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
create_activity_media_from_strava_bulk_import
create_activity_media_from_strava_bulk_import(activity_id, media_strava_filename, data, db)
Attaches a media file from a Strava export to an activity that was just imported.
The bytes are validated as an image through the unified upload pipeline before they are stored and become servable; a rejected file is logged and skipped so one bad photo does not fail the activity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
activity_id
|
int
|
The imported activity. |
required |
media_strava_filename
|
str
|
The file's name in the export. |
required |
data
|
bytes
|
The file's bytes. |
required |
db
|
Session
|
Database session. |
required |
Source code in backend/app/modules/strava/bulk_import_utils.py
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | |
create_gear_dictionary_for_bulk_import
create_gear_dictionary_for_bulk_import(user_id, db)
Creates a dictionary that links gear nicknames in Endurain to their Endurain gear IDs.
The dictionary includes both base Endurain gear nickname and the gear name smoosh ("{brand} {model} {name}") that Strava uses for its shoe gear listing in activities.csv
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user_id
|
int
|
The importing user. |
required |
db
|
Session
|
Database session. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, list[int]]
|
Dictionary that uses the gear name as a key to look up the gear ID; empty |
dict[str, list[int]]
|
when the user has no gear, which only means no activity is linked to one. |
Source code in backend/app/modules/strava/bulk_import_utils.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
does_activity_start_time_match_the_data_in_strava_activities_csv
does_activity_start_time_match_the_data_in_strava_activities_csv(activity, activity_metadata_dict)
Strava includes each multi-activity .fit file once for each activity inside the fit file. Thus, run without filtering, a 5-activity fit file is imported 5 times for 25 activities imported.
So, to find out which activity goes with which file, we check if start time of activity aligns with start time of the activity's strava activities.csv file data. If it does import.
Formatting of times in the two files
Activity start time - from Endurain activity parser: 2023-10-21T07:41:47 Activity start time - from Strava activities.csv: Oct 21, 2023, 8:13:28 AM
Returns: True if the start times match. False if they do not.
Source code in backend/app/modules/strava/bulk_import_utils.py
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | |
import_media_from_strava_bulk_export
import_media_from_strava_bulk_export(strava_activities, created_activity, file_base_name, read_member, db)
Import media files associated with a Strava bulk export activity.
Reads the pipe-separated media list from the Strava activities.csv entry
for the given file, then attaches each referenced image found in the export's
media folder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
strava_activities
|
dict
|
Strava bulk-import metadata dict keyed by filename, then by activities.csv column header. |
required |
created_activity
|
ActivityRead
|
The newly created activity object to which media will be attached. |
required |
file_base_name
|
str
|
Base filename of the activity file, used as the lookup key in strava_activities. |
required |
read_member
|
Callable[[str], bytes | None]
|
Reads a member of the archive being imported by name. |
required |
db
|
Session
|
SQLAlchemy database session. |
required |
Source code in backend/app/modules/strava/bulk_import_utils.py
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | |
parse_activities_manifest
parse_activities_manifest(manifest_path)
Parses a Strava activities.csv manifest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifest_path
|
str
|
Path of the manifest file. |
required |
Returns:
| Type | Description |
|---|---|
dict | None
|
Dictionary of the manifest's rows keyed by each activity's filename, or |
dict | None
|
|
Source code in backend/app/modules/strava/bulk_import_utils.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | |
Utilities
StravaRateLimitTracker
Tracks Strava API rate limit state in the platform's shared state.
The limits are Strava's, per application, so every process and replica has to
see the same answer: a process-local flag would let each worker keep
hammering an API another one was already told to back off from. The flags are
TTL keys in the platform StateProvider (in-process under the local
profile, Redis when distributed), so they expire on their own.
Source code in backend/app/modules/strava/utils.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
is_rate_limited
is_rate_limited()
Check if Strava API calls should be skipped.
Fails open when the shared state is unreachable: a missed back-off costs one rejected call, which marks the limit again, whereas failing closed would stop every sync for as long as the state store is down.
Returns:
| Type | Description |
|---|---|
bool
|
True if currently rate-limited. |
Source code in backend/app/modules/strava/utils.py
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
mark_rate_limited
mark_rate_limited(is_long_term=False)
Record that a rate limit was hit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
is_long_term
|
bool
|
True for the daily limit (resets at UTC midnight), False for the 15-minute limit. |
False
|
Source code in backend/app/modules/strava/utils.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
is_strava_not_found_error
is_strava_not_found_error(err)
Check if an exception is a Strava 404 Not Found error.
This occurs for activities that have no streams, such as those manually added via Apple Health and imported into Strava.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
err
|
Exception
|
The exception to check. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the error is a 404 not-found response. |
Source code in backend/app/modules/strava/utils.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
is_strava_rate_limit_error
is_strava_rate_limit_error(err)
Check if an exception is a Strava rate limit error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
err
|
Exception
|
The exception to check. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the error is a 429 rate limit response. |
Source code in backend/app/modules/strava/utils.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | |
refresh_user_strava_token
refresh_user_strava_token(user_id)
Refresh a user's Strava access token when it is about to expire.
Called at the start of the user's own sync job, so tokens are kept current without a separate pass over every user. The integration is read in one short session, the Strava call runs with none open, and the new tokens are written in another. A failure is logged and left to the sync that follows, which will fail visibly if the token really is unusable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user_id
|
int
|
The user whose token to refresh. |
required |
Source code in backend/app/modules/strava/utils.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |