Skip to content

Transfer team ownership to a member

POST
/api/team/{id}/transfer-ownership/
curl --request POST \
--url http://localhost:8000/api/team/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/transfer-ownership/ \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "to_admin_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" }'

Owner-only. Atomically hands the OWNER role to a current MEMBER of the team: in one transaction the caller is demoted to MEMBER and the target promoted to OWNER. The single-owner convention holds — there’s never two owners (or none) mid-flight. The path <id> is the team UUID; the body carries the target to_admin_id.

This is the unblock for a departing founder: a sole-owner who’s leaving the org transfers first, then DELETE /api/me/ succeeds (it 409s while you still own a team with other members). An audit row (from / to / by / at) is written and survives the former owner’s later account deletion.

Returns 200 with the new owner’s membership, 400 if to_admin_id isn’t a member of the team or is the caller, 403 if the caller isn’t the owner, 404 if the team doesn’t exist.

id
required
string format: uuid
object
to_admin_id
required

Admin UUID of the team member to promote to OWNER. Must be a current MEMBER of the team and not the caller. The swap is atomic — the caller is demoted to MEMBER in the same transaction.

string format: uuid
Examples
ExamplePromoteAMemberToOwner

Promote a member to owner

{
"to_admin_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9"
}
Media typeapplication/json

A team member — an (Admin, Team, role) triple, since the same Admin can sit in several teams with different roles.

Consumers pass a dict {"admin": Admin, "team": Team, "role": str} (or an object with those attributes) — much simpler than trying to thread a TeamMembership through joins in every view.

object
id
required

Admin UUID — same person can have memberships on several teams; this is the identity, not the membership row.

string format: uuid
email
required

Admin’s email (Firebase identity).

string format: email
display_name
required

Admin’s display name.

string
firebase_uid
required

Internal Firebase identity id. Useful for client-side identity mapping; not for auth.

string
team_id
required

UUID of the team this membership row points at. The invite-accept response carries this so the SPA can pass it as redirect_team_id to POST /api/me/onboarding/ once the invitee finishes their per-Admin onboarding funnel on their personal workspace, switching them into the team they just joined.

string format: uuid
role
required

Role on this team — owner or member.

string
is_owner
required

Convenience boolean — role == 'owner'. Saves the client a string compare.

boolean
session_count
required

How many sessions this admin uploaded under THIS team’s tenancy. Sessions on other teams the admin belongs to don’t count.

integer
created_at
required

When the admin joined the platform (not when they joined this team).

string format: date-time
updated_at
required

Last admin profile mutation.

string format: date-time
Examples
Example200—NewOwner'sMembership

200 — New owner's membership

{
"id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
"email": "assistant@club.example",
"display_name": "Assistant",
"team_id": "b3f1c0a2-0000-4000-8000-000000000001",
"role": "owner",
"is_owner": true
}
Media typeapplication/json
object
detail
required

Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).

string
Examples

400 — Can't transfer to yourself

{
"to_admin_id": [
"Cannot transfer ownership to yourself."
]
}
Media typeapplication/json
object
detail
required

Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).

string
Examplegenerated
{
"detail": "example"
}
Media typeapplication/json
object
detail
required

Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).

string
Examples
Example403—Owner-onlyAction

403 — Owner-only action

{
"detail": "Only the team owner can do that"
}
Media typeapplication/json
object
detail
required

Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).

string
Examples
Example404—NoSuchTeam

404 — No such team

{
"detail": "team_not_found"
}