Collectives & Authorization API

Last updated: 2026-05-26

Overview

The Collectives system enables group management for families, classes, and other trusted groups. Parents (or other adult guardians) create a Collective, invite teenagers via shareable links, manage members, and grant/revoke parental consent — all through a REST API.

This system is tightly integrated with the consent pipeline (see Onboarding) and the authorization flow (see Client Development).

Data Model

┌──────────────────────┐     ┌──────────────────────────────┐
│      Collective       │     │    CollectiveMembership       │
│──────────────────────│     │──────────────────────────────│
│ id: UUID (PK)        │◄────│ collective_id: UUID (FK)      │
│ name: str             │     │ user_id: UUID (FK)            │
│ type: str             │     │ role: CollectiveRoles         │
│ parental_consent_     │     │ joined_at: datetime           │
│   granted: bool        │     └──────────────┬───────────────┘
│ created_by: UUID (FK) │                     │
└──────────┬────────────┘                     │
           │                                  │
           │  ┌──────────────────────────────┐│
           │  │         User                  ││
           │  │  (teenager or parent)         │◄┘
           │  └──────────────────────────────┘
           │
           │  ┌──────────────────────────────┐
           │  │       Invitation              │
           │  │──────────────────────────────│
           │  │ id: UUID (PK)                 │
           │  │ collective_id: UUID (FK)      │
           │  │ inviter_id: UUID (FK)         │
           ├──│ age_class: AgeClass           │
           │  │ language: str                 │
           │  │ label: str (optional)          │
           │  │ usage_limit: int (nullable)    │
           │  │ current_uses: int              │
           │  │ status: InvitationStatus      │
           │  │ created_at / expires_at        │
           │  │ accepted_at / accepted_by      │
           │  └──────────────────────────────┘
           │
           │  ┌───────────────────────────────┐
           │  │ ParentalAuthorizationRequest   │
           │  │───────────────────────────────│
           │  │ id: UUID (PK)                  │
           │  │ teenager_id: UUID (FK)         │
           │  │ status: AuthorizationReqStatus │
           │  │ created_at / expires_at         │
           │  └───────────────────────────────┘
           │
           │  ┌───────────────────────────────┐
           │  │ ParentalConsent                │
           │  │───────────────────────────────│
           │  │ parent_id: UUID (FK, PK)       │
           │  │ teenager_id: UUID (FK, PK)     │
           │  │ created_at                     │
           │  └───────────────────────────────┘

Enums

Enum Values
CollectiveRoles owner, admin, member
InvitationStatus pending, active, accepted, expired, revoked
AuthorizationRequestStatus pending, fulfilled, expired
AgeClass child <11, preteen 11-14, teenager 15-17, adult

Role Hierarchy

  • Owner: Full control — create invitations, manage members, grant parental consent, view all data
  • Admin: Same as owner (roles are currently treated equivalently in access checks)
  • Member: Read-only — can accept invitations and be managed by owners/admins

Authorization Model

All collectives endpoints enforce parent-only access:

  • The authenticated user must have user_type == UserType.parent
  • The user must be an owner or admin of the target collective
  • Access is verified on every request via get_user_managed_collectives()

Teenagers and unauthorized parents receive 403 Forbidden.

Endpoints

All endpoints are prefixed with /api/v1/collectives.

List Collectives

GET /collectives
Authorization: Bearer <token>

Returns all collectives where the authenticated user is an owner or admin.

Response:

{
    "collectives": [
        {
            "id": "uuid",
            "name": "Smith Family",
            "type": "family",
            "parental_consent_granted": false
        }
    ]
}

List Members

GET /collectives/{collective_id}/members
Authorization: Bearer <token>

Returns all members of a collective with their consent status. Requires owner/admin access.

Response:

{
    "members": [
        {
            "id": "uuid",
            "name": "User a1b2c3d4",
            "role": "member",
            "status": "pending_consent",
            "needs_parental_consent": true
        }
    ]
}

Member status values:

Status Meaning
active User has consented to TOS and has parental consent (if needed)
pending_tos User has not yet accepted the Terms of Service
pending_consent User needs parental consent but it has not been granted

Create Invitation

POST /collectives/{collective_id}/invitations
Authorization: Bearer <token>
Content-Type: application/json

{
    "age_class": "teenager 15-17",
    "language": "fr",
    "label": "For Emma"
}

