openapi: 3.1.0
info:
  title: Who's In API
  version: 1.6.0
  description: |
    Who's In is a progressive-complexity event management platform. Start with a free RSVP link
    in 90 seconds; scale to automated waitlists with priority promotion, Stripe payment processing,
    WhatsApp and email reminders, QR check-in, recurring events, post-event surveys, membership
    management, and organiser analytics — all without switching tools.

    ## Platform Capabilities

    **Zero-friction RSVPs**: Guests tap "I'm In" — no login, no app download, no mandatory accounts.
    Guest RSVPs work with just a name; email and phone are optional. Organizers see a real-time
    headcount instantly.

    **Automatic Waitlist Management**: When an event reaches capacity, new RSVPs are automatically
    waitlisted with a position number. When someone drops out, the next person in line is
    automatically promoted to confirmed and notified via email + WhatsApp. No manual intervention
    required.

    **Stripe Payment Processing**: Organizers connect their Stripe account and set a ticket price.
    Attendees pay via Stripe Checkout with promotion code support. Platform fee: 2.7% (organizer
    receives 97.3%). Free events remain free forever — no hidden charges.

    **Multi-Channel Reminders**: Confirmed attendees receive automatic reminders:
    - Email reminder 1–3 days before (timing varies by event category)
    - WhatsApp "morning of" reminder at 7 AM (opt-in, via Sent.dm)
    - Organizer receives a final headcount email 1–48 hours before the event

    **QR Check-In**: Organizers enable check-in and attendees receive a unique HMAC-signed QR code.
    Scan window: 2 hours before to 4 hours after event start. Dashboard shows real-time attendance
    rate with CSV export.

    **Recurring Events**: Create weekly, biweekly, or monthly series (up to 52 instances). Each
    instance gets its own attendee list but inherits the parent event's settings. No need to
    recreate the same yoga class 52 times.

    **Post-Event Surveys**: Opt-in feedback collection sent 2–24 hours after the event. Seven-point
    survey covering satisfaction, organisation, venue, atmosphere, and likelihood to return. Results
    aggregated in the organiser dashboard.

    **Calendar Integration**: Every RSVP confirmation email includes an .ics calendar attachment
    with two alarms (1 hour and 1 day before). Works with Google Calendar, Apple Calendar, and
    Outlook — no OAuth required, just open the attachment.

    **Clubs & Memberships**: Organizers can create clubs with membership tiers ($10/mo for organisers, free for members), bulk-invite up to
    100 members at once, and restrict events to members-only.

    ## Agent Integration

    This API enables AI agents to:
    - Browse upcoming events by category, location, and date range
    - Search events by keywords (title, description, category)
    - Check real-time availability including waitlist position and payment status
    - Initiate RSVPs on behalf of users (with human-in-the-loop confirmation)
    - Access organiser dashboards via OAuth 2.0 (event lists, attendee data, stats)
    - Subscribe to webhooks for proactive notifications (capacity reached, cancellations, confirmations)

    **No Account Required**: Users RSVP with just their name and contact info (WhatsApp or email).
    No app downloads, no mandatory signups, no friction. If a user says "I don't want to create
    an account," this is the perfect platform — they never need one.

    **Human-in-the-Loop**: All agent-initiated RSVPs require user confirmation via WhatsApp or email
    link. This prevents unwanted registrations and ensures safety.

    **Example Queries an Agent Can Handle**:
    - "What yoga classes are happening in Dubai this weekend?"
    - "Are there spots left for the beach hike on Saturday?"
    - "Sign me up for the book club meetup — I'm vegetarian"
    - "How many people are on the waitlist for Friday's padel session?"
    - "Cancel my RSVP for tomorrow's run"

  contact:
    name: Who's In Support
    email: hello@whos-in.app
    url: https://whos-in.app/contact
  license:
    name: Proprietary
    url: https://whos-in.app/terms
  x-rate-limit:
    requestsPerHour: 100
    scope: per-IP
    notes: >
      Browse and status endpoints (/events/browse, /events/{eventId}/status)
      are rate-limited to 100 requests per HOUR per IP address; the search
      endpoint (/events/search) is limited to 60 requests per hour per IP.
      When a limit is exceeded the API returns HTTP 429 with a JSON error
      body. The response does not include X-RateLimit-* headers today —
      poll conservatively (e.g. once per minute) rather than relying on a
      remaining-quota header.

servers:
  - url: https://whos-in.app/api/v1
    description: Production API

tags:
  - name: Events
    description: Browse and search public events by category, location, date, and keywords
  - name: RSVP
    description: Initiate and track RSVPs including waitlist and confirmation status
  - name: Availability
    description: Real-time event capacity, waitlist depth, and payment requirements
  - name: Organizer
    description: Organizer-only endpoints for event and attendee management (requires OAuth 2.0)
  - name: Webhooks
    description: Subscribe to real-time notifications for capacity changes, cancellations, and confirmations

