Duplicate a training plan

POST/api/public/training-plans/{planId}/duplicate

Copy an existing training plan, including every exercise detail (per-set prescriptions, supersets, tempo, intensity, drop sets, alternatives). This is the supported way to turn a template into a trainee plan, because a template itself can never be assigned: you assign the copy, not the template. The copy is always created with isTemplate=false and isEditable=true. Returns the new plan, including its id, so no follow-up lookup is needed.

Assignment is optional. Send neither traineeId nor escortId to get an unassigned copy; send one of them to duplicate and assign in a single call.

Authentication

Requires an API key, sent as Authorization: Bearer cp_live_....

API keys carry scopes. Get the catalog of scopes and webhook events lists every scope.

Path parameters

Path parameters
NameTypeDescription
planIdrequiredstring

Header parameters

Header parameters
NameTypeDescription
Idempotency-Keystring

Optional. Send a unique key (e.g. a UUID) to make this POST safe to retry. The same key within 24h returns the original result instead of creating a duplicate.

Request body

application/json, required

Request body fields
NameTypeDescription
traineeIdstring

Assign the copy to this trainee by resolving their coaching period automatically. Picks the trainee's most recently created Active escort. Returns 400 'No active escort found for this trainee' when the trainee has no Active escort, which is the case for Pending, Suspended, or not-yet-started periods; use escortId for those. Omit both ids to leave the copy unassigned.

escortIdstring

Assign the copy to this exact coaching period, whatever its status. Use this instead of traineeId when the target period is not Active (Pending, Suspended, or scheduled for the future), or when the trainee has more than one period and you must not rely on automatic selection. When both ids are sent, escortId decides the target and traineeId is only validated against it: a mismatch returns 400 "escortId does not belong to the provided traineeId", which is a useful safety check when the two ids come from an external system.

titlestring

Title for the copy. Defaults to the source title with a copy suffix. The copy stays in the same plan group as its source, so duplicates do not scatter the plans list.

Request examples

cURL
curl --request POST \
  --url 'https://api.coach-platform.com/api/public/training-plans/<planId>/duplicate' \
  --header 'Authorization: Bearer cp_live_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "traineeId": "<string>"
}'
JavaScript (fetch)
const response = await fetch('https://api.coach-platform.com/api/public/training-plans/<planId>/duplicate', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cp_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "traineeId": "<string>"
  }),
});

const data = await response.json();

Responses

201 Created

Example body (application/json)
{
  "data": {
    "id": "<string>",
    "coach": "<string>",
    "escorts": [
      "<string>"
    ],
    "title": "<string>",
    "notes": "<string>",
    "level": "Beginner",
    "maxDuration": 123,
    "isTemplate": true,
    "workouts": [
      {
        "trainingName": "<string>",
        "trainingType": "A",
        "exerciseOrder": "Sequential",
        "notes": "<string>",
        "timeBasedDetails": {
          "totalRounds": 123,
          "timeLimit": 123,
          "workInterval": 123,
          "restInterval": 123,
          "restBetweenRounds": 123
        },
        "exercises": [
          {
            "exerciseDetails": {
              "id": "<string>",
              "name": "<string>"
            },
            "setsNumber": "<string>",
            "repsNumber": "<string>",
            "restTime": "<string>",
            "isDurationBased": true,
            "setDuration": "<string>",
            "weightPercentage": 123,
            "customNotes": "<string>",
            "weight": 123,
            "sets": [
              {
                "setNumber": 123,
                "reps": "<string>",
                "weight": 123,
                "restTime": "<string>",
                "intensityValue": 123,
                "dropSet": "DropSet",
                "isWarmupSet": true
              }
            ],
            "superSet": true,
            "superSetGroup": "<string>",
            "dropSet": "DropSet",
            "restPause": true,
            "cluster": true,
            "trackingType": "weight_reps",
            "intensityType": "Percentage",
            "intensityValue": 123,
            "tempo": {
              "eccentric": 123,
              "hold": 123,
              "concentric": 123,
              "rest": 123
            },
            "distance": 123,
            "distanceUnit": "meters",
            "specificAlternativeExercises": [
              "<string>"
            ]
          }
        ]
      }
    ],
    "createdAt": "<date-time>"
  },
  "warnings": [
    {
      "field": "<string>",
      "message": "<string>"
    }
  ]
}
Response fields (60)
Response fields
NameTypeDescription
dataobject
data.idstring
data.coachstring
data.escortsstring[]
data.titlestring
data.notesstring
data.levelstring

Allowed values: Beginner Intermediate Advanced

data.maxDurationnumber

Target session length in minutes.

data.isTemplateboolean

True when the plan is a reusable template.

data.workoutsobject[]
data.workouts[].trainingNamestring

Display name of the day, e.g. "Day A — Push".

data.workouts[].trainingTypestring

Day label, e.g. "A", "B", "FullBody".

