Skip to content

Accept an invite as the signed-in Admin

POST
/api/invites/{token}/accept/
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.

token
required
string
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
ExampleJoinedTheTeam

Joined the team

{
"admin": {
"id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
"email": "newcoach@club.example",
"display_name": "New Coach"
},
"role": "member"
}
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

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 0
  • subscription_suspended - Stripe dunning in flight (past_due / unpaid)
  • seat_limit_reached - Adding another team member would exceed plan.max_seats
  • client_limit_reached - Adding another Client would exceed plan.max_client_profiles
  • feature_not_in_plan - The tier does not include this whole feature surface
string
Allowed values: quota_exhausted subscription_suspended seat_limit_reached client_limit_reached feature_not_in_plan
reset_at
required

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).

string format: date-time
nullable
current_plan
required

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.

string
current_role
required

The active plan’s role string. See current_plan.

string
feature

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).

string
Examples
Example402—SeatLimitReachedAtAcceptTime

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"
}
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
Example404—UnknownInviteToken

404 — Unknown invite token

{
"detail": "invite_not_found"
}
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
Example410—InviteExpiredOrCancelled

410 — Invite expired or cancelled

{
"detail": "invite_unusable"
}