Collectives & Authorization API

Last updated: 2026-05-27

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

Auto-generated from SQLAlchemy models by Paracelsus during documentation build.

erDiagram
  collective_memberships {
    UUID collective_id PK,FK
    UUID user_id PK,FK
    UUID invitation_id FK "nullable"
    DATETIME created_at "nullable"
    ENUM role
    DATETIME updated_at "nullable"
  }

  collectives {
    UUID id PK
    UUID parent_id FK "nullable"
    DATETIME created_at "nullable"
    VARCHAR(255) name
    BOOLEAN parental_consent_granted
    VARCHAR(50) type
    DATETIME updated_at "nullable"
  }

  consents {
    UUID id PK
    UUID user_id FK
    VARCHAR(45) consent_ip "nullable"
    DATETIME created_at
  }

  invitations {
    UUID id PK
    UUID accepted_by FK "nullable"
    UUID collective_id FK
    UUID inviter_id FK
    DATETIME accepted_at "nullable"
    ENUM age_class "nullable"
    DATETIME created_at "nullable"
    INTEGER current_uses
    DATETIME expires_at "nullable"
    VARCHAR(255) label "nullable"
    VARCHAR(10) language
    ENUM status
    INTEGER usage_limit "nullable"
  }

  parental_authorization_requests {
    UUID id PK
    UUID teenager_id FK
    DATETIME created_at
    DATETIME expires_at "nullable"
    ENUM status
  }

  parental_consents {
    UUID parent_id PK,FK
    UUID teenager_id PK,FK
    VARCHAR(45) consent_ip "nullable"
    DATETIME created_at "nullable"
  }

  users {
    UUID id PK
    ENUM age_class "nullable"
    VARCHAR(100) country "nullable"
    DATETIME created_at "nullable"
    VARCHAR(2) language
    DATETIME last_seen_at "nullable"
    DATETIME updated_at "nullable"
    ENUM user_type
  }

  users ||--o| collective_memberships : user_id
  collectives ||--o| collective_memberships : collective_id
  invitations ||--o{ collective_memberships : invitation_id
  collectives ||--o{ collectives : parent_id
  users ||--o{ consents : user_id
  collectives ||--o{ invitations : collective_id
  users ||--o{ invitations : inviter_id
  users ||--o{ invitations : accepted_by
  users ||--o{ parental_authorization_requests : teenager_id
  users ||--o| parental_consents : parent_id
  users ||--o| parental_consents : teenager_id

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