Transfer team ownership to a member
const url = 'http://localhost:8000/api/team/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/transfer-ownership/';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"to_admin_id":"0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Request Bodyrequired
Section titled “Request Bodyrequired”object
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.
Examples
Promote a member to owner
{ "to_admin_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9"}object
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.
Examplegenerated
to_admin_id=2489E9AD-2EE2-8E00-8EC9-32D5F69181C0object
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.
Responses
Section titled “Responses”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
Admin UUID — same person can have memberships on several teams; this is the identity, not the membership row.
Admin’s email (Firebase identity).
Admin’s display name.
Internal Firebase identity id. Useful for client-side identity mapping; not for auth.
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.
Role on this team — owner or member.
Convenience boolean — role == 'owner'. Saves the client a string compare.
How many sessions this admin uploaded under THIS team’s tenancy. Sessions on other teams the admin belongs to don’t count.
When the admin joined the platform (not when they joined this team).
Last admin profile mutation.
Examples
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}object
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).
Examples
400 — Can't transfer to yourself
{ "to_admin_id": [ "Cannot transfer ownership to yourself." ]}400 — Target isn't a member of this team
{ "to_admin_id": [ "That admin is not a member of this team." ]}object
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).
Examplegenerated
{ "detail": "example"}object
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).
Examples
403 — Owner-only action
{ "detail": "Only the team owner can do that"}object
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).
Examples
404 — No such team
{ "detail": "team_not_found"}