POST /api/v1/enrollments/{enrollmentId}/transfer
Moves one participation to another cohort of the same program. The enrollment
itself does not change: history, progress, deliverables, reviews, attendance,
current action, deadlines and the assigned coach all stay attached to it. Only
the cohort context changes, on the date you choose — which is what live
sessions, the community space and cohort dashboards read from that moment on.
| Role in the organization | This endpoint |
|---|
OWNER | Allowed |
ADMIN | Allowed |
MEMBER | Conditional — only with a business role below |
| Requirement | Rule for this endpoint |
|---|
| Business role | program_director scoped to the organization |
| Resource scope | Enrollment and cohort of the organization attached to the API key |
| Participation | Not applicable — an assigned coach cannot transfer a participation |
| Resource state | The enrollment must not be closed; the target cohort must accept an entry |
Moving someone to another promotion is an operating decision, so it takes the
same authorization as pausing one: a coach assigned to the enrollment can
document sessions and plan interventions, but cannot transfer. A platform-wide
role grants nothing here. An enrollment or a cohort of another organization is
refused without revealing whether it exists.
| Parameter | Type | Required | Description |
|---|
enrollmentId | UUID | yes | Enrollment visible from the key's organization |
| Header | Required | Description |
|---|
x-api-key | yes | API key created for this organization |
Idempotency-Key | yes | Stable key for this transfer intention |
X-Correlation-Id | no | Caller correlation id, returned or generated |
Request body
| Field | Type | Required | Nullable | Description |
|---|
toCohortId | UUID | yes | no | Target cohort: same program, still accepting an entry, different from the current one |
reason | string | yes | no | 1 to 2000 characters; visible to your team and in the enrollment history, not to the member |
effectiveAt | ISO 8601 string | no | no | When the new cohort context starts. Defaults to now; a past value is accepted, a future one is refused |
capacityOverride | object | no | no | Enter a full target cohort anyway: {"enabled": true, "reason": "…"}, reason 1 to 500 characters. Refused unless the bearer can manage enrollments. Recorded in the audit |
A cohort is a valid target when all three hold:
- it belongs to the same program as the enrollment;
- it still accepts an entry — a completed or archived cohort does not;
- it is not the cohort the enrollment is already in.
A cohort still in preparation is a valid target. Its content stays locked for
the member until that cohort opens, exactly as it would for a new enrollment.
{
"toCohortId": "3b0e51a4-0000-4000-8000-000000000000",
"effectiveAt": "2026-09-01T09:00:00.000Z",
"reason": "Resumes with the next promotion."
}
curl -X POST "$COHORTE_API_BASE_URL/api/v1/enrollments/ENROLLMENT_ID/transfer" \
-H "x-api-key: $COHORTE_API_KEY" \
-H "Idempotency-Key: transfer-2026-09-01" \
-H 'Content-Type: application/json' \
-d '{"toCohortId":"COHORT_ID","reason":"Resumes with the next promotion."}'
201 Created — the participation now belongs to the target cohort.
200 OK — the same Idempotency-Key replayed the original transfer; nothing was written a second time.
400 Bad Request — missing Idempotency-Key, invalid JSON, an empty reason, or an effective date in the future.
401 Unauthorized — the key is missing, invalid, or no longer attached to a member.
403 Forbidden — the bearer cannot change the lifecycle of enrollments in this organization.
404 Not Found — the enrollment or the target cohort is unknown, or belongs to another organization.
409 Conflict — the participation is closed, the target cohort belongs to another program, no longer accepts an entry, or is the one the enrollment is already in.
| Field | Type | Nullable | Description |
|---|
success | boolean | no | true for a successful request |
replayed | boolean | no | true when the idempotency key replayed a transfer |
data.id | UUID | no | Transfer identifier |
data.enrollmentId | UUID | no | Enrollment that moved |
data.fromCohortId | UUID | yes | Cohort left behind; null when there was none |
data.fromCohortName | string | yes | Display name of that cohort |
data.toCohortId | UUID | no | Cohort the enrollment now belongs to |
data.toCohortName | string | yes | Display name of that cohort |
data.effectiveAt | ISO 8601 string | no | When the new cohort context starts, in UTC |
data.reason | string | no | Reason given for the transfer |
data.transferredBy | UUID | no | Account that decided the transfer |
data.transferredByName | string | yes | Display name of that account |
data.createdAt | ISO 8601 string | no | When the transfer was recorded, in UTC |
correlationId | UUID | no | Correlation id for support |
{
"success": true,
"replayed": false,
"data": {
"id": "7c1d9a20-0000-4000-8000-000000000000",
"enrollmentId": "4af4cd8d-0000-4000-8000-000000000000",
"fromCohortId": "0f2a77b1-0000-4000-8000-000000000000",
"fromCohortName": "Autumn promotion",
"toCohortId": "3b0e51a4-0000-4000-8000-000000000000",
"toCohortName": "Summer 2026 promotion",
"effectiveAt": "2026-09-01T09:00:00.000Z",
"reason": "Resumes with the next promotion.",
"transferredBy": "6d41ab55-0000-4000-8000-000000000000",
"transferredByName": "Paul Vidal",
"createdAt": "2026-09-01T09:00:05.000Z"
},
"correlationId": "0f6b1d92-0000-4000-8000-000000000000"
}
- List the cohorts of the enrollment's program and keep those that still accept
an entry, excluding the one the member is already in.
- Transfer with a reason your team will read later, and an effective date if the
move was agreed earlier.
- Read the enrollment timeline to confirm the move — it now carries a
"transferred" entry naming both cohorts.
- A paused participation can be transferred, and that is the main use case:
someone who interrupted their journey resumes with the next promotion. Only a
closed participation is refused. The transfer does not resume a pause —
that is a separate call.
- Capacity is not checked. There is no headcount or seat limit anywhere in
the product yet, so a target cohort is never refused for being full. If your
process caps a promotion, enforce it before calling.
- No notification, email or message is sent by this endpoint, to the member
or to anyone else.
- The coach assignment, deadlines, current action, open blockers, planned
interventions and recorded attendance are untouched. Attendance already
recorded on a session of the previous cohort stays: it belongs to the
participation, not to the cohort.
- What follows the member immediately, because it is read from the current
cohort: visible live sessions, the cohort community space, the content lock
that depends on the cohort state, and the cohort dashboards. The enrollment
appears in the target cohort's views with its full history and leaves the
previous one.
- Retrying with the same
Idempotency-Key returns the original transfer and
writes no second record, audit entry or event.
- One enrollment can be transferred as many times as needed, including back to a
cohort it already left. Each move is recorded with its own effective date, so
the whole path stays readable.
- The transfer, its audit entry and its domain event are written in one
transaction.