DunMove Favicon 1.12.0

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

Base URL https://app.dunmove.com
OAuth Authorization Code + PKCE S256
Scopes read:athlete read:events
MVP 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.

  1. Call GET /events?oldest=<today>&newest=<today+60d> to get the planned events for the next 60 days.
  2. Use start_date_local to place events on a calendar.
  3. Check has_workout: true before fetching details — events without steps (rest days, races, notes) do not need a detail call.
  4. When the user opens an event, call GET /events/{id} using the id from the list to load the full workout.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.