Partner API Beta
DunMove API v1
Technical documentation for integration partners. After OAuth consent, the API gives read-only access to the connected athlete's active DunMove training plan.
Last updated: 2026-08-05
https://app.dunmove.com
Authorization Code + PKCE S256
read:athlete read:events
Read-only, active plan
Beta Scope
- The API is read-only in the MVP.
- Only the connected athlete's active DunMove plan is exposed.
- Activity and FIT uploads are not available yet.
- Event IDs can change after a full training plan regeneration.
- The v1 beta can still change slightly based on integration feedback.
OAuth2 Flow
The partner application starts the connection flow. DunMove is the OAuth provider; the partner application is the OAuth client. Each integration partner receives a dedicated client ID after the development redirect URI is confirmed.
Authorize
GET https://app.dunmove.com/oauth/authorize
?response_type=code
&client_id=<your_client_id>
&redirect_uri=<your_redirect_uri>
&scope=read:athlete read:events
&code_challenge=<s256_code_challenge>
&code_challenge_method=S256
&state=<opaque_state>
Token Exchange
POST https://app.dunmove.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
code=<authorization_code>
redirect_uri=<your_redirect_uri>
client_id=<your_client_id>
client_secret=<client_secret>
code_verifier=<pkce_code_verifier>
Refresh Token
POST https://app.dunmove.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
refresh_token=<refresh_token>
client_id=<your_client_id>
client_secret=<client_secret>
Token Response
{
"access_token": "opaque_access_token",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "opaque_refresh_token",
"scope": "read:athlete read:events"
}
API Endpoints
All API requests use Bearer authentication:
Authorization: Bearer <access_token>
GET /api/v1/athlete
{
"id": "dm_athlete_123",
"username": "sven",
"display_name": "Sven",
"language": "de",
"timezone": "Europe/Berlin"
}
GET /api/v1/athlete/{athlete_id}/events
GET https://app.dunmove.com/api/v1/athlete/dm_athlete_123/events?oldest=2026-05-01&newest=2026-05-31
| Parameter | Required | Format | Description |
|---|---|---|---|
oldest |
Yes | YYYY-MM-DD |
Inclusive start date. Events with start_date_local on or after this date are returned. |
newest |
Yes | YYYY-MM-DD |
Inclusive end date. Events with start_date_local on or before this date are returned. Must be equal to or after oldest. |
Both parameters are required. If either is missing the API returns 400 invalid_request. A typical integration fetches a rolling window, e.g. today through the next 60 days.
The response is always a JSON array — even if only one event falls within the date range or no events exist. Do not use this endpoint to fetch a single event by ID; use GET /events/{event_id} for that.
[
{
"id": "dm_event_20260505_060611_2026-05-12_001",
"plan_id": "20260505_060611",
"name": "Build - VO2max",
"type": "WORKOUT",
"sport": "Ride",
"start_date_local": "2026-05-12T09:00:00",
"duration": 3600,
"planned_load": null,
"description": "Planned DunMove workout",
"has_workout": true
}
]
Key fields
| Field | Description |
|---|---|
id |
Stable event identifier. Use this to call the detail endpoint. |
start_date_local |
Local start time of the event. Use this field for calendar display. |
has_workout |
true if structured workout steps are available via the detail endpoint. false for rest days, races, or notes without steps. Only fetch the detail endpoint when this is true. |
duration |
Planned duration in seconds. |
planned_load |
Planned training load (TSS). Currently null in the MVP. |
GET /api/v1/athlete/{athlete_id}/events/{event_id}
Returns a single event object (not an array) including full workout.steps. Use the id field from the events list to construct the URL.
Workout steps are returned in the structured format and supported ON/OFF repeats are preserved.
GET https://app.dunmove.com/api/v1/athlete/dm_athlete_123/events/dm_event_..._001
{
"id": "dm_event_20260505_060611_2026-05-12_001",
"plan_id": "20260505_060611",
"name": "Build - VO2max",
"type": "WORKOUT",
"sport": "Ride",
"start_date_local": "2026-05-12T09:00:00",
"duration": 3600,
"planned_load": null,
"description": "Planned DunMove workout",
"has_workout": true,
"workout": {
"format": "workout-structured-v1",
"repeat_depth": 1,
"repeat_mode": "on_off",
"steps": [
{
"duration": 300,
"ramp": true,
"power": { "start": 45.0, "end": 60.0, "units": "%ftp" }
},
{
"repeat": {
"count": 2,
"steps": [
{
"duration": 600,
"power": { "value": 115.0, "units": "%ftp" }
},
{
"duration": 240,
"power": { "value": 60.0, "units": "%ftp" }
}
]
}
}
]
}
}
Recommended Integration Pattern
A typical integration fetches the event list for a rolling window and only requests workout details on demand.
- Call
GET /events?oldest=<today>&newest=<today+60d>to get the planned events for the next 60 days. - Use
start_date_localto place events on a calendar. - Check
has_workout: truebefore fetching details — events without steps (rest days, races, notes) do not need a detail call. - When the user opens an event, call
GET /events/{id}using theidfrom the list to load the fullworkout.steps.
# 1. Fetch event list
GET /api/v1/athlete/dm_athlete_123/events?oldest=2026-05-13&newest=2026-07-12
# 2. For each event where has_workout is true, fetch details on demand
GET /api/v1/athlete/dm_athlete_123/events/dm_event_20260505_060611_2026-05-12_001
Workout Steps
The step format is aligned with the intervals.icu native format.
duration is always in seconds. Power is expressed as percent of FTP via the power object.
Steady-state step
{
"duration": 240,
"power": { "value": 115.0, "units": "%ftp" }
}
Ramp step (warmup / cooldown)
{
"duration": 300,
"ramp": true,
"power": { "start": 45.0, "end": 60.0, "units": "%ftp" }
}
Structured ON/OFF repeat
This is the default representation of the event detail endpoint. It preserves exactly one repeat level with exactly two constant FTP steps: the first step is the ON phase and the The API exposes only this non-recursive ON/OFF representation.
"workout": {
"format": "workout-structured-v1",
"repeat_depth": 1,
"repeat_mode": "on_off",
"steps": [
{ "duration": 300, "power": { "value": 60.0, "units": "%ftp" } },
{
"repeat": {
"count": 4,
"steps": [
{ "duration": 120, "power": { "value": 95.0, "units": "%ftp" } },
{ "duration": 60, "power": { "value": 55.0, "units": "%ftp" } }
]
}
}
]
}
count is the total number of repetitions. Nested repeats, ramps, warmup/cooldown phases,
and repeats with three or more child steps are returned flat in this structured response. Consumers only
need to implement one non-recursive two-step repeat shape.
Migration from v1 (before 2026-05)
If you integrated before May 2026, you may have used the old format:
// OLD (deprecated)
{ "target_type": "percent_ftp", "target": 115, "duration": 240 }
// NEW
{ "duration": 240, "power": { "value": 115.0, "units": "%ftp" } }
Ramps were not available in the old format. The ramp: true flag and power.start / power.end are new fields.
Future extensions
Absolute watt support and HR-based targets (%hr, %lthr) are planned. Always read power.units rather than assuming %ftp — the exact format for future target types will be documented here when available.
Error Responses
{
"error": "insufficient_scope",
"message": "Scope read:events is required"
}
| HTTP | error | Meaning |
|---|---|---|
| 400 | invalid_request |
A required parameter is missing or invalid. |
| 401 | invalid_token |
The token is missing, invalid, expired, or revoked. |
| 403 | insufficient_scope |
The access token does not include the required scope. |
| 404 | no_active_plan |
The athlete has no active DunMove plan. |
| 404 | not_found |
The athlete or event was not found. |
Current Test Status
- A local partner simulator has completed the OAuth and API flow against DunMove.
- OAuth start, login, consent, callback, and token exchange are working.
- Calendar and event detail responses include planned workouts and
workout.steps. - Refresh-token rotation and core error cases are covered by route-level checks.