# Duplicate a training plan

`POST https://api.coach-platform.com/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.

- Resource: Training Plans
- Authentication: API key in the `Authorization: Bearer cp_live_...` header
- HTML version: https://www.coach-platform.com/docs/api/reference/post-training-plans-plan-id-duplicate
- OpenAPI spec: https://www.coach-platform.com/openapi.json

## Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `planId` | string | Yes |   |

## Header parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `Idempotency-Key` | string | No | 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

Content type: `application/json`. Required.

| Field | Type | Required | Description |
|---|---|---|---|
| `traineeId` | string | No | 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. |
| `escortId` | string | No | 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. |
| `title` | string | No | 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. |

## Responses

| Status | Description |
|---|---|
| `201` Created |  |
| `400` Bad Request |  |
| `401` Unauthorized |  |
| `403` Forbidden |  |
| `404` Not Found |  |
| `409` Conflict |  |
| `429` Too Many Requests |  |
| `500` Internal Server Error |  |

### 201 Created

Content type: `application/json`

| Field | Type | Description |
|---|---|---|
| `data` | object |   |
| `data.id` | string |   |
| `data.coach` | string |   |
| `data.escorts` | string[] |   |
| `data.title` | string |   |
| `data.notes` | string |   |
| `data.level` | string | Allowed values: `Beginner`, `Intermediate`, `Advanced`. |
| `data.maxDuration` | number | Target session length in minutes. |
| `data.isTemplate` | boolean | True when the plan is a reusable template. |
| `data.workouts` | object[] |   |
| `data.workouts[].trainingName` | string | Display name of the day, e.g. "Day A — Push". |
| `data.workouts[].trainingType` | string | Day label, e.g. "A", "B", "FullBody". Allowed values: `A`, `B`, `C`, `D`, `E`, `FullBody`, `CrossFit`, `Tabata`, `HIIT`, `EMOM`, `AMRAP`, `Circuit`, `ForTime`. |
| `data.workouts[].exerciseOrder` | string | How exercises are performed. Defaults to Sequential. Allowed values: `Sequential`, `Circuit`, `Superset`, `Complex`. |
| `data.workouts[].notes` | string | Free-text note for the whole day. |
| `data.workouts[].timeBasedDetails` | object | Timing configuration for circuit-style days (Tabata, HIIT, EMOM, AMRAP). Seconds unless noted. |
| `data.workouts[].timeBasedDetails.totalRounds` | number |   |
| `data.workouts[].timeBasedDetails.timeLimit` | number |   |
| `data.workouts[].timeBasedDetails.workInterval` | number |   |
| `data.workouts[].timeBasedDetails.restInterval` | number |   |
| `data.workouts[].timeBasedDetails.restBetweenRounds` | number |   |
| `data.workouts[].exercises` | object[] |   |
| `data.workouts[].exercises[].exerciseDetails` | object | The exercise catalog entry. On write, pass exerciseDetails as the catalog id string (from GET /exercises). |
| `data.workouts[].exercises[].exerciseDetails.id` | string |   |
| `data.workouts[].exercises[].exerciseDetails.name` | string |   |
| `data.workouts[].exercises[].setsNumber` | string | Number of sets, e.g. "3". |
| `data.workouts[].exercises[].repsNumber` | string | Reps per set, e.g. "10" or "8-12". |
| `data.workouts[].exercises[].restTime` | string | Rest between sets in seconds, e.g. "90". |
| `data.workouts[].exercises[].isDurationBased` | boolean | True for timed exercises (e.g. plank) instead of reps. |
| `data.workouts[].exercises[].setDuration` | string | Duration per set in seconds when isDurationBased is true. |
| `data.workouts[].exercises[].weightPercentage` | number | Working weight as % of 1RM, e.g. 75. |
| `data.workouts[].exercises[].customNotes` | string | Free-text note shown to the trainee for this exercise. |
| `data.workouts[].exercises[].weight` | number | Working weight in kg for the exercise. |
| `data.workouts[].exercises[].sets` | object[] | Per-set prescription. Takes precedence over setsNumber/repsNumber when present. |
| `data.workouts[].exercises[].sets[].setNumber` | number | 1-based position of the set. |
| `data.workouts[].exercises[].sets[].reps` | string | Reps for this set, e.g. "8". |
| `data.workouts[].exercises[].sets[].weight` | number | Working weight for this set. |
| `data.workouts[].exercises[].sets[].restTime` | string | Rest after this set in seconds. |
| `data.workouts[].exercises[].sets[].intensityValue` | number | Intensity for this set, read against intensityType. |
| `data.workouts[].exercises[].sets[].dropSet` | string | Allowed values: `DropSet`, `DoubleDropSet`, `TripleDropSet`. |
| `data.workouts[].exercises[].sets[].isWarmupSet` | boolean | Warmup sets are not counted towards working volume. |
| `data.workouts[].exercises[].superSet` | boolean | True when this exercise belongs to a superset. |
| `data.workouts[].exercises[].superSetGroup` | string | Shared identifier grouping the exercises performed together in one superset. |
| `data.workouts[].exercises[].dropSet` | string | Allowed values: `DropSet`, `DoubleDropSet`, `TripleDropSet`. |
| `data.workouts[].exercises[].restPause` | boolean | Rest-pause technique. |
| `data.workouts[].exercises[].cluster` | boolean | Cluster-set technique. |
| `data.workouts[].exercises[].trackingType` | string | What the trainee logs for this exercise. Allowed values: `weight_reps`, `reps_only`, `duration`, `completion`. |
| `data.workouts[].exercises[].intensityType` | string | How intensityValue is interpreted. Allowed values: `Percentage`, `RPE`, `RIR`. |
| `data.workouts[].exercises[].intensityValue` | number | Intensity target. |
| `data.workouts[].exercises[].tempo` | object | Tempo in seconds per phase of the lift. |
| `data.workouts[].exercises[].tempo.eccentric` | number |   |
| `data.workouts[].exercises[].tempo.hold` | number |   |
| `data.workouts[].exercises[].tempo.concentric` | number |   |
| `data.workouts[].exercises[].tempo.rest` | number |   |
| `data.workouts[].exercises[].distance` | number | Distance for cardio exercises. |
| `data.workouts[].exercises[].distanceUnit` | string | Allowed values: `meters`, `km`, `miles`, `yards`. |
| `data.workouts[].exercises[].specificAlternativeExercises` | string[] | Catalog ids the trainee may swap in for this exercise. |
| `data.createdAt` | string<date-time> |   |
| `warnings` | object[] | Non-fatal problems with follow-up writes. The resource was created, but each listed field was not applied. |
| `warnings[].field` | string |   |
| `warnings[].message` | string |   |

Example:

```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>"
    }
  ]
}
```

### Error responses 400, 401, 403, 404, 409, 429, 500

Content type: `application/json`

| Field | Type | Description |
|---|---|---|
| `error` | object |   |
| `error.code` | string |   |
| `error.message` | string |   |
| `error.fix` | string \| null |   |
| `error.details` | object \| null |   |
| `error.retryAfterMs` | number \| null |   |

Example:

```json
{
  "error": {
    "code": "<string>",
    "message": "<string>",
    "fix": "<string>",
    "details": {},
    "retryAfterMs": 123
  }
}
```

## Examples

### cURL

```bash
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)