Creates an invitation link for a teenager. The age_class determines the consent requirements. Setting age_class to an age class that requires parental consent automatically sets usage_limit = 1.

Response:

{
    "invitation_id": "uuid",
    "invite_url": "https://<frontend>/join?invitation_id=<uuid>"
}

The invite_url is a shareable link that the teenager uses to join the collective. The URL points to the teenager frontend (FRONTEND_TEENAGER_URL) with the invitation ID as a query parameter.

List Invitations

GET /collectives/{collective_id}/invitations
Authorization: Bearer <token>

Returns all active/pending invitations for a collective. Requires owner/admin access.

Response:

{
    "invitations": [
        {
            "id": "uuid",
            "age_class": "teenager 15-17",
            "language": "fr",
            "label": "For Emma",
            "status": "pending",
            "usage_limit": 1,
            "current_uses": 0,
            "created_at": "2026-05-26T12:00:00Z",
            "invite_url": "https://<frontend>/join?invitation_id=<uuid>"
        }
    ]
}

Only invitations with status pending or active are returned. Expired and revoked invitations are filtered out.

Revoke Invitation

DELETE /collectives/{collective_id}/invitations/{invitation_id}
Authorization: Bearer <token>

Revokes an invitation, preventing further use. Requires owner/admin access.

Authorize Teenager

POST /collectives/authorize-teenager
Authorization: Bearer <token>
Content-Type: application/json

{
    "teenager_id": "uuid"
}

Authorizes a teenager (identified by their user UUID) and adds them to the parent’s collective. Only callable by users with user_type == parent.

Response:

{
    "message": "Teenager authorized successfully",
    "collective_id": "uuid"
}

Invitation Lifecycle

  Create          Accept           Join Collective
  ──────►  PENDING  ──────►  ACCEPTED  ──────► membership record
                │                     
                ├──►  ACTIVE (in use)  
                ├──►  EXPIRED (timeout)
                └──►  REVOKED (manual delete)
  1. Parent creates invitationPOST /collectives/{id}/invitations. Status: pending.
  2. Teenager opens link — Frontend reads invitation_id from URL query param. Passed to POST /auth/login as invitation_id.
  3. Backend validates — Checks invitation status, usage limit, expiration. If valid, auto-grants consent (the invitation serves as implicit parental consent).
  4. Invitation consumedcurrent_uses incremented. If current_uses >= usage_limit, status changes to accepted.
  5. Parent can revokeDELETE /collectives/{id}/invitations/{inv_id} changes status to revoked.

Usage Limits

  • usage_limit = null: Unlimited uses (invitation can be reused)
  • usage_limit = 1: One-time use (auto-set when age_class requires parental consent)
  • current_uses: Tracks how many times the invitation has been accepted

Authorization Request Flow

The ParentalAuthorizationRequest model provides a separate mechanism for teenagers to request authorization from parents without exposing internal user IDs:

1. Teenager clicks "Ask parent" in UI
   → Frontend calls createAuthorizationRequest(token)
   → Backend creates ParentalAuthorizationRequest (status: pending)
   → Returns authorization_id (safe UUID, not the teenager's user ID)

2. Parent receives authorization link
   → URL contains authorization_id param
   → AuthorizeTeenagerModal opens on parent frontend

3. Parent approves
   → POST /collectives/authorize-teenager {"teenager_id": "<resolved>"}
   → Teenager added to parent's collective
   → ParentalAuthorizationRequest status → fulfilled

This flow is documented in detail in Client Development — Collectives API.

Schemas

Request Schemas

Schema Fields
InvitationCreateRequest age_class: AgeClass?, language: str, label: str?
MemberConsentUpdateRequest has_parental_consent: bool
AuthorizeTeenagerRequest teenager_id: UUID
ParentalConsentGrantedRequest granted: bool

Response Schemas

Schema Fields
CollectiveListResponse collectives: List[CollectiveRecord]
CollectiveRecord id: UUID, name: str, type: str, parental_consent_granted: bool
CollectiveMemberResponse members: List[MemberRecord]
MemberRecord id: UUID, name: str, role: CollectiveRoles, status: str, needs_parental_consent: bool
InvitationResponse invitation_id: UUID, invite_url: str
CollectiveInvitationListResponse invitations: List[CollectiveInvitationRecord]
CollectiveInvitationRecord id: UUID, age_class: AgeClass?, language: str, label: str?, status: str, usage_limit: int?, current_uses: int, created_at: datetime, invite_url: str