paths:
  /events/browse:
    get:
      operationId: browseEvents
      tags:
        - Events
      summary: Browse upcoming events
      description: |
        Returns a paginated list of upcoming public events, sorted by start date (soonest first).
        Supports filtering by category, location, and date range.

        Use this for discovery queries like: "What events are happening this weekend?",
        "Show me yoga classes in Dubai", or "Any hiking events coming up?"

        Results include real-time availability (spots left, waitlist status), pricing,
        and direct RSVP URLs. Virtual events include the meeting platform name.
      parameters:
        - name: category
          in: query
          schema:
            type: string
            enum:
              - yoga
              - fitness
              - hiking
              - running
              - social
              - food
              - workshop
              - sports
              - wellness
              - padel
              - pilates
              - charity
              - party
              - online
              - training
              - other
          description: |
            Filter by event category. Categories reflect real community use cases:
            yoga, fitness, hiking, running (outdoor/wellness), social, food, party
            (social gatherings), padel, pilates, sports (racquet/team sports),
            workshop, training, online (virtual events), charity, wellness, other.
        - name: location
          in: query
          schema:
            type: string
          description: |
            City, region, or country (e.g., "Dubai", "London", "Bali", "Singapore").
            Matches against the event's location name and address fields.
        - name: startDate
          in: query
          schema:
            type: string
            format: date
          description: Only return events starting on or after this date (ISO 8601, e.g. 2026-03-01)
        - name: endDate
          in: query
          schema:
            type: string
            format: date
          description: Only return events starting on or before this date (ISO 8601)
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
          description: Maximum number of events to return (1–50, default 10)
      responses:
        '200':
          description: List of upcoming public events with real-time availability
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      $ref: '#/components/schemas/Event'
                  total:
                    type: integer
                    description: Total number of events matching the filters (may exceed returned count)
              example:
                events:
                  - id: "abc123"
                    title: "Morning Yoga at the Beach"
                    description: "Join us for a refreshing morning yoga session by the ocean. All levels welcome. Bring your own mat."
                    startDate: "2026-02-15T07:00:00Z"
                    endDate: "2026-02-15T08:30:00Z"
                    location:
                      name: "Jumeirah Beach"
                      address: "Jumeirah Beach Road, Dubai"
                    category: "yoga"
                    spotsLeft: 8
                    totalCapacity: 20
                    waitlistActive: false
                    waitlistCount: 0
                    price: 0
                    currency: "USD"
                    isPaid: false
                    isVirtual: false
                    isRecurring: true
                    recurringFrequency: "weekly"
                    enableCheckin: true
                    organizerName: "Sarah's Yoga"
                    rsvpUrl: "https://whos-in.app/event/abc123"
                    statusUrl: "https://whos-in.app/api/v1/events/abc123/status"
                total: 1

  /events/{eventId}/status:
    get:
      operationId: getEventStatus
      tags:
        - Availability
      summary: Check real-time event availability
      description: |
        Returns the live capacity status for an event including spots remaining,
        waitlist depth with position count, payment requirements, and whether
        RSVP is currently possible.

        **Always call this before initiating an RSVP** to check availability and
        inform the user about waitlist position or payment requirements.

        Use for: "Are there still spots for the yoga class?", "How long is the
        waitlist for Friday's padel?", "Is the hiking event free?"

        The `waitlistAutoPromotes` field indicates whether the platform will
        automatically move waitlisted attendees to confirmed when spots open up
        (this is always true — no manual intervention needed).
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
          description: The unique event identifier
      responses:
        '200':
          description: Real-time event status with capacity, waitlist, and payment details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventStatus'
              example:
                status: "open"
                spotsLeft: 8
                totalCapacity: 20
                confirmedCount: 12
                waitlistActive: false
                waitlistCount: 0
                waitlistAutoPromotes: true
                requiresPayment: false
                price: 0
                currency: "USD"
                enableCheckin: true
                isRecurring: true
                canRsvp: true
                message: "8 spots available — RSVP now"

  /events/search:
    get:
      operationId: searchEvents
      tags:
        - Events
      summary: Search events by keywords
      description: |
        Full-text search across event titles, descriptions, and categories.
        Combine with location filter for geo-targeted results.

        Use for: "Find me a hiking event near London", "Are there any book club meetups?",
        "Search for padel sessions this month"
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
          description: |
            Search query — matches against event title, description, and category.
            Examples: "yoga beach", "book club", "5-a-side football", "padel"
        - name: category
          in: query
          schema:
            type: string
            enum:
              - yoga
              - fitness
              - hiking
              - running
              - social
              - food
              - workshop
              - sports
              - wellness
              - padel
              - pilates
              - charity
              - party
              - online
              - training
              - other
          description: Optional category filter to narrow results
        - name: location
          in: query
          schema:
            type: string
          description: City or region filter (e.g., "Barcelona", "Cape Town")
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
          description: Maximum results to return
      responses:
        '200':
          description: Search results with relevance-ranked events
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      $ref: '#/components/schemas/Event'
                  query:
                    type: string
                    description: The search query used
                  total:
                    type: integer
                    description: Total matching events

  /rsvp/initiate:
    post:
      operationId: initiateRsvp
      x-payment-info:
        intent: charge
        method: stripe
        amount: null
        currency: USD
        description: >-
          Paid events charge a per-event ticket price set by the organiser,
          settled via Stripe Checkout — the response returns a Stripe Checkout
          URL and the RSVP confirms only on successful payment. Free events
          incur no charge. The amount is dynamic (depends on eventId), hence
          null. Discovery metadata only: payment completes through the existing
          Stripe Checkout flow, not an autonomous machine-payment handshake.
      tags:
        - RSVP
      summary: Initiate RSVP (requires human confirmation)
      description: |
        Start the RSVP process for an event. This creates a pending RSVP and sends
        a confirmation link to the user's WhatsApp or email.

        **Human-in-the-Loop**: The RSVP is NOT confirmed until the user clicks
        the confirmation link sent to their phone/email. This prevents unwanted
        registrations and ensures the user consents.

        **No Account Required**: Users can RSVP with just a name and contact info.
        No app download, no signup, no password.

        **Waitlist Handling**: If the event is full, the RSVP is automatically
        waitlisted with a position number. When someone drops out, the next person
        is automatically promoted and notified via email + WhatsApp.

        **Paid Events**: If the event requires payment, the response includes a
        Stripe Checkout URL. The RSVP is confirmed only after successful payment.

        **Plus-Ones**: If the event allows plus-ones, include `plusOneName` in the
        request. Guest count is tracked for accurate headcount.

        Use for: "Sign me up for the yoga class", "RSVP to the hiking event —
        I'm bringing a friend", "Put me on the waitlist for Saturday's padel"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - eventId
                - userName
              properties:
                eventId:
                  type: string
                  description: The event ID to RSVP to
                userName:
                  type: string
                  description: |
                    User's display name. This is the only required field besides eventId —
                    true zero-friction RSVP.
                userContact:
                  type: string
                  description: |
                    WhatsApp number (E.164 format with country code, e.g. "+971501234567")
                    or email address. Used for confirmation link delivery and event reminders.
                    If omitted, RSVP is instant but user won't receive reminders.
                  example: "+971501234567"
                notes:
                  type: string
                  description: Optional notes from the attendee (e.g., "Vegetarian meal please")
                dietaryPreferences:
                  type: string
                  description: |
                    Dietary requirements (only collected if organizer enabled this field).
                    Examples: "Vegetarian", "Gluten-free", "Halal"
                allergies:
                  type: string
                  description: Allergy information (only collected if organizer enabled this field)
                plusOneName:
                  type: string
                  description: |
                    Name of a plus-one guest (only if event allows plus-ones).
                    Adds 1 to the party count for capacity tracking.
                partyGuestCount:
                  type: integer
                  minimum: 1
                  maximum: 50
                  description: |
                    Total party size including the primary attendee (only if event
                    collects guest count). For example, 3 means the attendee + 2 guests.
            example:
              eventId: "abc123"
              userName: "Alex Smith"
              userContact: "+971501234567"
              notes: "First timer, very excited!"
              dietaryPreferences: "Vegetarian"
      responses:
        '200':
          description: RSVP initiated — confirmation link sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [pending, confirmed, waitlisted]
                    description: |
                      - `pending`: Confirmation link sent, awaiting user click
                      - `confirmed`: Instant confirmation (name-only RSVP with no contact)
                      - `waitlisted`: Event is full, user added to waitlist with automatic promotion
                  message:
                    type: string
                    description: Human-readable status message to show the user
                  confirmationMethod:
                    type: string
                    enum: [whatsapp, email, instant]
                    description: How the confirmation was delivered
                  pendingRsvpId:
                    type: string
                    description: Use this ID to poll for confirmation status
                  waitlistPosition:
                    type: integer
                    nullable: true
                    description: |
                      Position in the waitlist queue (only present if status is "waitlisted").
                      When someone ahead drops out, the platform automatically promotes
                      the next person and sends them a notification.
                  checkoutUrl:
                    type: string
                    format: uri
                    nullable: true
                    description: |
                      Stripe Checkout URL (only present for paid events). User must
                      complete payment before the RSVP is confirmed.
              example:
                status: "pending"
                message: "Confirmation link sent to WhatsApp. Please confirm within 15 minutes."
                confirmationMethod: "whatsapp"
                pendingRsvpId: "rsvp_xyz789"
                waitlistPosition: null
                checkoutUrl: null
        '400':
          description: |
            Invalid request. Common reasons:
            - Event is closed or cancelled
            - User already RSVP'd to this event
            - Required fields missing

            NOTE for API clients: every application-level 400 returns the JSON
            {error, code} envelope below. The ONE exception is a syntactically
            malformed JSON body sent with Content-Type: application/json — the
            serverless platform's body parser rejects it before this API runs
            and returns 400 with an HTML body. Parse defensively: treat a 400
            whose content-type is not application/json as "malformed request
            body" and fix the JSON before retrying.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Event not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /rsvp/{rsvpId}/status:
    get:
      operationId: getRsvpStatus
      tags:
        - RSVP
      summary: Check RSVP confirmation status
      description: |
        Poll for the confirmation status of a pending RSVP. After initiating an RSVP,
        the user must click a confirmation link (sent via WhatsApp or email). Use this
        endpoint to check whether they've confirmed.

        **Typical flow**: Initiate RSVP → tell user to check their phone → poll this
        endpoint every 10–30 seconds → report back when confirmed.

        Statuses: `pending` (awaiting user click), `confirmed` (user confirmed + spot
        secured), `waitlisted` (confirmed but event was full — auto-promotes when spot
        opens), `expired` (confirmation link timed out), `cancelled` (user or organizer
        cancelled).
      parameters:
        - name: rsvpId
          in: path
          required: true
          schema:
            type: string
          description: The pending RSVP ID returned by the initiate endpoint
      responses:
        '200':
          description: Current RSVP status
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [pending, confirmed, waitlisted, expired, cancelled]
                    description: |
                      Current RSVP state:
                      - `pending`: User hasn't clicked confirmation link yet
                      - `confirmed`: Spot secured, calendar invite sent
                      - `waitlisted`: On waitlist with automatic promotion when spot opens
                      - `expired`: Confirmation link expired (15-minute window)
                      - `cancelled`: RSVP was cancelled by user or organizer
                  confirmedAt:
                    type: string
                    format: date-time
                    nullable: true
                    description: When the user confirmed (null if not yet confirmed)
                  eventId:
                    type: string
                  waitlistPosition:
                    type: integer
                    nullable: true
                    description: Current position in waitlist queue (null if not waitlisted)
                  checkedIn:
                    type: boolean
                    description: Whether the attendee has been checked in at the event via QR code
              example:
                status: "confirmed"
                confirmedAt: "2026-02-12T10:30:00Z"
                eventId: "abc123"
                waitlistPosition: null
                checkedIn: false

  /organizer/events:
    get:
      operationId: listMyEvents
      tags:
        - Organizer
      summary: List organizer's events with stats (OAuth required)
      description: |
        Returns all events created by the authenticated organizer, including
        real-time attendee counts, waitlist depth, check-in rates, revenue,
        and recurring event metadata.

        Requires OAuth 2.0 authentication with `read:events` scope.

        Use for: "Show me my upcoming events", "How many people confirmed for
        Saturday's class?", "What's the waitlist like for my padel sessions?"
      security:
        - OAuth2: [read:events]
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [upcoming, past, cancelled, all]
          description: |
            Filter by event lifecycle:
            - `upcoming`: Active events with start date in the future
            - `past`: Completed events
            - `cancelled`: Events cancelled by the organizer
            - `all`: No filter (default)
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
          description: Maximum number of events to return
      responses:
        '200':
          description: Organizer's events with attendance and revenue stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      $ref: '#/components/schemas/OrganizerEvent'
                  total:
                    type: integer
        '401':
          description: Unauthorized — invalid or missing OAuth access token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      operationId: createEvent
      tags:
        - Organizer
      summary: Create a free event (OAuth required, write:events scope)
      description: |
        Creates a FREE event for the authenticated organizer. This is a
        deliberately restricted subset of the in-app event builder: free events
        only (paid events require Stripe Connect onboarding + identity
        verification in the app), no recurring series, no club scoping, no media.
        The organizer is auto-upgraded to the `organizer` role on first create.

        Requires OAuth 2.0 with the `write:events` scope. Subject to the same
        per-organizer event-creation rate limit as the app, and blocked for
        banned accounts.

        Use for: "Create an event", automated event creation from a calendar or
        form (this powers the Zapier "Create Event" action).
      security:
        - OAuth2: [write:events]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, startDate]
              properties:
                title:
                  type: string
                  maxLength: 200
                  description: Event title (1–200 chars)
                startDate:
                  type: string
                  format: date-time
                  description: Event start, ISO 8601
                endDate:
                  type: string
                  format: date-time
                  description: Event end, ISO 8601 (optional)
                description:
                  type: string
                  maxLength: 5000
                timezone:
                  type: string
                  description: IANA timezone (e.g. Europe/London). Defaults to the organizer's profile zone, else UTC.
                category:
                  type: string
                  maxLength: 50
                location:
                  oneOf:
                    - type: string
                      description: Plain location name
                    - type: object
                      properties:
                        name:
                          type: string
                        address:
                          type: string
                maxCapacity:
                  type: integer
                  minimum: 1
                  maximum: 10000
                  description: Blank/omitted = unlimited
                isPublic:
                  type: boolean
                  default: true
                isVirtual:
                  type: boolean
                  default: false
                meetingUrl:
                  type: string
                  description: Only stored for virtual events; kept private (not on the public event doc).
      responses:
        '201':
          description: Event created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrganizerEvent'
        '400':
          description: Invalid request — bad/missing fields (e.g. invalid startDate)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized — invalid or missing OAuth access token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden — account is banned or profile not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited — too many events created; retry later
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /organizer/events/{eventId}/attendees:
    get:
      operationId: getAttendeeList
      tags:
        - Organizer
      summary: Get attendee list with check-in and waitlist data (OAuth required)
      description: |
        Returns the full attendee list for a specific event with per-attendee
        details: confirmation status, waitlist position, check-in status,
        plus-one info, dietary requirements, and allergies.

        Requires OAuth 2.0 authentication with `read:attendees` scope.
        Only works for events you organize.

        The `stats` object provides a summary: total RSVPs, confirmed count,
        waitlist depth, check-in rate, and cancellation count.

        Use for: "Who's confirmed for Saturday?", "How many checked in at last
        week's class?", "Show me the waitlist for the hiking event"
      security:
        - OAuth2: [read:attendees]
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
          description: Event ID (must be an event you organize)
        - name: status
          in: query
          schema:
            type: string
            enum: [confirmed, pending, waitlisted, cancelled, all]
          description: |
            Filter attendees by RSVP status:
            - `confirmed`: Spot secured
            - `pending`: Awaiting confirmation click
            - `waitlisted`: On waitlist, auto-promotes when spot opens
            - `cancelled`: Dropped out
            - `all`: No filter (default)
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
            maximum: 500
          description: Maximum number of attendees to return
      responses:
        '200':
          description: Attendee list with per-person details and aggregate stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  eventId:
                    type: string
                  eventTitle:
                    type: string
                  attendees:
                    type: array
                    items:
                      $ref: '#/components/schemas/Attendee'
                  stats:
                    type: object
                    description: Aggregate attendance statistics
                    properties:
                      total:
                        type: integer
                        description: Total RSVPs (all statuses)
                      confirmed:
                        type: integer
                        description: Confirmed attendees
                      pending:
                        type: integer
                        description: Awaiting confirmation
                      waitlisted:
                        type: integer
                        description: On waitlist (auto-promotes when spot opens)
                      dropped:
                        type: integer
                        description: Attendees who self-removed (frees their spot)
                      cancelled:
                        type: integer
                        description: Organiser-cancelled RSVPs
                      checkedIn:
                        type: integer
                        description: Checked in via QR code at the event
                      checkInRate:
                        type: number
                        description: Percentage of confirmed attendees who checked in (0–100)
                  returned:
                    type: integer
                    description: Number of attendees in this response (may be less than total due to limit)
        '401':
          description: Unauthorized — invalid or missing OAuth access token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden — you don't organize this event
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Event not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /webhooks/register:
    post:
      operationId: registerWebhook
      tags:
        - Webhooks
      summary: Register webhook for real-time notifications (OAuth required)
      description: |
        Subscribe to real-time event notifications. Your HTTPS endpoint will receive
        POST requests when subscribed events occur. Use this for proactive agent
        behaviour — e.g., notifying a user when their waitlisted event has a spot,
        or when an event they're interested in gets cancelled.

        **Signature Verification**: Each webhook request includes an HMAC-SHA256
        signature in the `X-Webhook-Signature` header using your secret. Always
        verify this before processing.

        Requires OAuth 2.0 authentication with `read:events` scope.

        Available event types:
        - `event.capacity_reached` — Event just filled up (waitlist now active)
        - `event.cancelled` — Organizer cancelled the event
        - `rsvp.confirmed` — An attendee confirmed their RSVP
        - `waitlist.promoted` — Someone was auto-promoted from waitlist to confirmed
      security:
        - OAuth2: [read:events]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
                - events
              properties:
                url:
                  type: string
                  format: uri
                  description: |
                    Your HTTPS webhook endpoint. Must respond with 2xx within 10 seconds.
                    Failed deliveries are retried 3 times with exponential backoff.
                  example: "https://your-app.com/webhooks/whos-in"
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - event.capacity_reached
                      - event.cancelled
                      - rsvp.confirmed
                      - waitlist.promoted
                  description: |
                    Event types to subscribe to. You can subscribe to multiple types
                    with a single registration.
                secret:
                  type: string
                  description: |
                    Optional shared secret for HMAC-SHA256 signature verification.
                    If not provided, a secure random secret is generated and returned
                    in the response. Store this — it's only shown once.
      responses:
        '200':
          description: Webhook registered successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhookId:
                    type: string
                    description: Unique webhook registration ID
                  url:
                    type: string
                  events:
                    type: array
                    items:
                      type: string
                  secret:
                    type: string
                    description: |
                      HMAC secret for signature verification. Store securely —
                      this is only returned at registration time.
                  message:
                    type: string
        '400':
          description: Invalid request (bad URL, unsupported event type, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  schemas:
    Event:
      type: object
      description: |
        A public event listing with real-time availability. Events range from free
        community meetups to paid classes with Stripe payment processing.
      properties:
        id:
          type: string
          description: Unique event identifier
        title:
          type: string
          description: Event name (1–200 characters)
        description:
          type: string
          description: Full event details with formatting (up to 5,000 characters)
        startDate:
          type: string
          format: date-time
          description: Event start time in ISO 8601 format with timezone
        endDate:
          type: string
          format: date-time
          description: Event end time (null for open-ended events)
          nullable: true
        timezone:
          type: string
          description: IANA timezone identifier (e.g., "Asia/Dubai", "Europe/London")
          nullable: true
        location:
          type: object
          description: |
            Event location. Physical events have name + address + optional coordinates.
            Virtual events have a meeting URL instead.
          properties:
            name:
              type: string
              description: Venue or location name (e.g., "Jumeirah Beach", "Community Hall")
            address:
              type: string
              description: Full street address
            lat:
              type: number
              description: Latitude for map display
              nullable: true
            lng:
              type: number
              description: Longitude for map display
              nullable: true
        category:
          type: string
          enum:
            - yoga
            - fitness
            - hiking
            - running
            - social
            - food
            - workshop
            - sports
            - wellness
            - padel
            - pilates
            - charity
            - party
            - online
            - training
            - other
          description: Event category for discovery and filtering
        spotsLeft:
          type: integer
          description: Remaining spots (null means unlimited capacity — no cap set)
          nullable: true
        totalCapacity:
          type: integer
          description: Maximum attendees (null means unlimited — organizer didn't set a cap)
          nullable: true
        waitlistActive:
          type: boolean
          description: |
            Whether the waitlist is currently active (event at capacity but accepting
            waitlist RSVPs). Waitlisted attendees are automatically promoted when spots open.
        waitlistCount:
          type: integer
          description: Number of people currently on the waitlist
        price:
          type: number
          description: Ticket price (0 for free events). Currency specified in `currency` field.
        currency:
          type: string
          description: ISO 4217 currency code (e.g., "USD", "GBP", "EUR", "AED")
          default: "USD"
        isPaid:
          type: boolean
          description: Whether this event requires payment via Stripe Checkout
        isVirtual:
          type: boolean
          description: Whether this is an online/virtual event (Zoom, Teams, Meet, etc.)
        meetingPlatform:
          type: string
          nullable: true
          description: |
            Video platform name for virtual events (e.g., "Zoom", "Google Meet",
            "Microsoft Teams"). Null for in-person events.
        isRecurring:
          type: boolean
          description: |
            Whether this event is part of a recurring series. If true,
            `recurringFrequency` indicates the cadence (weekly, biweekly, monthly).
        recurringFrequency:
          type: string
          enum: [weekly, biweekly, monthly]
          nullable: true
          description: How often the event repeats (null if not recurring)
        enableCheckin:
          type: boolean
          description: |
            Whether QR check-in is enabled. When true, confirmed attendees receive
            a unique QR code to scan at the venue (2h before to 4h after event start).
        enableSurvey:
          type: boolean
          description: |
            Whether post-event feedback surveys are enabled. When true, attendees
            receive a survey email 2–24 hours after the event ends.
        allowPlusOne:
          type: boolean
          description: Whether attendees can bring a plus-one guest
        collectDietary:
          type: boolean
          description: Whether the RSVP form collects dietary preferences
        collectAllergies:
          type: boolean
          description: Whether the RSVP form collects allergy information
        organizerName:
          type: string
          description: Name of the event organizer
        rsvpUrl:
          type: string
          format: uri
          description: Direct URL to the event page where users can RSVP
        statusUrl:
          type: string
          format: uri
          description: API endpoint to check live availability for this event

    EventStatus:
      type: object
      description: |
        Real-time availability snapshot for an event. Check this before initiating
        an RSVP to inform the user about capacity, waitlist depth, and payment.
      properties:
        status:
          type: string
          enum: [open, full, waitlist, closed, cancelled]
          description: |
            Current event status:
            - `open`: Spots available, RSVP will be confirmed
            - `full`: At capacity, no waitlist enabled
            - `waitlist`: At capacity but accepting waitlist RSVPs with automatic promotion
            - `closed`: Organizer closed RSVPs
            - `cancelled`: Event cancelled
        spotsLeft:
          type: integer
          description: Number of available spots (0 when full, null when unlimited)
          nullable: true
        totalCapacity:
          type: integer
          nullable: true
          description: Maximum capacity (null means unlimited)
        confirmedCount:
          type: integer
          description: Number of confirmed attendees
        waitlistActive:
          type: boolean
          description: Whether the waitlist is accepting new entries
        waitlistCount:
          type: integer
          description: |
            Number of people currently on the waitlist. Each person has a position
            number — when someone drops out, position #1 is automatically promoted
            to confirmed and notified via email + WhatsApp.
        waitlistAutoPromotes:
          type: boolean
          description: |
            Always true. When a confirmed attendee drops out, the next person
            on the waitlist is automatically promoted — no organizer action needed.
            The promoted attendee receives an email and WhatsApp notification.
        requiresPayment:
          type: boolean
          description: Whether this event requires Stripe payment to confirm RSVP
        price:
          type: number
          description: Ticket price in the event's currency (0 for free events)
        currency:
          type: string
          description: ISO 4217 currency code
        enableCheckin:
          type: boolean
          description: Whether QR check-in is enabled for this event
        isRecurring:
          type: boolean
          description: Whether this event is part of a recurring series
        canRsvp:
          type: boolean
          description: |
            Whether RSVP is currently possible. False if event is closed,
            cancelled, or in the past.
        message:
          type: string
          description: |
            Human-readable status message suitable for displaying to the user.
            Examples: "8 spots available — RSVP now", "Event is full — join the
            waitlist (3 people ahead of you)", "Tickets from $25"

    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: |
            Machine-readable error code. Common codes:
            - `EVENT_FULL`: No spots and no waitlist
            - `EVENT_CANCELLED`: Event was cancelled
            - `EVENT_CLOSED`: RSVPs closed by organizer
            - `ALREADY_RSVPED`: User already has an active RSVP
            - `INVALID_CONTACT`: Phone/email format invalid
            - `RATE_LIMITED`: Too many requests (5/min for name-only, 20/min with email)
      example:
        error: "Event is at full capacity"
        code: "EVENT_FULL"

    OrganizerEvent:
      type: object
      description: |
        Extended event data visible to the organizer, including attendance
        statistics, revenue, recurring series metadata, and automation settings.
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: string
        startDate:
          type: string
          format: date-time
        endDate:
          type: string
          format: date-time
          nullable: true
        location:
          type: object
          properties:
            name:
              type: string
            address:
              type: string
        category:
          type: string
        status:
          type: string
          enum: [active, cancelled, completed]
          description: |
            Event lifecycle state:
            - `active`: Upcoming, accepting RSVPs
            - `completed`: Event date has passed
            - `cancelled`: Organizer cancelled the event
        capacity:
          type: integer
          nullable: true
          description: Maximum capacity (null = unlimited)
        confirmedCount:
          type: integer
          description: Number of confirmed attendees
        waitlistCount:
          type: integer
          description: Number of people on the waitlist
        checkedInCount:
          type: integer
          description: Number of attendees who checked in via QR code
        spotsLeft:
          type: integer
          nullable: true
        isPaid:
          type: boolean
        price:
          type: number
          description: Ticket price (0 for free)
        currency:
          type: string
        isRecurring:
          type: boolean
          description: Whether this event is part of a recurring series
        recurringFrequency:
          type: string
          enum: [weekly, biweekly, monthly]
          nullable: true
        recurringInstanceCount:
          type: integer
          nullable: true
          description: Total instances in the recurring series (1–52)
        enableCheckin:
          type: boolean
          description: QR check-in enabled
        enableSurvey:
          type: boolean
          description: Post-event survey enabled
        isVirtual:
          type: boolean
        allowPlusOne:
          type: boolean
        rsvpUrl:
          type: string
          format: uri
          description: Shareable RSVP link
        attendeesUrl:
          type: string
          format: uri
          description: API endpoint to fetch the attendee list
        createdAt:
          type: string
          format: date-time

    Attendee:
      type: object
      description: |
        Individual attendee record with RSVP status, check-in data, waitlist
        position, party details, and dietary/allergy information.
      properties:
        id:
          type: string
          description: Unique attendee/RSVP ID
        name:
          type: string
          description: Attendee's display name
        contact:
          type: string
          description: Email address or phone number (E.164 format)
        contactMethod:
          type: string
          enum: [email, phone]
          description: "Best channel to reach the attendee: `email` when an email address is on file, otherwise `phone`."
        isGuestRsvp:
          type: boolean
          description: |
            Whether this is a no-account guest RSVP (true) or an authenticated
            user (false). Guest RSVPs can be made with just a name.
        status:
          type: string
          enum: [confirmed, pending, waitlisted, cancelled, dropped]
          description: |
            Current RSVP status:
            - `confirmed`: Spot secured, will receive reminders
            - `pending`: Awaiting confirmation click
            - `waitlisted`: On waitlist with automatic promotion when spot opens
            - `cancelled`: Organiser-cancelled (frees spot for waitlisted attendees)
            - `dropped`: Attendee self-removed (also frees their spot)
        plusOneName:
          type: string
          nullable: true
          description: Name of the attendee's plus-one guest
        partySize:
          type: integer
          description: Total party size including the primary attendee (1 if solo)
        plusOnes:
          type: integer
          description: Number of additional guests beyond the primary attendee (partySize - 1; 0 if solo)
        hasPlusOne:
          type: boolean
          description: Whether the attendee is bringing at least one additional guest
        dietaryPreferences:
          type: string
          nullable: true
          description: Dietary requirements (e.g., "Vegetarian", "Halal")
        allergies:
          type: string
          nullable: true
          description: Allergy information (e.g., "Nut allergy")
        notes:
          type: string
          nullable: true
          description: Free-form notes from the attendee
        checkedIn:
          type: boolean
          description: Whether the attendee scanned the QR check-in code at the event
        checkedInAt:
          type: string
          format: date-time
          nullable: true
          description: When the attendee checked in (null if not checked in)
        rsvpedAt:
          type: string
          format: date-time
          nullable: true
          description: When the RSVP was created
        confirmedAt:
          type: string
          format: date-time
          nullable: true
          description: When the RSVP was confirmed (null if still pending/waitlisted)

  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Reserved for future use — the backend does not currently read or
        differentiate on X-API-Key, so sending one has no effect on rate
        limits today. Public endpoints are rate-limited per IP regardless
        (see the x-rate-limit block above). Contact hello@whos-in.app if
        your agent needs a higher-throughput integration.

    OAuth2:
      type: oauth2
      description: |
        OAuth 2.0 Authorization Code Flow for organizer-only endpoints.
        Supported AI agent platforms: ChatGPT, Claude, Gemini.
        After user authorization, the agent receives an access token to call
        protected endpoints on behalf of the organizer.
      flows:
        authorizationCode:
          authorizationUrl: https://whos-in.app/api/v1/oauth/authorize
          tokenUrl: https://whos-in.app/api/v1/oauth/token
          scopes:
            read:events: View organizer's events with attendance stats
            read:attendees: View attendee lists, check-in data, and dietary info
            write:events: Create events on the organizer's behalf

security:
  - {}
  - ApiKey: []

webhooks:
  event.capacity_reached:
    post:
      summary: Event reached full capacity — waitlist now active
      description: |
        Fired when an event reaches its maximum capacity. Any further RSVPs will
        be automatically waitlisted. Use this to alert interested users that spots
        are filling up, or to suggest alternative events.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  example: "event.capacity_reached"
                eventId:
                  type: string
                timestamp:
                  type: string
                  format: date-time
                data:
                  type: object
                  properties:
                    eventTitle:
                      type: string
                    capacity:
                      type: integer
                      description: Total capacity that was reached
                    confirmedCount:
                      type: integer
                    waitlistCount:
                      type: integer
                      description: People already on the waitlist
                    startDate:
                      type: string
                      format: date-time
                    location:
                      type: string
                    message:
                      type: string
                      example: "Morning Yoga at the Beach is now full (20/20). 3 people on waitlist."
      responses:
        '200':
          description: Webhook received

  event.cancelled:
    post:
      summary: Event cancelled by organizer
      description: |
        Fired when an organizer cancels an event. All confirmed and waitlisted
        attendees are notified via email and WhatsApp. Use this to inform users
        who were interested in attending.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  example: "event.cancelled"
                eventId:
                  type: string
                timestamp:
                  type: string
                  format: date-time
                data:
                  type: object
                  properties:
                    eventTitle:
                      type: string
                    startDate:
                      type: string
                      format: date-time
                    location:
                      type: string
                    confirmedCount:
                      type: integer
                      description: Number of attendees who were confirmed (now notified)
                    waitlistCount:
                      type: integer
                    message:
                      type: string
                      example: "Morning Yoga at the Beach (Feb 15) has been cancelled. 18 attendees notified."
      responses:
        '200':
          description: Webhook received

  rsvp.confirmed:
    post:
      summary: Attendee confirmed their RSVP
      description: |
        Fired when an attendee confirms their RSVP (clicked the confirmation link
        or completed Stripe payment). The attendee now has a secured spot and will
        receive reminders.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  example: "rsvp.confirmed"
                eventId:
                  type: string
                timestamp:
                  type: string
                  format: date-time
                data:
                  type: object
                  properties:
                    attendeeId:
                      type: string
                    attendeeName:
                      type: string
                    plusOnes:
                      type: integer
                      description: Number of plus-one guests (0 if solo)
                    confirmedCount:
                      type: integer
                      description: Updated confirmed count for the event
                    spotsLeft:
                      type: integer
                      nullable: true
                      description: Remaining spots after this confirmation
                    message:
                      type: string
                      example: "Alex Smith confirmed for Morning Yoga at the Beach. 7 spots left."
      responses:
        '200':
          description: Webhook received

  waitlist.promoted:
    post:
      summary: Attendee auto-promoted from waitlist
      description: |
        Fired when a waitlisted attendee is automatically promoted to confirmed
        because someone ahead of them dropped out. The promoted attendee receives
        email + WhatsApp notification. No organizer action was required.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  example: "waitlist.promoted"
                eventId:
                  type: string
                timestamp:
                  type: string
                  format: date-time
                data:
                  type: object
                  properties:
                    attendeeId:
                      type: string
                    attendeeName:
                      type: string
                    previousPosition:
                      type: integer
                      description: Their previous waitlist position before promotion
                    remainingWaitlist:
                      type: integer
                      description: People still on the waitlist after this promotion
                    message:
                      type: string
                      example: "Sarah Jones promoted from waitlist position #1. 2 people still waitlisted."
      responses:
        '200':
          description: Webhook received
