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-05-07
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.
{
"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": {
"steps": [
{
"duration": 300,
"ramp": true,
"power": { "start": 45.0, "end": 60.0, "units": "%ftp" }
},
{
"duration": 600,
"power": { "value": 115.0, "units": "%ftp" }
},
{
"duration": 240,
"power": { "value": 115.0, "units": "%ftp" }
},
{
"duration": 300,
"ramp": true,
"power": { "start": 55.0, "end": 40.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" }
}
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.