Accept an invite as the signed-in Admin
const url = 'http://localhost:8000/api/invites/example/accept/';const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
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/invites/example/accept/ \ --header 'Authorization: Bearer <token>'Joins the signed-in coach to the team described by the invite token. Never stamps onboarded_at. active_team flips to invite.team ONLY when the accepter has already completed onboarding (onboarded_at IS NOT NULL); a first-time invitee stays on their auto-provisioned personal workspace so the onboarding funnel’s owner-gated PATCHes hit a team they own, then the funnel’s final POST /api/me/onboarding/ carries redirect_team_id and atomically flips into the joined team along with stamping onboarded_at. The Firebase email used at sign-in does NOT need to match the email the invite was sent to. Returns the new TeamMembership. 404 for unknown tokens, 410 for expired / cancelled invites.
402 — seat_limit_reached when the team has filled plan.max_seats since the invite was issued (e.g. a Pro → Free downgrade left stale Pro-era invites pending). The invite stays in pending so re-issuing after an upgrade still works.
Auth: must be signed in; the token in the path selects the team.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”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
Joined the team
{ "admin": { "id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", "email": "newcoach@club.example", "display_name": "New Coach" }, "role": "member"}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
One of five canonical strings. The front-end paywall switches copy + CTA on this value (exact string equality — do not localise). quota_exhausted / seat_limit_reached / client_limit_reached / feature_not_in_plan lead to an upgrade CTA; subscription_suspended leads to a Billing Portal CTA so the buyer can update their payment method.
quota_exhausted- Session quota for the period reached 0subscription_suspended- Stripe dunning in flight (past_due / unpaid)seat_limit_reached- Adding another team member would exceed plan.max_seatsclient_limit_reached- Adding another Client would exceed plan.max_client_profilesfeature_not_in_plan- The tier does not include this whole feature surface
ISO-8601 UTC timestamp the team’s current period ends. Null when the team has no live subscription (legacy un-migrated row). On Free + paid this is the renewal moment; quota refreshes to exactly plan.session_quota (NOT additive — leftover sessions don’t roll over).
The active plan’s role string (free, starter, professional, organization, strategic). Same value as current_role — kept as two separate fields for forward-compatibility with a future named-plan split where current_plan could carry an SKU and current_role the tier label.
The active plan’s role string. See current_plan.
Which feature surface is locked. Present ONLY on feature_not_in_plan, so one modal can name what’s gated without a detail string per feature (#773). The only value today is agents (aikynetix/agents/services.py::AGENTS_FEATURE).
Examples
Pro team downgraded to Free with a pending invite still in flight
{ "detail": "seat_limit_reached", "reset_at": "2026-06-01T00:00:00Z", "current_plan": "free", "current_role": "free"}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
404 — Unknown invite token
{ "detail": "invite_not_found"}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
410 — Invite expired or cancelled
{ "detail": "invite_unusable"}