Allowed values: A B C D E FullBody CrossFit Tabata HIIT EMOM AMRAP Circuit ForTime

data.workouts[].exerciseOrderstring

How exercises are performed. Defaults to Sequential.

Allowed values: Sequential Circuit Superset Complex

data.workouts[].notesstring

Free-text note for the whole day.

data.workouts[].timeBasedDetailsobject

Timing configuration for circuit-style days (Tabata, HIIT, EMOM, AMRAP). Seconds unless noted.

data.workouts[].timeBasedDetails.totalRoundsnumber
data.workouts[].timeBasedDetails.timeLimitnumber
data.workouts[].timeBasedDetails.workIntervalnumber
data.workouts[].timeBasedDetails.restIntervalnumber
data.workouts[].timeBasedDetails.restBetweenRoundsnumber
data.workouts[].exercisesobject[]
data.workouts[].exercises[].exerciseDetailsobject

The exercise catalog entry. On write, pass exerciseDetails as the catalog id string (from GET /exercises).

data.workouts[].exercises[].exerciseDetails.idstring
data.workouts[].exercises[].exerciseDetails.namestring
data.workouts[].exercises[].setsNumberstring

Number of sets, e.g. "3".

data.workouts[].exercises[].repsNumberstring

Reps per set, e.g. "10" or "8-12".

data.workouts[].exercises[].restTimestring

Rest between sets in seconds, e.g. "90".

data.workouts[].exercises[].isDurationBasedboolean

True for timed exercises (e.g. plank) instead of reps.

data.workouts[].exercises[].setDurationstring

Duration per set in seconds when isDurationBased is true.

data.workouts[].exercises[].weightPercentagenumber

Working weight as % of 1RM, e.g. 75.

data.workouts[].exercises[].customNotesstring

Free-text note shown to the trainee for this exercise.

data.workouts[].exercises[].weightnumber

Working weight in kg for the exercise.

data.workouts[].exercises[].setsobject[]

Per-set prescription. Takes precedence over setsNumber/repsNumber when present.

data.workouts[].exercises[].sets[].setNumbernumber

1-based position of the set.

data.workouts[].exercises[].sets[].repsstring

Reps for this set, e.g. "8".

data.workouts[].exercises[].sets[].weightnumber

Working weight for this set.

data.workouts[].exercises[].sets[].restTimestring

Rest after this set in seconds.

data.workouts[].exercises[].sets[].intensityValuenumber

Intensity for this set, read against intensityType.

data.workouts[].exercises[].sets[].dropSetstring

Allowed values: DropSet DoubleDropSet TripleDropSet

data.workouts[].exercises[].sets[].isWarmupSetboolean

Warmup sets are not counted towards working volume.

data.workouts[].exercises[].superSetboolean

True when this exercise belongs to a superset.

data.workouts[].exercises[].superSetGroupstring

Shared identifier grouping the exercises performed together in one superset.

data.workouts[].exercises[].dropSetstring

Allowed values: DropSet DoubleDropSet TripleDropSet

data.workouts[].exercises[].restPauseboolean

Rest-pause technique.

data.workouts[].exercises[].clusterboolean

Cluster-set technique.

data.workouts[].exercises[].trackingTypestring

What the trainee logs for this exercise.

Allowed values: weight_reps reps_only duration completion

data.workouts[].exercises[].intensityTypestring

How intensityValue is interpreted.

Allowed values: Percentage RPE RIR

data.workouts[].exercises[].intensityValuenumber

Intensity target.

data.workouts[].exercises[].tempoobject

Tempo in seconds per phase of the lift.

data.workouts[].exercises[].tempo.eccentricnumber
data.workouts[].exercises[].tempo.holdnumber
data.workouts[].exercises[].tempo.concentricnumber
data.workouts[].exercises[].tempo.restnumber
data.workouts[].exercises[].distancenumber

Distance for cardio exercises.

data.workouts[].exercises[].distanceUnitstring

Allowed values: meters km miles yards

data.workouts[].exercises[].specificAlternativeExercisesstring[]

Catalog ids the trainee may swap in for this exercise.

data.createdAtstring<date-time>
warningsobject[]

Non-fatal problems with follow-up writes. The resource was created, but each listed field was not applied.

warnings[].fieldstring
warnings[].messagestring

Error responses

  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 429 Too Many Requests
  • 500 Internal Server Error

These statuses share the same response body.

Example body (application/json)
{
  "error": {
    "code": "<string>",
    "message": "<string>",
    "fix": "<string>",
    "details": {},
    "retryAfterMs": 123
  }
}
Error fields (6)
Error fields
NameTypeDescription
errorobject
error.codestring
error.messagestring
error.fixstring | null
error.detailsobject | null
error.retryAfterMsnumber | null

This page is also available as Markdown. Browse it in the interactive explorer or download the OpenAPI spec.