```js
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();
```

## Related endpoints

- [List training plans](https://www.coach-platform.com/docs/api/reference/get-training-plans/index.md): `GET /api/public/training-plans`. List training plans.
- [Create a training plan](https://www.coach-platform.com/docs/api/reference/post-training-plans/index.md): `POST /api/public/training-plans`. Create a training plan.
- [Get a training plan](https://www.coach-platform.com/docs/api/reference/get-training-plans-plan-id/index.md): `GET /api/public/training-plans/{planId}`. Get a training plan by id.
- [Update a training plan](https://www.coach-platform.com/docs/api/reference/patch-training-plans-plan-id/index.md): `PATCH /api/public/training-plans/{planId}`. Partially update a training plan.
- [Delete a training plan](https://www.coach-platform.com/docs/api/reference/delete-training-plans-plan-id/index.md): `DELETE /api/public/training-plans/{planId}`. Delete a training plan permanently.
- [Get a trainee's active training plan](https://www.coach-platform.com/docs/api/reference/get-trainees-trainee-id-active-training-plan/index.md): `GET /api/public/trainees/{traineeId}/active-training-plan`. Get a trainee's currently active training plan (resolved via the active coaching period).
- Previous: [Get a trainee's active training plan](https://www.coach-platform.com/docs/api/reference/get-trainees-trainee-id-active-training-plan/index.md): `GET /api/public/trainees/{traineeId}/active-training-plan`. Get a trainee's currently active training plan (resolved via the active coaching period).
- Next: [List workout logs](https://www.coach-platform.com/docs/api/reference/get-workout-logs/index.md): `GET /api/public/workout-logs`. List workout logs for a single trainee.
