openapi: "3.1.0"
info:
  title: GroundCam API
  version: "1.0.0"
  description: |
    The GroundCam API allows you to create and manage video assistance sessions
    programmatically from your CRM, ticketing system, or any other tool.

    ## Authentication
    All requests require a Bearer token (API key) in the Authorization header:
    ```
    Authorization: Bearer gc_live_your_api_key_here
    ```

    API keys can be created from the GroundCam dashboard under Settings > API.

    ## Rate Limiting
    Requests are rate-limited based on your subscription plan. Rate limit info is
    included in response headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.

    ## Errors
    Errors follow a consistent format:
    ```json
    { "error": { "message": "Description", "code": "ERROR_CODE" } }
    ```
  contact:
    name: GroundCam Support
    url: https://groundcam.io
servers:
  - url: https://api.groundcam.io/v1
    description: Production
security:
  - BearerAuth: []
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: API key (gc_live_...)
  schemas:
    Session:
      type: object
      description: Full session object returned by GET /sessions/{id}
      properties:
        id:
          type: string
          description: Unique session identifier
          example: "Xk9mPq2rT4wZ"
        status:
          type: string
          enum: [pending, active, completed, expired, cancelled]
          description: Current session status
          example: "completed"
        agentId:
          type: string
          description: Agent user ID
          example: "f7Kj2mNpQ1xR"
        agentName:
          type: string
          nullable: true
          description: Agent display name
          example: "Marie Martin"
        clientPhone:
          type: string
          description: Client phone number (E.164 format)
          example: "+33612345678"
        clientName:
          type: string
          nullable: true
          description: Client name
          example: "Jean Dupont"
        clientEmail:
          type: string
          nullable: true
          description: Client email address
          example: "jean.dupont@email.com"
        crmId:
          type: string
          nullable: true
          description: External CRM identifier
          example: "TICKET-12345"
        ticketUrl:
          type: string
          nullable: true
          description: Link to support ticket or CRM record
          example: "https://mycrm.com/tickets/12345"
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: Custom key-value metadata (strings, numbers, booleans)
          example:
            department: "support"
            priority: "high"
        mode:
          type: string
          description: "Session mode: camera (default, mobile video) or screen (desktop screen sharing). When mode is screen, clientPhone is optional and no SMS is sent."
          enum: [camera, screen]
          default: camera
          example: "camera"
        source:
          type: string
          description: How the session was created
          enum: [dashboard, api, scheduled]
          example: "api"
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: Session creation timestamp
          example: "2026-02-14T10:30:00.000Z"
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: Last update timestamp. Useful for incremental polling and detecting recent mutations.
          example: "2026-02-14T10:46:12.000Z"
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: Session expiration timestamp
          example: "2026-02-14T11:30:00.000Z"
        startedAt:
          type: string
          format: date-time
          nullable: true
          description: Session start timestamp (when video started)
          example: "2026-02-14T10:32:15.000Z"
        endedAt:
          type: string
          format: date-time
          nullable: true
          description: Session end timestamp
          example: "2026-02-14T10:45:30.000Z"
        admittedAt:
          type: string
          format: date-time
          nullable: true
          description: When the client was admitted to the session
          example: "2026-02-14T10:32:00.000Z"
        duration:
          type: number
          nullable: true
          description: Total session duration in seconds
          example: 1548
        agentDuration:
          type: number
          nullable: true
          description: Time agent was connected (seconds)
          example: 798
        clientDuration:
          type: number
          nullable: true
          description: Time client was connected (seconds)
          example: 750
        clientRating:
          type: number
          nullable: true
          minimum: 1
          maximum: 5
          description: Client satisfaction rating (1-5)
          example: 5
        clientComment:
          type: string
          nullable: true
          description: Client feedback comment
          example: "Very helpful, resolved my issue quickly!"
        closureTypeName:
          type: string
          nullable: true
          description: Session closure category
          example: "Problem resolved"
        sessionTags:
          type: array
          items:
            type: string
          description: Tags associated with the session
          example: ["installation", "urgent"]
        notes:
          type: string
          nullable: true
          description: Agent notes about the session
          example: "Client had a plumbing issue under the kitchen sink"
        clientUrl:
          type: string
          nullable: true
          description: |
            URL for the client to join the session (contains the access token).
            Null if the session was created without a client token (rare).
            Valid until `expiresAt`. Store this URL at creation time — it is always recoverable via this endpoint.
          example: "https://app.groundcam.io/l/Xk9mPq2rT4wZ?token=abc123def456ghi789jkl012"
        agentUrl:
          type: string
          description: URL for the agent dashboard page for this session
          example: "https://app.groundcam.io/dashboard/lobbies/Xk9mPq2rT4wZ"
        screenshotsCount:
          type: integer
          description: Number of screenshots captured
          example: 3
        recordingsCount:
          type: integer
          description: Number of recordings available
          example: 1
      required:
        - id
        - status
        - agentId

    CreateSessionRequest:
      type: object
      properties:
        clientPhone:
          type: string
          description: Client phone number (E.164 format required, e.g. +33612345678). Spaces are stripped before validation.
          example: "+33612345678"
        clientName:
          type: string
          description: Client name (optional)
          example: "Jean Dupont"
        clientEmail:
          type: string
          description: Client email address (optional). Stored on the session for email-based invitation.
          example: "jean.dupont@email.com"
        agentId:
          type: string
          description: Agent user ID
          example: "agent_xyz789"
        crmId:
          type: string
          description: External CRM/ticket identifier (optional)
          example: "TICKET-12345"
        ticketUrl:
          type: string
          description: Link to support ticket or CRM record (optional)
          example: "https://mycrm.com/tickets/12345"
        metadata:
          type: object
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
          description: "Custom key-value metadata. Flat object only, max 20 keys, less than 4 KB serialized. Values must be strings, numbers, or booleans."
          example:
            department: "support"
            priority: "high"
        mode:
          type: string
          enum: [camera, screen]
          description: "Session mode. 'camera' (default): client shares mobile camera, SMS sent. 'screen': client shares desktop screen, no SMS sent, clientPhone optional."
          default: "camera"
          example: "camera"
        sendSms:
          type: boolean
          description: Whether to send an SMS to the client (default true). Set to false to create the session and get the clientUrl without sending an SMS. Forced to false when mode is screen.
          default: true
          example: false
        language:
          type: string
          enum: [fr, en, es, de, it, pt, nl, pl, ru, zh]
          description: Language for the SMS sent to the client
          default: "fr"
          example: "fr"
        ttlMinutes:
          type: integer
          enum: [5, 10, 15, 20]
          description: Session link validity in minutes
          default: 20
          example: 20
        sessionTags:
          type: array
          items:
            type: string
          description: Tags to associate with the session at creation
          example: ["installation", "urgent"]
      required:
        - agentId
      description: |
        Note: clientPhone is required when mode is "camera" (default). When mode is "screen", clientPhone is optional.
        clientEmail is always optional. When provided, it is stored on the session and can be used to send the session link by email.

    BulkSessionInput:
      type: object
      description: |
        Session input for bulk creation. Same fields as CreateSessionRequest but without clientEmail and mode.
        Bulk sessions always use camera mode.
      properties:
        clientPhone:
          type: string
          description: Client phone number (E.164 format required, e.g. +33612345678). Spaces are stripped before validation.
          example: "+33612345678"
        clientName:
          type: string
          description: Client name (optional)
          example: "Jean Dupont"
        agentId:
          type: string
          description: Agent user ID
          example: "agent_xyz789"
        crmId:
          type: string
          description: External CRM/ticket identifier (optional)
          example: "TICKET-12345"
        ticketUrl:
          type: string
          description: Link to support ticket or CRM record (optional)
          example: "https://mycrm.com/tickets/12345"
        metadata:
          type: object
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
          description: "Custom key-value metadata. Flat object only, max 20 keys, less than 4 KB serialized. Values must be strings, numbers, or booleans."
          example:
            department: "support"
            priority: "high"
        sendSms:
          type: boolean
          description: Whether to send an SMS to the client (default true)
          default: true
          example: false
        language:
          type: string
          enum: [fr, en, es, de, it, pt, nl, pl, ru, zh]
          description: Language for the SMS sent to the client
          default: "fr"
          example: "fr"
        ttlMinutes:
          type: integer
          enum: [5, 10, 15, 20]
          description: Session link validity in minutes
          default: 20
          example: 20
        sessionTags:
          type: array
          items:
            type: string
          description: Tags to associate with the session at creation
          example: ["installation", "urgent"]
      required:
        - agentId
        - clientPhone

    Screenshot:
      type: object
      properties:
        id:
          type: string
          description: Screenshot identifier
          example: "screenshot_abc123"
        url:
          type: string
          description: |
            Signed download URL on the groundcam.io domain. Following the URL returns
            a 302 redirect to the actual file. URLs are valid for `url_ttl` seconds
            (default 3600s, configurable via the `url_ttl` query parameter on the
            endpoint). After expiration, re-fetch this endpoint to obtain a fresh URL.
          example: "https://groundcam.io/sc/abc123/screenshot_xyz?exp=1742031600&sig=..."
        timestamp:
          type: string
          format: date-time
          description: When the screenshot was captured
          example: "2026-02-14T10:35:20Z"
        location:
          type: object
          nullable: true
          properties:
            latitude:
              type: number
              example: 48.8566
            longitude:
              type: number
              example: 2.3522
          description: GPS coordinates (if available, null otherwise)
        tags:
          type: array
          items:
            type: string
          description: Tags associated with the screenshot
          example: ["defect", "product"]
      required:
        - id
        - url
        - timestamp

    Recording:
      type: object
      properties:
        id:
          type: string
          description: Recording identifier
          example: "recording_xyz789"
        url:
          type: string
          description: |
            Firebase Storage download URL. URLs use a download token and remain valid
            as long as the file exists (no expiry). Do not cache URLs indefinitely —
            always re-fetch from this endpoint if you need a fresh reference.
          example: "https://firebasestorage.googleapis.com/..."
        duration:
          type: number
          nullable: true
          description: Recording duration in seconds (null if not yet available)
          example: 798
        size:
          type: number
          nullable: true
          description: File size in bytes (null if not yet available)
          example: 15728640
        createdAt:
          type: string
          format: date-time
          description: Recording creation timestamp
          example: "2026-02-14T10:45:30Z"
      required:
        - id
        - url

    CreditBalance:
      type: object
      description: Credit balance as returned by GET /credits
      properties:
        available:
          type: integer
          description: Total available credits (subscription + packs)
          example: 150
        subscription:
          type: object
          properties:
            remaining:
              type: integer
              description: Remaining subscription credits this period
              example: 75
            quota:
              type: integer
              description: Monthly credit quota from plan
              example: 100
            resetsAt:
              type: string
              format: date-time
              nullable: true
              description: When subscription credits reset
              example: "2026-04-01T00:00:00.000Z"
        pack:
          type: object
          properties:
            remaining:
              type: integer
              description: Remaining credits from purchased packs
              example: 75
      required:
        - available
        - subscription
        - pack

    WebhookEndpoint:
      type: object
      properties:
        id:
          type: string
          description: Webhook endpoint identifier
          example: "webhook_abc123"
        url:
          type: string
          description: Target URL for webhook delivery
          example: "https://mycrm.com/webhooks/groundcam"
        events:
          type: array
          items:
            type: string
            enum:
              - session.created
              - session.client_waiting
              - session.active
              - session.completed
              - session.rated
              - session.cancelled
              - session.expired
              - session.updated
              - session.deleted
              - appointment.created
              - appointment.updated
              - appointment.cancelled
          description: Events to subscribe to
          example: ["session.created", "session.completed"]
        active:
          type: boolean
          description: Whether the webhook is active
          example: true
        createdAt:
          type: string
          format: date-time
          description: Webhook creation timestamp
          example: "2026-02-14T09:00:00Z"
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: Last update timestamp for the webhook configuration
          example: "2026-03-12T11:42:00Z"
      required:
        - id
        - url
        - events
        - active

    CreateWebhookRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: Target URL for webhook delivery (must be HTTPS)
          example: "https://mycrm.com/webhooks/groundcam"
        events:
          type: array
          items:
            type: string
            enum:
              - session.created
              - session.client_waiting
              - session.active
              - session.completed
              - session.rated
              - session.cancelled
              - session.expired
              - session.updated
              - session.deleted
              - appointment.created
              - appointment.updated
              - appointment.cancelled
          description: Events to subscribe to
          example: ["session.created", "session.completed"]
      required:
        - url
        - events

    WebhookDelivery:
      type: object
      description: |
        A delivery attempt for a webhook event. Each event subscribed to by an
        endpoint produces one delivery record per endpoint. Records track the
        retry lifecycle: initial attempt, scheduled retries, and final state.
      properties:
        id:
          type: string
          description: Unique delivery identifier (also sent as the `X-GroundCam-Delivery` header on the HTTP call — use it as your idempotency key).
          example: "Ev9kLm3nPq7r"
        eventType:
          type: string
          description: Event type that triggered the delivery (e.g. `session.completed`, `appointment.updated`).
          example: "appointment.updated"
        status:
          type: string
          enum:
            - delivered
            - failed
            - exhausted
          description: |
            Current state of the delivery:
            - **delivered**: a `2xx` response was received (terminal).
            - **failed**: an attempt failed and another retry is scheduled (see `nextAttemptAt`).
            - **exhausted**: the retry budget or the 15-minute deadline has been reached, no more attempts will be made (terminal).
          example: "delivered"
        httpStatus:
          type: integer
          nullable: true
          description: HTTP status returned by the most recent attempt. `null` if the request could not reach the endpoint (DNS, timeout, etc.).
          example: 200
        attempts:
          type: integer
          description: Number of attempts performed so far (initial attempt counts as 1).
          example: 1
        maxAttempts:
          type: integer
          nullable: true
          description: Maximum number of attempts that will be made. Typically `3` for regular events (initial + 2 retries) and `1` for `webhook.test` deliveries.
          example: 3
        lastError:
          type: string
          nullable: true
          description: Error message from the most recent attempt (HTTP status string or transport error).
          example: "HTTP 500"
        lastAttemptAt:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the most recent delivery attempt.
          example: "2026-03-12T14:05:21Z"
        nextAttemptAt:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the next scheduled retry. `null` when the delivery is in a terminal state (`delivered` or `exhausted`).
          example: "2026-03-12T14:06:21Z"
        deadlineAt:
          type: string
          format: date-time
          nullable: true
          description: Absolute deadline after which no further retries will be attempted (typically creation time + 15 minutes).
          example: "2026-03-12T14:20:21Z"
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: Timestamp at which the delivery was first created (the initial attempt happened at this time).
          example: "2026-03-12T14:05:21Z"
        payload:
          type: object
          nullable: true
          description: Full envelope that was (or will be) sent to the endpoint — same body the consumer receives over HTTP.
          properties:
            event:
              type: string
              example: "appointment.updated"
            timestamp:
              type: string
              format: date-time
              example: "2026-03-12T14:05:21Z"
            data:
              type: object
              description: Event-specific payload (see the event reference for the schema of each event type).
              additionalProperties: true
      required:
        - id
        - eventType
        - status
        - attempts

    WebhookEvent:
      type: object
      description: |
        Payload sent to your webhook URL when an event occurs.
        Each delivery includes HTTP headers for signature verification:
        - `X-GroundCam-Signature`: `sha256=<hex_digest>` (HMAC-SHA256 of `{timestamp}.{body}`)
        - `X-GroundCam-Timestamp`: Unix timestamp of the delivery
        - `X-GroundCam-Event`: Event type
        - `X-GroundCam-Delivery`: Unique delivery ID
      properties:
        event:
          type: string
          enum:
            - session.created
            - session.client_waiting
            - session.active
            - session.completed
            - session.rated
            - session.cancelled
            - session.expired
            - session.updated
            - session.deleted
            - appointment.created
            - appointment.updated
            - appointment.cancelled
            - webhook.test
          description: Event type
          example: "session.completed"
        timestamp:
          type: string
          format: date-time
          description: Event timestamp (ISO 8601)
          example: "2026-02-14T10:45:30Z"
        data:
          type: object
          description: |
            Event-specific payload. Fields vary by event type:

            **Session events:**
            - **session.created** (via POST /sessions): id, status, agentId, agentName, clientPhone, clientName, clientEmail, clientUrl, agentUrl, crmId, ticketUrl, metadata, sessionTags, source ("api"), mode, createdAt, expiresAt
            - **session.created** (via POST /sessions/bulk): id, status, agentId, agentName, clientPhone, clientName, clientUrl, agentUrl, crmId, ticketUrl, metadata, sessionTags, source ("api"), createdAt, expiresAt. Note: no clientEmail, no mode.
            - **session.created** (via scheduled appointment): id, status, mode, agentId, agentName, clientPhone, clientName, clientUrl, agentUrl, crmId, ticketUrl, metadata, appointmentId, source ("scheduled"), createdAt, expiresAt. Note: no clientEmail, no sessionTags.
            - **session.client_waiting**: id, status, agentId, clientName, clientWaitingAt, appointmentId
            - **session.active**: id, status, agentId, agentName, clientName, startedAt, appointmentId
            - **session.completed**: id, status, agentId, agentName, clientName, crmId, ticketUrl, metadata, duration, agentDuration, clientDuration, closureTypeName, notes, sessionTags, completedAt, appointmentId. Emitted both by manual session end (POST /sessions/{id}/end) and by the auto-expiry cron when an expired session had at least one connection.
            - **session.rated**: id, status, agentId, agentName, clientName, crmId, ticketUrl, metadata, clientRating, clientComment, ratedAt, appointmentId
            - **session.cancelled**: id, status, agentId, crmId, ticketUrl, metadata, previousStatus, cancelledAt, appointmentId
            - **session.expired**: id, status, agentId, crmId, ticketUrl, metadata, expiredAt, appointmentId. Emitted both by manual expire actions and by the auto-expiry cron when an expired session never had any connection.
            - **session.updated**: id, status, updatedFields, notes, sessionTags, closureTypeName, updatedAt, appointmentId. Emitted by PATCH /sessions/{id} for session metadata changes (notes / sessionTags / closureTypeName) **and** by the dashboard when an agent edits or deletes a screenshot's tags — including on sessions already `completed`. For screenshot edits the payload also includes either `screenshot: { id, tags, tagDetails? }` or `screenshotDeleted: { id }`, and `updatedFields` contains `"screenshots"`.
            - **session.deleted**: id, agentId, crmId, ticketUrl, metadata, deletedBy, deletedAt, appointmentId
            Most session.* events include `appointmentId` (string|null). The session.created event only includes it when the session originates from a scheduled appointment.

            **Appointment events:**
            - **appointment.created**: id, organizationId, status, agentId, agentName, clientPhone, clientName, clientEmail, crmId, ticketUrl, scheduledAt, durationMinutes, notifyBy, mode, notes, metadata, createdAt, createdBy, updatedAt
            - **appointment.updated** (via PATCH): full appointment object (including mode and `lobbyId`) + updatedFields[]. `lobbyId` is always present in the payload: `null` until the associated lobby is created, then populated. Integrators can detect the linking transition by checking `updatedFields.includes("lobbyId")`.
            - **appointment.updated** (via cron/lobby creation): id, status ("lobby_created"), lobbyId, mode, agentId, agentName, clientPhone, clientName, crmId, ticketUrl, metadata, clientUrl, agentUrl, updatedFields, updatedAt
            - **appointment.cancelled**: id, status, agentId, agentName, clientPhone, clientName, clientEmail, crmId, ticketUrl, metadata, scheduledAt, durationMinutes, mode, previousStatus, cancellationReason, lobbyId, cancelledAt

            **Test event:**
            - **webhook.test**: message, webhookId, organizationId
      required:
        - event
        - timestamp
        - data

    Agent:
      type: object
      properties:
        id:
          type: string
          description: Agent user ID (use as agentId when creating sessions)
          example: "abc123def456"
        email:
          type: string
          format: email
          description: Agent email address
          example: "jean.dupont@acme.fr"
        displayName:
          type: string
          description: Agent display name
          example: "Jean Dupont"
        role:
          type: string
          enum: [owner, admin, agent, viewer]
          description: Agent role in the organization
          example: "agent"
        disabled:
          type: boolean
          description: Whether the agent account is disabled
          example: false
        joinedAt:
          type: string
          format: date-time
          description: When the agent joined the organization
          example: "2025-06-15T10:00:00Z"
      required:
        - id
        - email
        - displayName
        - role
        - disabled

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            message:
              type: string
              description: Human-readable error message
              example: "Invalid phone number format"
            code:
              type: string
              description: Machine-readable error code
              example: "INVALID_PHONE_NUMBER"
          required:
            - message
            - code
      required:
        - error

    SessionListItem:
      type: object
      description: Subset of session fields returned in list responses
      properties:
        id:
          type: string
          example: "Xk9mPq2rT4wZ"
        status:
          type: string
          enum: [pending, active, completed, expired, cancelled]
          example: "completed"
        agentId:
          type: string
          example: "f7Kj2mNpQ1xR"
        agentName:
          type: string
          nullable: true
          example: "Marie Martin"
        clientPhone:
          type: string
          example: "+33612345678"
        clientName:
          type: string
          nullable: true
          example: "Jean Dupont"
        crmId:
          type: string
          nullable: true
          example: "TICKET-12345"
        ticketUrl:
          type: string
          nullable: true
          example: "https://mycrm.com/tickets/12345"
        metadata:
          type: object
          nullable: true
        source:
          type: string
          example: "api"
        createdAt:
          type: string
          format: date-time
          nullable: true
          example: "2026-02-14T10:30:00.000Z"
        expiresAt:
          type: string
          format: date-time
          nullable: true
          example: "2026-02-14T11:30:00.000Z"
        startedAt:
          type: string
          format: date-time
          nullable: true
        endedAt:
          type: string
          format: date-time
          nullable: true
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: Last modification timestamp
        duration:
          type: number
          nullable: true
        clientRating:
          type: number
          nullable: true
        screenshotsCount:
          type: integer
          example: 3
        recordingsCount:
          type: integer
          example: 1

    OrganizationInfo:
      type: object
      description: Organization info with plan and features
      properties:
        id:
          type: string
          description: Organization unique identifier
          example: "org_abc123"
        name:
          type: string
          nullable: true
          description: Organization display name
          example: "Acme Corp"
        slug:
          type: string
          nullable: true
          description: Subdomain slug
          example: "acme"
        membersCount:
          type: integer
          description: Number of active (non-disabled) members
          example: 5
        plan:
          type: object
          properties:
            id:
              type: string
              nullable: true
              example: "pro"
            name:
              type: string
              nullable: true
              example: "Pro"
            status:
              type: string
              nullable: true
              enum: [active, trialing, past_due, canceled, null]
              example: "active"
        features:
          type: object
          description: Features enabled by the active plan
          properties:
            screenshots:
              type: boolean
              example: true
            audio:
              type: boolean
              example: true
            drawing:
              type: boolean
              example: true
            laserPointer:
              type: boolean
              example: true
            chat:
              type: boolean
              example: true
            geolocation:
              type: boolean
              example: true
            maxSessionDuration:
              type: integer
              description: "Maximum session duration in minutes. 0 = unlimited."
              example: 60
            maxSmsPerSession:
              type: integer
              description: "Maximum SMS per session. 0 = unlimited."
              example: 3
            satisfactionSurvey:
              type: boolean
              example: true
            customSubdomain:
              type: boolean
              example: false
            apiAccess:
              type: boolean
              example: true
            detailedStatistics:
              type: boolean
              example: true
        credits:
          type: object
          properties:
            available:
              type: integer
              description: Total available credits (subscription + pack)
              example: 100
            subscriptionCredits:
              type: integer
              example: 80
            packCredits:
              type: integer
              example: 20

    Stats:
      type: object
      description: Aggregated session statistics for a given period
      properties:
        period:
          type: string
          enum: [day, week, month]
          example: "month"
        from:
          type: string
          format: date-time
          description: Start of the period (inclusive)
          example: "2026-03-01T00:00:00.000Z"
        to:
          type: string
          format: date-time
          description: End of the period (inclusive)
          example: "2026-03-12T15:30:00.000Z"
        sessions:
          type: object
          properties:
            total:
              type: integer
              example: 42
            completed:
              type: integer
              example: 35
            cancelled:
              type: integer
              example: 5
            expired:
              type: integer
              example: 2
        duration:
          type: object
          properties:
            totalSeconds:
              type: integer
              description: Total duration of all completed sessions in seconds
              example: 12600
            averageSeconds:
              type: integer
              nullable: true
              description: Average duration of completed sessions in seconds. Null if no completed sessions.
              example: 360
        satisfaction:
          type: object
          properties:
            average:
              type: number
              nullable: true
              description: Average client satisfaction rating (1–5). Null if no ratings.
              example: 4.2
            count:
              type: integer
              description: Number of sessions with a rating
              example: 30
        sms:
          type: object
          properties:
            sent:
              type: integer
              description: Total SMS sent during the period
              example: 44

    Appointment:
      type: object
      description: Scheduled video session appointment
      properties:
        id:
          type: string
          description: Unique appointment identifier
          example: "apt_Xk9mPq2r"
        status:
          type: string
          enum: [scheduled, notified, lobby_created, completed, cancelled]
          description: Current appointment status
          example: "scheduled"
        agentId:
          type: string
          description: Assigned agent user ID
          example: "f7Kj2mNpQ1xR"
        agentName:
          type: string
          description: Agent display name
          example: "Marie Martin"
        organizationId:
          type: string
          description: Organization ID
          example: "org_AbC123xYz"
        clientPhone:
          type: string
          nullable: true
          description: Client phone number (E.164 format). Required for camera mode, optional for screen mode.
          example: "+33612345678"
        clientName:
          type: string
          nullable: true
          description: Client name
          example: "Jean Dupont"
        clientEmail:
          type: string
          nullable: true
          description: Client email. Required for screen mode and when notifyBy includes email.
          example: "jean.dupont@email.com"
        scheduledAt:
          type: string
          format: date-time
          description: Scheduled start time (ISO 8601, must be aligned to 5-minute slots)
          example: "2026-03-20T14:30:00.000Z"
        durationMinutes:
          type: integer
          enum: [5, 10, 15, 20]
          description: Expected duration in minutes
          example: 15
        notifyBy:
          type: string
          enum: [sms, email, both]
          description: How the client should be notified 5 minutes before. Forced to "email" for screen mode.
          example: "both"
        mode:
          type: string
          enum: [camera, screen]
          description: "Session mode: camera (client uses phone camera) or screen (client shares their screen). Defaults to camera. Screen mode requires clientEmail and forces notifyBy to email."
          default: "camera"
          example: "camera"
        crmId:
          type: string
          nullable: true
          description: External CRM/ticket identifier
          example: "ODOO-12345"
        ticketUrl:
          type: string
          nullable: true
          description: Link to support ticket or CRM record
          example: "https://myodoo.com/helpdesk/12345"
        metadata:
          type: object
          nullable: true
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
          description: "Custom key-value metadata. Flat object only, max 20 keys, less than 4 KB serialized. Values must be strings, numbers, or booleans."
          example:
            odoo_ticket_id: "12345"
            department: "support"
        notes:
          type: string
          nullable: true
          description: Notes about the appointment
          example: "Client needs help with installation"
        lobbyId:
          type: string
          nullable: true
          description: Video session ID (created automatically 5 min before)
          example: "Xk9mPq2rT4wZ"
        notifiedAt:
          type: string
          format: date-time
          nullable: true
          description: When the reminder was sent
        cancelledAt:
          type: string
          format: date-time
          nullable: true
          description: When the appointment was cancelled
        cancellationReason:
          type: string
          nullable: true
          description: Reason for cancellation
        cancelledBy:
          type: string
          nullable: true
          description: Who cancelled the appointment (format api:{apiKeyId} or user:{userId}), null if not cancelled
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp
        updatedAt:
          type: string
          format: date-time
          description: Last update timestamp
        createdBy:
          type: string
          description: "Who created the appointment (format: api:{apiKeyId} or user:{userId})"
          example: "api:key_AbC123"
      required:
        - id
        - status
        - agentId
        - organizationId
        - scheduledAt
        - durationMinutes
        - notifyBy

    AppointmentCancelResponse:
      type: object
      description: Partial appointment data returned when an appointment is cancelled
      properties:
        id:
          type: string
          description: Appointment identifier
          example: "apt_Xk9mPq2r"
        status:
          type: string
          description: Appointment status (always "cancelled")
          example: "cancelled"
        crmId:
          type: string
          nullable: true
          description: External CRM/ticket identifier
        metadata:
          type: object
          nullable: true
          description: Custom key-value metadata
        cancellationReason:
          type: string
          nullable: true
          description: Reason for cancellation
        mode:
          type: string
          enum: [camera, screen]
          description: Session mode
          example: "camera"
        lobbyId:
          type: string
          nullable: true
          description: Video session ID if one was created
        cancelledAt:
          type: string
          format: date-time
          description: When the appointment was cancelled
          example: "2026-03-20T14:35:00.000Z"
      required:
        - id
        - status
        - cancelledAt

    CreateAppointmentRequest:
      type: object
      properties:
        agentId:
          type: string
          description: Agent user ID to assign the appointment to
          example: "f7Kj2mNpQ1xR"
        clientPhone:
          type: string
          description: Client phone number (E.164 format). Required for camera mode, optional for screen mode.
          example: "+33612345678"
        clientName:
          type: string
          description: Client name (optional)
          example: "Jean Dupont"
        clientEmail:
          type: string
          description: Client email. Required for screen mode and when notifyBy is "email" or "both".
          example: "jean.dupont@email.com"
        scheduledAt:
          type: string
          format: date-time
          description: Appointment start time (ISO 8601). Must be in the future and aligned to 5-minute slots (minutes must be 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, or 55).
          example: "2026-03-20T14:30:00.000Z"
        durationMinutes:
          type: integer
          enum: [5, 10, 15, 20]
          description: Expected duration in minutes
          example: 15
        notifyBy:
          type: string
          enum: [sms, email, both]
          description: How to notify the client 5 minutes before the appointment. Forced to "email" for screen mode.
          example: "both"
        mode:
          type: string
          enum: [camera, screen]
          description: "Session mode: camera (client uses phone camera) or screen (client shares their screen). Defaults to camera."
          default: "camera"
          example: "camera"
        crmId:
          type: string
          description: External CRM identifier (optional, useful for Odoo/Salesforce integration)
          example: "ODOO-12345"
        ticketUrl:
          type: string
          description: Link to support ticket or CRM record (optional)
          example: "https://myodoo.com/helpdesk/12345"
        metadata:
          type: object
          additionalProperties: true
          description: Custom key-value metadata (max 20 keys, 4KB total)
          example:
            odoo_ticket_id: "12345"
            department: "support"
        notes:
          type: string
          description: Notes about the appointment (optional)
          example: "Client needs help with installation"
      required:
        - agentId
        - scheduledAt
        - durationMinutes

    AvailableSlot:
      type: object
      properties:
        start:
          type: string
          format: date-time
          description: Slot start time
          example: "2026-03-20T14:00:00.000Z"
        end:
          type: string
          format: date-time
          description: Slot end time
          example: "2026-03-20T14:30:00.000Z"
        available:
          type: boolean
          description: Whether the slot is available
          example: true

paths:
  /agents:
    get:
      summary: List agents
      description: |
        Retrieve the list of members in your organization. Use this endpoint to obtain
        agent IDs (`id` field) required when creating sessions via `POST /sessions`.

        By default, disabled accounts are excluded from the results.
      operationId: listAgents
      tags:
        - Agents
      parameters:
        - name: role
          in: query
          description: Filter by role
          schema:
            type: string
            enum: [owner, admin, agent, viewer]
          example: "agent"
        - name: includeDisabled
          in: query
          description: Include disabled accounts (excluded by default)
          schema:
            type: boolean
            default: false
          example: false
      responses:
        "200":
          description: Agents retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      agents:
                        type: array
                        items:
                          $ref: "#/components/schemas/Agent"
                      total:
                        type: integer
                        description: Total number of agents returned
                        example: 3
              examples:
                success:
                  value:
                    data:
                      agents:
                        - id: "f7Kj2mNpQ1xR"
                          email: "marie.martin@acme.fr"
                          displayName: "Marie Martin"
                          role: "admin"
                          disabled: false
                          joinedAt: "2025-05-10T08:30:00.000Z"
                        - id: "h9Ls4oPr3tUv"
                          email: "jean.dupont@acme.fr"
                          displayName: "Jean Dupont"
                          role: "agent"
                          disabled: false
                          joinedAt: "2025-06-15T10:00:00.000Z"
                        - id: "k2Mw6qXs5uYz"
                          email: "sophie.bernard@acme.fr"
                          displayName: "Sophie Bernard"
                          role: "agent"
                          disabled: false
                          joinedAt: "2025-09-01T14:00:00.000Z"
                      total: 3
                filtered:
                  summary: Filtered by role=agent
                  value:
                    data:
                      agents:
                        - id: "h9Ls4oPr3tUv"
                          email: "jean.dupont@acme.fr"
                          displayName: "Jean Dupont"
                          role: "agent"
                          disabled: false
                          joinedAt: "2025-06-15T10:00:00.000Z"
                        - id: "k2Mw6qXs5uYz"
                          email: "sophie.bernard@acme.fr"
                          displayName: "Sophie Bernard"
                          role: "agent"
                          disabled: false
                          joinedAt: "2025-09-01T14:00:00.000Z"
                      total: 2
        "400":
          description: Invalid filter parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                invalidRole:
                  value:
                    error:
                      message: "role must be one of: owner, admin, agent, viewer"
                      code: "INVALID_FIELD"
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sessions:
    post:
      summary: Create a session
      description: |
        Create a new video assistance session and send an SMS invitation to the client.
        The SMS contains a unique link for the client to join the session.
      operationId: createSession
      tags:
        - Sessions
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: |
            Unique key for idempotent request processing. If a request with the same key
            was successfully processed within the last 24 hours, the cached response is returned.
            Also accepted as X-Idempotency-Key.
          schema:
            type: string
          example: "req_abc123_unique_id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSessionRequest"
            examples:
              basic:
                summary: Basic session
                value:
                  clientPhone: "+33612345678"
                  clientName: "Jean Dupont"
              withCRM:
                summary: With CRM integration
                value:
                  clientPhone: "+33612345678"
                  clientName: "Jean Dupont"
                  crmId: "TICKET-12345"
                  ticketUrl: "https://mycrm.com/tickets/12345"
                  agentId: "agent_xyz789"
                  metadata:
                    department: "support"
                    priority: "high"
              screenSharing:
                summary: Screen sharing session with email
                value:
                  agentId: "agent_xyz789"
                  clientName: "Jean Dupont"
                  clientEmail: "jean.dupont@email.com"
                  mode: "screen"
      responses:
        "201":
          description: Session created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      status:
                        type: string
                      clientUrl:
                        type: string
                        description: URL for the client to join the session
                      agentUrl:
                        type: string
                        description: URL for the agent dashboard
                      createdAt:
                        type: string
                        format: date-time
                      expiresAt:
                        type: string
                        format: date-time
                      crmId:
                        type: string
                        nullable: true
                      ticketUrl:
                        type: string
                        nullable: true
                      clientEmail:
                        type: string
                        nullable: true
                        description: Client email address
                      metadata:
                        type: object
                        nullable: true
                      sms:
                        type: object
                        properties:
                          status:
                            type: string
                            enum: [sent, skipped]
                          to:
                            type: string
                      sessionTags:
                        type: array
                        items:
                          type: string
                        description: Tags associated with this session
                        example: ["installation", "urgent"]
                      mode:
                        type: string
                        enum: [camera, screen]
                        description: Session mode
                        example: "camera"
              examples:
                withSms:
                  summary: Session with SMS sent
                  value:
                    data:
                      id: "Xk9mPq2rT4wZ"
                      status: "pending"
                      clientUrl: "https://app.groundcam.io/l/Xk9mPq2rT4wZ?token=abc123def456ghi789jkl012"
                      agentUrl: "https://app.groundcam.io/dashboard/lobbies/Xk9mPq2rT4wZ"
                      createdAt: "2026-03-07T14:30:00.000Z"
                      expiresAt: "2026-03-07T15:30:00.000Z"
                      crmId: "TICKET-12345"
                      ticketUrl: "https://mycrm.com/tickets/12345"
                      metadata:
                        department: "support"
                        priority: "high"
                      mode: "camera"
                      sms:
                        status: "sent"
                        to: "+33612345678"
                withoutSms:
                  summary: Session without SMS (sendSms=false)
                  value:
                    data:
                      id: "Yp4nRs7tU8vA"
                      status: "pending"
                      clientUrl: "https://app.groundcam.io/l/Yp4nRs7tU8vA?token=mno345pqr678stu901vwx234"
                      agentUrl: "https://app.groundcam.io/dashboard/lobbies/Yp4nRs7tU8vA"
                      createdAt: "2026-03-07T14:35:00.000Z"
                      expiresAt: "2026-03-07T15:35:00.000Z"
                      crmId: null
                      ticketUrl: null
                      metadata: null
                      mode: "camera"
                      sms:
                        status: "skipped"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missingPhone:
                  value:
                    error:
                      message: "clientPhone is required"
                      code: "MISSING_FIELD"
                invalidPhone:
                  value:
                    error:
                      message: "Invalid phone format. Use international format (e.g., +33612345678)"
                      code: "INVALID_PHONE"
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "402":
          description: Insufficient credits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                noCredits:
                  value:
                    error:
                      message: "Insufficient credits"
                      code: "INSUFFICIENT_CREDITS"
        "409":
          description: Idempotency conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                idempotencyConflict:
                  value:
                    error:
                      message: "A request with this idempotency key is currently being processed"
                      code: "IDEMPOTENCY_CONFLICT"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Agent permission error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                agentNotMember:
                  value:
                    error:
                      message: "Agent is not a member of this organization"
                      code: "AGENT_NOT_MEMBER"
                agentDisabled:
                  value:
                    error:
                      message: "Agent account is disabled"
                      code: "AGENT_DISABLED"
                insufficientRole:
                  value:
                    error:
                      message: "Agent role 'viewer' cannot create sessions"
                      code: "INSUFFICIENT_ROLE"
        "404":
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                orgNotFound:
                  value:
                    error:
                      message: "Organization not found"
                      code: "ORG_NOT_FOUND"
        "500":
          description: SMS delivery error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                smsNotConfigured:
                  value:
                    error:
                      message: "SMS is not configured for this organization"
                      code: "SMS_NOT_CONFIGURED"
                smsFailed:
                  value:
                    error:
                      message: "Failed to send SMS"
                      code: "SMS_FAILED"

    get:
      summary: List sessions
      description: |
        Retrieve a paginated list of sessions for your organization.
        Results are ordered by creation date (newest first).
      operationId: listSessions
      tags:
        - Sessions
      parameters:
        - name: status
          in: query
          description: Filter by session status
          schema:
            type: string
            enum: [pending, active, completed, expired, cancelled]
          example: "completed"
        - name: agentId
          in: query
          description: Filter by agent ID
          schema:
            type: string
          example: "agent_xyz789"
        - name: crmId
          in: query
          description: Filter by CRM/ticket identifier
          schema:
            type: string
          example: "TICKET-12345"
        - name: createdAfter
          in: query
          description: Filter sessions created after this timestamp (ISO 8601)
          schema:
            type: string
            format: date-time
          example: "2026-02-01T00:00:00Z"
        - name: createdBefore
          in: query
          description: Filter sessions created before this timestamp (ISO 8601)
          schema:
            type: string
            format: date-time
          example: "2026-02-28T23:59:59Z"
        - name: updatedAfter
          in: query
          description: |
            Filter sessions modified after this timestamp (ISO 8601). Useful for incremental sync.
            Cannot be combined with createdAfter/createdBefore.
            When used, results are ordered by updatedAt instead of createdAt.
          schema:
            type: string
            format: date-time
          example: "2026-03-12T00:00:00Z"
        - name: limit
          in: query
          description: Number of results per page (max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: startAfter
          in: query
          description: Cursor for pagination (session ID from previous response)
          schema:
            type: string
          example: "session_def456"
        - name: metadata.*
          in: query
          description: |
            Filter by custom metadata values using dot notation.
            Example: `?metadata.department=support&metadata.odoo_ticket_id=12345`
            Multiple metadata filters are combined with AND logic.
          schema:
            type: string
          example: "support"
      responses:
        "200":
          description: Sessions retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      sessions:
                        type: array
                        items:
                          $ref: "#/components/schemas/SessionListItem"
                      pagination:
                        type: object
                        properties:
                          limit:
                            type: integer
                          hasMore:
                            type: boolean
                          nextCursor:
                            type: string
                            nullable: true
              examples:
                success:
                  value:
                    data:
                      sessions:
                        - id: "Xk9mPq2rT4wZ"
                          status: "completed"
                          agentId: "f7Kj2mNpQ1xR"
                          agentName: "Marie Martin"
                          clientPhone: "+33612345678"
                          clientName: "Jean Dupont"
                          crmId: "TICKET-12345"
                          ticketUrl: null
                          metadata: null
                          source: "api"
                          createdAt: "2026-03-07T10:30:00.000Z"
                          expiresAt: "2026-03-07T11:30:00.000Z"
                          startedAt: "2026-03-07T10:32:15.000Z"
                          endedAt: "2026-03-07T10:45:30.000Z"
                          duration: 1548
                          clientRating: 5
                          screenshotsCount: 3
                          recordingsCount: 1
                        - id: "Yp4nRs7tU8vA"
                          status: "pending"
                          agentId: "f7Kj2mNpQ1xR"
                          agentName: "Marie Martin"
                          clientPhone: "+33698765432"
                          clientName: "Sophie Bernard"
                          crmId: null
                          ticketUrl: null
                          metadata: null
                          source: "dashboard"
                          createdAt: "2026-03-07T09:15:00.000Z"
                          expiresAt: "2026-03-07T10:15:00.000Z"
                          startedAt: null
                          endedAt: null
                          duration: null
                          clientRating: null
                          screenshotsCount: 0
                          recordingsCount: 0
                      pagination:
                        limit: 20
                        hasMore: false
                        nextCursor: null
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sessions/{id}:
    get:
      summary: Get session details
      description: Retrieve detailed information about a specific session.
      operationId: getSession
      tags:
        - Sessions
      parameters:
        - name: id
          in: path
          required: true
          description: Session ID
          schema:
            type: string
          example: "abc123def456"
      responses:
        "200":
          description: Session retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Session"
              examples:
                success:
                  value:
                    data:
                      id: "Xk9mPq2rT4wZ"
                      status: "completed"
                      agentId: "f7Kj2mNpQ1xR"
                      agentName: "Marie Martin"
                      clientPhone: "+33612345678"
                      clientName: "Jean Dupont"
                      clientUrl: "https://app.groundcam.io/l/Xk9mPq2rT4wZ?token=abc123def456ghi789jkl012"
                      agentUrl: "https://app.groundcam.io/dashboard/lobbies/Xk9mPq2rT4wZ"
                      crmId: "TICKET-12345"
                      ticketUrl: "https://mycrm.com/tickets/12345"
                      metadata:
                        department: "support"
                        priority: "high"
                      source: "api"
                      createdAt: "2026-03-07T10:30:00.000Z"
                      expiresAt: "2026-03-07T11:30:00.000Z"
                      startedAt: "2026-03-07T10:32:15.000Z"
                      endedAt: "2026-03-07T10:45:30.000Z"
                      admittedAt: "2026-03-07T10:32:00.000Z"
                      duration: 1548
                      agentDuration: 798
                      clientDuration: 750
                      clientRating: 5
                      clientComment: "Very helpful, resolved my issue quickly!"
                      closureTypeName: "Problem resolved"
                      sessionTags: ["installation", "urgent"]
                      notes: "Client had a plumbing issue under the kitchen sink"
                      screenshotsCount: 3
                      recordingsCount: 1
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                notFound:
                  value:
                    error:
                      message: "Session not found"
                      code: "NOT_FOUND"
    patch:
      summary: Update session metadata
      description: |
        Update metadata for a session (notes, tags, closure type).
        At least one field must be provided.
      operationId: updateSession
      tags:
        - Sessions
      parameters:
        - name: id
          in: path
          required: true
          description: Session ID
          schema:
            type: string
          example: "abc123def456"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                notes:
                  type: string
                  nullable: true
                  description: Session notes
                  example: "Client had a plumbing issue, resolved remotely"
                sessionTags:
                  type: array
                  items:
                    type: string
                  description: Tags associated with the session
                  example: ["plumbing", "resolved"]
                closureTypeName:
                  type: string
                  nullable: true
                  description: Session closure category name
                  example: "Problem resolved"
            examples:
              updateNotes:
                summary: Update notes and tags
                value:
                  notes: "Client had a plumbing issue, resolved remotely"
                  sessionTags: ["plumbing", "resolved"]
                  closureTypeName: "Problem resolved"
      responses:
        "200":
          description: Session updated successfully — returns the full updated session
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Session"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                invalidClosureType:
                  value:
                    error:
                      message: "Closure type \"Unknown type\" not found"
                      code: "INVALID_CLOSURE_TYPE"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sessions/{id}/agent-auth-url:
    post:
      summary: Generate a single-use agent authentication URL
      description: |
        Generates a short-lived (5 minutes), single-use authentication URL for the session's agent.
        When clicked, this URL authenticates the agent and redirects them directly to the lobby page,
        eliminating the need to log in manually. Useful for CRM integrations (e.g., Odoo) where the
        agent clicks a link from an external system.

        The `authUrl` should be used instead of the raw `agentUrl` when the agent may not have an
        active GroundCam session in their browser.

        **Security:** Tokens are single-use, expire after 5 minutes, and are tied to a specific
        agent and session. Rate limited to 5 generations per session per hour.
      operationId: generateAgentAuthUrl
      tags:
        - Sessions
      parameters:
        - name: id
          in: path
          required: true
          description: Session ID
          schema:
            type: string
          example: "abc123def456"
      responses:
        "201":
          description: Auth URL generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      authUrl:
                        type: string
                        description: Single-use authentication URL for the agent
                      agentUrl:
                        type: string
                        description: Standard agent URL (for reference)
                      expiresAt:
                        type: string
                        format: date-time
                        description: Token expiry timestamp (ISO 8601)
                      lobbyId:
                        type: string
                        description: Session/lobby ID
              examples:
                success:
                  value:
                    data:
                      authUrl: "https://acme.groundcam.io/api/auth/agent-token?token=aat_xxxx"
                      agentUrl: "https://acme.groundcam.io/dashboard/lobbies/abc123def456"
                      expiresAt: "2026-03-19T14:35:00.000Z"
                      lobbyId: "abc123def456"
        "400":
          description: Invalid session status or no agent assigned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                invalidStatus:
                  value:
                    error:
                      message: 'Cannot generate auth URL for session with status "completed"'
                      code: "INVALID_STATUS"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded (max 5 per session per hour)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                rateLimited:
                  value:
                    error:
                      message: "Rate limit: max 5 auth URLs per session per hour"
                      code: "TOKEN_RATE_LIMITED"

  /sessions/{id}/cancel:
    post:
      summary: Cancel a session
      description: |
        Cancel a pending session. Active sessions cannot be cancelled — use POST /sessions/{id}/end instead.
        Cannot cancel completed, expired, or already cancelled sessions.
        No SMS notification is sent on cancellation.
      operationId: cancelSession
      tags:
        - Sessions
      parameters:
        - name: id
          in: path
          required: true
          description: Session ID
          schema:
            type: string
          example: "abc123def456"
      responses:
        "200":
          description: Session cancelled successfully — returns the full updated session
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Session"
        "409":
          description: Cannot cancel session in current status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                alreadyCompleted:
                  value:
                    error:
                      message: "Session is already completed"
                      code: "INVALID_STATUS"
                isActive:
                  value:
                    error:
                      message: "Cannot cancel an active session. End it instead."
                      code: "INVALID_STATUS"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sessions/{id}/resend-sms:
    post:
      summary: Resend SMS
      description: |
        Resend the SMS invitation for a pending session.
        Respects the organization's SMS limit per session (maxSmsPerSession).
      operationId: resendSessionSms
      tags:
        - Sessions
      parameters:
        - name: id
          in: path
          required: true
          description: Session ID
          schema:
            type: string
          example: "abc123def456"
      responses:
        "200":
          description: |
            SMS resent successfully. Returns the full updated session
            plus SMS-specific fields (`smsCount`, `maxSms`, `sms`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: "#/components/schemas/Session"
                      - type: object
                        properties:
                          smsCount:
                            type: integer
                            description: Total SMS sent for this session after the resend
                          maxSms:
                            type: integer
                            description: SMS limit per session for this organization
                          sms:
                            type: object
                            properties:
                              status:
                                type: string
                                example: "sent"
                              to:
                                type: string
                                example: "+33612345678"
        "409":
          description: Invalid session status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: SMS limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                limitReached:
                  value:
                    error:
                      message: "SMS limit reached for this session"
                      code: "SMS_LIMIT_REACHED"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sessions/{id}/end:
    post:
      summary: End a session
      description: |
        End an active or pending session. Cannot end completed, cancelled, or expired sessions.
        Optionally provide agent and client duration in seconds.
      operationId: endSession
      tags:
        - Sessions
      parameters:
        - name: id
          in: path
          required: true
          description: Session ID
          schema:
            type: string
          example: "abc123def456"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                agentDuration:
                  type: number
                  description: Agent connection duration in seconds
                  example: 798
                clientDuration:
                  type: number
                  description: Client connection duration in seconds
                  example: 750
                closureTypeName:
                  type: string
                  nullable: true
                  description: Session closure category name. Must match an existing closure type configured in the organization. Pass null to clear.
                  example: "Problem resolved"
                notes:
                  type: string
                  nullable: true
                  description: Agent notes about the session. Pass null to clear.
                  example: "Issue resolved by guiding client through restart procedure"
            examples:
              withDurations:
                summary: With duration tracking
                value:
                  agentDuration: 798
                  clientDuration: 750
                  closureTypeName: "Problem resolved"
                  notes: "Issue resolved by guiding client through restart procedure"
      responses:
        "200":
          description: Session ended successfully — returns the full updated session
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Session"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                invalidClosureType:
                  value:
                    error:
                      message: "Closure type \"Unknown type\" not found"
                      code: "INVALID_CLOSURE_TYPE"
        "409":
          description: Invalid session status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                alreadyCompleted:
                  value:
                    error:
                      message: "Session is already completed"
                      code: "INVALID_STATUS"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sessions/{id}/screenshots:
    get:
      summary: Get session screenshots
      description: |
        Retrieve all screenshots captured during a session.

        Download URLs are signed, time-limited URLs on the `groundcam.io` domain
        that 302-redirect to the underlying storage. The URL TTL is controlled
        by the `url_ttl` query parameter (default 3600s, min 60s, max 86400s).
        After expiration, re-fetch this endpoint to obtain a fresh URL.
      operationId: getSessionScreenshots
      tags:
        - Sessions
      parameters:
        - name: id
          in: path
          required: true
          description: Session ID
          schema:
            type: string
          example: "abc123def456"
        - name: url_ttl
          in: query
          required: false
          description: Lifetime in seconds for the returned download URLs (min 60, max 86400)
          schema:
            type: integer
            minimum: 60
            maximum: 86400
            default: 3600
          example: 3600
      responses:
        "200":
          description: Screenshots retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      screenshots:
                        type: array
                        items:
                          $ref: "#/components/schemas/Screenshot"
              examples:
                success:
                  value:
                    data:
                      screenshots:
                        - id: "scr_Abc12xYz"
                          url: "https://groundcam.io/sc/abc123def456/scr_Abc12xYz?exp=1742031600&sig=Xy7..."
                          timestamp: "2026-03-07T10:35:20.000Z"
                          location:
                            latitude: 48.8566
                            longitude: 2.3522
                          tags: ["defect", "product"]
                        - id: "scr_Def34wVu"
                          url: "https://groundcam.io/sc/abc123def456/scr_Def34wVu?exp=1742031600&sig=Ab3..."
                          timestamp: "2026-03-07T10:38:45.000Z"
                          location: null
                          tags: ["serial-number"]
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sessions/{id}/recordings:
    get:
      summary: Get session recordings
      description: |
        Retrieve all recordings for a session.
        Download URLs are Firebase Storage token URLs and do not expire.
      operationId: getSessionRecordings
      tags:
        - Sessions
      parameters:
        - name: id
          in: path
          required: true
          description: Session ID
          schema:
            type: string
          example: "abc123def456"
      responses:
        "200":
          description: Recordings retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      recordings:
                        type: array
                        items:
                          $ref: "#/components/schemas/Recording"
              examples:
                success:
                  value:
                    data:
                      recordings:
                        - id: "rec_Ghi56tSr"
                          url: "https://firebasestorage.googleapis.com/v0/b/groundcam.appspot.com/o/recordings%2Frec_Ghi56tSr.webm?alt=media&token=def"
                          duration: 798
                          size: 15728640
                          createdAt: "2026-03-07T10:45:30.000Z"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /credits:
    get:
      summary: Check credit balance
      description: Retrieve the current credit balance and usage for your organization.
      operationId: getCredits
      tags:
        - Credits
      responses:
        "200":
          description: Credit balance retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/CreditBalance"
              examples:
                success:
                  value:
                    data:
                      available: 150
                      subscription:
                        remaining: 75
                        quota: 100
                        resetsAt: "2026-04-01T00:00:00.000Z"
                      pack:
                        remaining: 75
                noCredits:
                  summary: No credits available
                  value:
                    data:
                      available: 0
                      subscription:
                        remaining: 0
                        quota: 0
                        resetsAt: null
                      pack:
                        remaining: 0
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /webhooks:
    post:
      summary: Register a webhook
      description: |
        Register a webhook endpoint to receive real-time notifications about session events.

        ## Webhook Security
        Each webhook delivery includes these headers:
        - `X-GroundCam-Signature`: HMAC-SHA256 signature (format: `sha256=<hex_digest>`)
        - `X-GroundCam-Timestamp`: Unix timestamp (prevent replay attacks)
        - `X-GroundCam-Event`: Event type (e.g., "session.completed")
        - `X-GroundCam-Delivery`: Unique delivery ID for tracking

        ## Signature Verification
        To verify the signature:
        1. Get the timestamp from `X-GroundCam-Timestamp` header
        2. Concatenate: `${timestamp}.${raw_json_body}`
        3. Compute HMAC-SHA256 with your webhook secret
        4. Compare with the signature header value (after the `sha256=` prefix)

        Use timing-safe comparison and reject timestamps older than 5 minutes to prevent replay attacks.

        ## Delivery
        Your endpoint must respond with a `2xx` status code within 10 seconds.
        Deliveries are logged (success or failure) for monitoring — see
        `GET /webhooks/{id}/deliveries`.

        ## Retry policy
        Delivery is **at-least-once**. The platform retries failed deliveries
        automatically:
        - Up to **3 attempts total** per event (initial + 2 retries).
        - Backoff: **+1 minute** then **+5 minutes** after the previous attempt.
        - Hard deadline: **15 minutes** after the initial attempt — past this
          point, the delivery is marked `exhausted` and no further retry is made.

        A retry is triggered when the consumer returns a non-`2xx` status, fails
        to respond within the 10 s timeout, or is unreachable (DNS, connection,
        TLS). A new HMAC signature with a fresh timestamp is computed for every
        attempt, so consumers performing anti-replay validation should treat
        each delivery as independent.

        ## Idempotency
        Because delivery is at-least-once, your endpoint **may** receive the
        same event more than once (typically when an attempt times out on our
        side but actually succeeded on yours). The `X-GroundCam-Delivery`
        header carries a unique delivery identifier — use it as an idempotency
        key on your side to deduplicate.

        ## Reconciliation
        For full coverage (beyond the 15-minute retry window) we recommend
        periodically reconciling with the `GET /v1/appointments` and
        `GET /v1/sessions` endpoints using the `updatedAfter` query parameter,
        starting from the timestamp of your last successful sync. This catches
        any events lost to consumer outages longer than the retry budget.
      operationId: createWebhook
      tags:
        - Webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookRequest"
            examples:
              basic:
                summary: Subscribe to key events
                value:
                  url: "https://mycrm.com/webhooks/groundcam"
                  events: ["session.created", "session.completed"]
              comprehensive:
                summary: Subscribe to all events
                value:
                  url: "https://mycrm.com/webhooks/groundcam"
                  events:
                    - session.created
                    - session.client_waiting
                    - session.active
                    - session.completed
                    - session.rated
                    - session.cancelled
                    - session.expired
                    - session.updated
                    - session.deleted
                    - appointment.created
                    - appointment.updated
                    - appointment.cancelled
      responses:
        "201":
          description: Webhook created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      url:
                        type: string
                      events:
                        type: array
                        items:
                          type: string
                      active:
                        type: boolean
                      secret:
                        type: string
                        description: Webhook signing secret (store securely, shown only at creation)
              examples:
                success:
                  value:
                    data:
                      id: "Wh3kLp9mNq2r"
                      url: "https://mycrm.com/webhooks/groundcam"
                      events: ["session.created", "session.completed"]
                      active: true
                      secret: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                invalidUrl:
                  value:
                    error:
                      message: "URL must use HTTPS"
                      code: "INVALID_URL"
                invalidEvent:
                  value:
                    error:
                      message: "Invalid event: session.unknown"
                      code: "INVALID_EVENT"
        "409":
          description: Duplicate URL or webhook limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                duplicateUrl:
                  value:
                    error:
                      message: "A webhook with this URL already exists"
                      code: "DUPLICATE_URL"
                limitReached:
                  value:
                    error:
                      message: "Maximum number of webhooks reached (10)"
                      code: "LIMIT_REACHED"

    get:
      summary: List webhooks
      description: Retrieve all webhook endpoints registered for your organization.
      operationId: listWebhooks
      tags:
        - Webhooks
      responses:
        "200":
          description: Webhooks retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      webhooks:
                        type: array
                        items:
                          $ref: "#/components/schemas/WebhookEndpoint"
              examples:
                success:
                  value:
                    data:
                      webhooks:
                        - id: "Wh3kLp9mNq2r"
                          url: "https://mycrm.com/webhooks/groundcam"
                          events: ["session.created", "session.completed"]
                          active: true
                          createdAt: "2026-03-01T09:00:00.000Z"
                          updatedAt: "2026-03-01T09:00:00.000Z"
                        - id: "Wh7tYs4uPx8v"
                          url: "https://analytics.example.com/hooks"
                          events: ["session.completed", "session.cancelled"]
                          active: true
                          createdAt: "2026-03-05T14:20:00.000Z"
                          updatedAt: "2026-03-08T12:11:00.000Z"
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /webhooks/{id}:
    delete:
      summary: Delete a webhook
      description: Delete a webhook endpoint. This action cannot be undone.
      operationId: deleteWebhook
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          description: Webhook endpoint ID
          schema:
            type: string
          example: "webhook_abc123"
      responses:
        "200":
          description: Webhook deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      deleted:
                        type: boolean
              examples:
                success:
                  value:
                    data:
                      deleted: true
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                notFound:
                  value:
                    error:
                      message: "Webhook not found"
                      code: "NOT_FOUND"

    patch:
      summary: Update a webhook
      description: |
        Update a webhook endpoint configuration. At least one field (url, events, active) must be provided.
        The webhook secret cannot be changed.
      operationId: updateWebhook
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          description: Webhook endpoint ID
          schema:
            type: string
          example: "webhook_abc123"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                  description: New target URL (must be HTTPS)
                  example: "https://api.example.com/webhooks/groundcam"
                events:
                  type: array
                  items:
                    type: string
                    enum: [session.created, session.client_waiting, session.active, session.completed, session.rated, session.cancelled, session.expired, session.updated, session.deleted, appointment.created, appointment.updated, appointment.cancelled]
                  description: Events to subscribe to
                  example: ["session.completed", "session.cancelled"]
                active:
                  type: boolean
                  description: Whether the webhook is active
                  example: true
            example:
              events: ["session.completed", "session.cancelled"]
              active: true
      responses:
        "200":
          description: Webhook updated successfully — returns the full updated webhook
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookEndpoint"
              example:
                data:
                  id: "wh_abc123"
                  url: "https://api.example.com/webhooks/groundcam"
                  events: ["session.completed", "session.cancelled"]
                  active: true
                  createdAt: "2026-01-15T10:30:00Z"
                  updatedAt: "2026-03-12T11:42:00Z"
        "400":
          description: Invalid request (missing fields, invalid URL, invalid event)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                invalidUrl:
                  value:
                    error:
                      message: "URL must use HTTPS"
                      code: "INVALID_URL"
                invalidEvent:
                  value:
                    error:
                      message: "Invalid event: session.unknown"
                      code: "INVALID_EVENT"
                noFields:
                  value:
                    error:
                      message: "At least one field is required: url, events, active"
                      code: "MISSING_FIELD"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                notFound:
                  value:
                    error:
                      message: "Webhook not found"
                      code: "NOT_FOUND"

  /webhooks/{id}/test:
    post:
      summary: Test a webhook
      description: |
        Send a test event (`webhook.test`) to a webhook endpoint to verify it's working correctly.
        The test event is signed with the webhook's secret just like real events.
      operationId: testWebhook
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          description: Webhook endpoint ID
          schema:
            type: string
          example: "webhook_abc123"
      responses:
        "200":
          description: Test event sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        description: Whether the webhook endpoint responded with 2xx
                      httpStatus:
                        type: integer
                        nullable: true
                        description: HTTP status code returned by the endpoint
                      deliveryId:
                        type: string
                        description: Unique delivery ID for tracking
                      error:
                        type: string
                        nullable: true
                        description: Error message if the delivery failed
              examples:
                success:
                  summary: Endpoint responded OK
                  value:
                    data:
                      success: true
                      httpStatus: 200
                      deliveryId: "Ev9kLm3nPq7r"
                failed:
                  summary: Endpoint returned error
                  value:
                    data:
                      success: false
                      httpStatus: 500
                      deliveryId: "Ev4tYs8uWx2v"
                      error: null
                connectionFailed:
                  summary: Connection failed
                  value:
                    data:
                      success: false
                      httpStatus: null
                      deliveryId: "Ev1aBc5dEf9g"
                      error: "fetch failed"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /webhooks/{id}/deliveries:
    get:
      summary: List webhook deliveries
      description: |
        Return the recent delivery attempts for a webhook endpoint — useful to
        debug failing integrations from your side without needing access to the
        GroundCam dashboard.

        Each record reflects one delivery attempt (initial or retry) and
        captures its full lifecycle: status, HTTP response, retry counter,
        scheduled next attempt, deadline, and the exact envelope that was
        sent.

        Results are returned in **reverse chronological order** (newest first)
        and capped at the value of the `limit` query parameter.
      operationId: listWebhookDeliveries
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          description: Webhook endpoint ID
          schema:
            type: string
          example: "webhook_abc123"
        - name: limit
          in: query
          required: false
          description: Maximum number of deliveries to return (1-100, default 20).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: status
          in: query
          required: false
          description: Filter on delivery status.
          schema:
            type: string
            enum:
              - delivered
              - failed
              - exhausted
        - name: eventType
          in: query
          required: false
          description: Filter on the event type (e.g. `appointment.updated`).
          schema:
            type: string
        - name: createdAfter
          in: query
          required: false
          description: Return only deliveries created on or after this ISO 8601 timestamp.
          schema:
            type: string
            format: date-time
        - name: createdBefore
          in: query
          required: false
          description: Return only deliveries created on or before this ISO 8601 timestamp.
          schema:
            type: string
            format: date-time
        - name: startAfter
          in: query
          required: false
          description: |
            Pagination cursor — pass the value of `pagination.nextCursor` from
            the previous response to fetch the next page.
          schema:
            type: string
      responses:
        "200":
          description: Deliveries retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      deliveries:
                        type: array
                        items:
                          $ref: "#/components/schemas/WebhookDelivery"
                      pagination:
                        type: object
                        properties:
                          limit:
                            type: integer
                            example: 20
                          hasMore:
                            type: boolean
                            example: true
                          nextCursor:
                            type: string
                            nullable: true
                            example: "Ev0aBc5dEf9g"
              examples:
                mixed:
                  summary: Mix of delivered and retry-pending deliveries
                  value:
                    data:
                      deliveries:
                        - id: "Ev9kLm3nPq7r"
                          eventType: "appointment.updated"
                          status: "delivered"
                          httpStatus: 200
                          attempts: 2
                          maxAttempts: 3
                          lastError: null
                          lastAttemptAt: "2026-03-12T14:06:25Z"
                          nextAttemptAt: null
                          deadlineAt: "2026-03-12T14:20:21Z"
                          createdAt: "2026-03-12T14:05:21Z"
                          payload:
                            event: "appointment.updated"
                            timestamp: "2026-03-12T14:05:21Z"
                            data:
                              id: "Appt7sLp4Wq2"
                              status: "lobby_created"
                              lobbyId: "Lb9kLm3nPq7r"
                        - id: "Ev4tYs8uWx2v"
                          eventType: "session.created"
                          status: "failed"
                          httpStatus: 502
                          attempts: 1
                          maxAttempts: 3
                          lastError: "HTTP 502"
                          lastAttemptAt: "2026-03-12T14:10:08Z"
                          nextAttemptAt: "2026-03-12T14:11:08Z"
                          deadlineAt: "2026-03-12T14:25:08Z"
                          createdAt: "2026-03-12T14:10:08Z"
                          payload:
                            event: "session.created"
                            timestamp: "2026-03-12T14:10:08Z"
                            data:
                              id: "Lb2vYs8uWx2v"
                              status: "pending"
                      pagination:
                        limit: 20
                        hasMore: false
                        nextCursor: null
        "400":
          description: Invalid query parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /organization:
    get:
      summary: Get organization info
      description: |
        Retrieve information about your organization: name, active plan, enabled features, and credit balance.
        Useful to adapt your CRM UI — e.g. hide the session button when no credits remain, or display the org name.
      operationId: getOrganization
      tags:
        - Organization
      responses:
        "200":
          description: Organization info retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/OrganizationInfo"
              examples:
                success:
                  value:
                    data:
                      id: "org_abc123"
                      name: "Acme Corp"
                      slug: "acme"
                      membersCount: 5
                      plan:
                        id: "pro"
                        name: "Pro"
                        status: "active"
                      features:
                        screenshots: true
                        audio: true
                        drawing: true
                        laserPointer: true
                        chat: true
                        geolocation: true
                        maxSessionDuration: 60
                        maxSmsPerSession: 3
                        satisfactionSurvey: true
                        customSubdomain: false
                        apiAccess: true
                        detailedStatistics: true
                      credits:
                        available: 100
                        subscriptionCredits: 80
                        packCredits: 20
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /stats:
    get:
      summary: Get aggregated statistics
      description: |
        Retrieve aggregated session statistics for a given period (`day`, `week`, or `month`).
        Can be used to power a dashboard widget in your CRM showing sessions created, average duration,
        client satisfaction, and SMS usage.
      operationId: getStats
      tags:
        - Statistics
      parameters:
        - name: period
          in: query
          required: false
          description: "Time period for aggregation. Defaults to `month`."
          schema:
            type: string
            enum: [day, week, month]
            default: month
      responses:
        "200":
          description: Statistics retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Stats"
              examples:
                month:
                  summary: Monthly stats
                  value:
                    data:
                      period: "month"
                      from: "2026-03-01T00:00:00.000Z"
                      to: "2026-03-12T15:30:00.000Z"
                      sessions:
                        total: 42
                        completed: 35
                        cancelled: 5
                        expired: 2
                      duration:
                        totalSeconds: 12600
                        averageSeconds: 360
                      satisfaction:
                        average: 4.2
                        count: 30
                      sms:
                        sent: 44
                empty:
                  summary: No sessions in period
                  value:
                    data:
                      period: "day"
                      from: "2026-03-12T00:00:00.000Z"
                      to: "2026-03-12T15:30:00.000Z"
                      sessions:
                        total: 0
                        completed: 0
                        cancelled: 0
                        expired: 0
                      duration:
                        totalSeconds: 0
                        averageSeconds: null
                      satisfaction:
                        average: null
                        count: 0
                      sms:
                        sent: 0
        "400":
          description: Invalid period parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /auth/verify:
    get:
      summary: Verify API key
      description: |
        Verify the current API key and return organization details, plan info, enabled features, and credit balance.
        Use this endpoint to test API connectivity and check permissions from your CRM configuration page.
      operationId: verifyApiKey
      tags:
        - Authentication
      responses:
        "200":
          description: API key verified successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      organizationId:
                        type: string
                        example: "org_abc123"
                      organizationName:
                        type: string
                        nullable: true
                        example: "Acme Corp"
                      key:
                        type: object
                        properties:
                          id:
                            type: string
                            example: "key_xyz789"
                          name:
                            type: string
                            nullable: true
                            example: "Odoo Integration"
                          prefix:
                            type: string
                            nullable: true
                            example: "gc_live_a1b2c3d4"
                      plan:
                        type: object
                        properties:
                          id:
                            type: string
                            nullable: true
                            example: "pro"
                          name:
                            type: string
                            nullable: true
                            example: "Pro"
                          status:
                            type: string
                            nullable: true
                            example: "active"
                      features:
                        type: object
                        description: Features enabled by the active plan
                        properties:
                          screenshots:
                            type: boolean
                          audio:
                            type: boolean
                          drawing:
                            type: boolean
                          laserPointer:
                            type: boolean
                          chat:
                            type: boolean
                          geolocation:
                            type: boolean
                          maxSessionDuration:
                            type: integer
                          maxSmsPerSession:
                            type: integer
                          satisfactionSurvey:
                            type: boolean
                          customSubdomain:
                            type: boolean
                          apiAccess:
                            type: boolean
                          detailedStatistics:
                            type: boolean
                      credits:
                        type: object
                        properties:
                          available:
                            type: integer
                            example: 100
                          subscriptionCredits:
                            type: integer
                            example: 80
                          packCredits:
                            type: integer
                            example: 20
              examples:
                success:
                  value:
                    data:
                      organizationId: "org_abc123"
                      organizationName: "Acme Corp"
                      key:
                        id: "key_xyz789"
                        name: "Odoo Integration"
                        prefix: "gc_live_a1b2c3d4"
                      plan:
                        id: "pro"
                        name: "Pro"
                        status: "active"
                      features:
                        screenshots: true
                        audio: true
                        drawing: true
                        laserPointer: true
                        chat: true
                        geolocation: true
                        maxSessionDuration: 60
                        maxSmsPerSession: 3
                        satisfactionSurvey: true
                        customSubdomain: false
                        apiAccess: true
                        detailedStatistics: true
                      credits:
                        available: 100
                        subscriptionCredits: 80
                        packCredits: 20
        "401":
          description: Authentication failed (invalid or revoked API key)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /closure-types:
    get:
      summary: List closure types
      description: |
        Retrieve the list of session closure types configured for your organization.
        Use these values in the `closureTypeName` field when updating sessions via PATCH.
      operationId: listClosureTypes
      tags:
        - Configuration
      responses:
        "200":
          description: Closure types retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      closureTypes:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                              example: "ct_abc123"
                            name:
                              type: string
                              example: "Problem resolved"
                            icon:
                              type: string
                              nullable: true
                              description: Lucide icon name
                              example: "CheckCircle"
                            color:
                              type: string
                              nullable: true
                              description: Color stem (e.g., green, red, blue)
                              example: "green"
                            requireComment:
                              type: boolean
                              description: Whether notes are required when using this closure type
                              example: false
              examples:
                success:
                  value:
                    data:
                      closureTypes:
                        - id: "ct_abc123"
                          name: "Problem resolved"
                          icon: "CheckCircle"
                          color: "green"
                          requireComment: false
                        - id: "ct_def456"
                          name: "Escalated"
                          icon: "AlertTriangle"
                          color: "orange"
                          requireComment: true
                        - id: "ct_ghi789"
                          name: "No resolution"
                          icon: "XCircle"
                          color: "red"
                          requireComment: true
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /tags:
    get:
      summary: List session tags
      description: |
        Retrieve unique session tags used across your organization (from the last 1000 sessions).
        Use these values for auto-completion when updating sessions via PATCH.
        Returns up to 200 unique tags, sorted alphabetically.
      operationId: listTags
      tags:
        - Configuration
      responses:
        "200":
          description: Tags retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      tags:
                        type: array
                        items:
                          type: string
                        example: ["billing", "hardware", "installation", "plumbing", "shipping", "urgent"]
                      total:
                        type: integer
                        example: 6
              examples:
                success:
                  value:
                    data:
                      tags: ["billing", "hardware", "installation", "plumbing", "shipping", "urgent"]
                      total: 6
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sessions/bulk:
    post:
      summary: Create multiple sessions
      description: |
        Create up to 10 sessions in a single request. Sessions are processed sequentially.
        Returns per-session results with a summary of successes and failures.

        **Differences from POST /sessions**: Bulk does not support `clientEmail` or `mode` fields.
        All bulk sessions use camera mode. Each item requires `agentId` and `clientPhone`.

        **Credits**: Requires at least N credits available (where N is the number of sessions).
        **SMS**: Each session's `sendSms` flag is handled individually.
      operationId: createSessionsBulk
      tags:
        - Sessions
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: |
            Unique key for idempotent request processing. If a request with the same key
            was successfully processed within the last 24 hours, the cached response is returned.
            Also accepted as X-Idempotency-Key.
          schema:
            type: string
          example: "req_bulk_abc123_unique_id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sessions:
                  type: array
                  minItems: 1
                  maxItems: 10
                  items:
                    $ref: "#/components/schemas/BulkSessionInput"
                  description: Array of session creation requests (1-10)
              required:
                - sessions
            examples:
              basic:
                summary: Two sessions with different options
                value:
                  sessions:
                    - clientPhone: "+33612345678"
                      clientName: "Jean Dupont"
                      agentId: "agent_xyz789"
                      crmId: "TICKET-001"
                      language: "en"
                      ttlMinutes: 15
                    - clientPhone: "+33698765432"
                      clientName: "Marie Martin"
                      agentId: "agent_xyz789"
                      sendSms: false
      responses:
        "201":
          description: Sessions created (some may have failed)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      results:
                        type: array
                        items:
                          type: object
                          properties:
                            index:
                              type: integer
                              description: Position in the input array (0-based)
                            success:
                              type: boolean
                            session:
                              type: object
                              description: Created session (present if success=true)
                            error:
                              type: object
                              description: Error details (present if success=false)
                              properties:
                                message:
                                  type: string
                                code:
                                  type: string
                      summary:
                        type: object
                        properties:
                          total:
                            type: integer
                          succeeded:
                            type: integer
                          failed:
                            type: integer
              examples:
                success:
                  value:
                    data:
                      results:
                        - index: 0
                          success: true
                          session:
                            id: "Xk9mPq2rT4wZ"
                            status: "pending"
                            clientUrl: "https://app.groundcam.io/l/Xk9mPq2rT4wZ?token=abc123"
                            agentUrl: "https://app.groundcam.io/dashboard/lobbies/Xk9mPq2rT4wZ"
                            createdAt: "2026-03-07T14:30:00.000Z"
                            expiresAt: "2026-03-07T15:30:00.000Z"
                            crmId: "TICKET-001"
                            ticketUrl: null
                            metadata: null
                            sessionTags: []
                            sms:
                              status: "sent"
                              to: "+33612345678"
                        - index: 1
                          success: true
                          session:
                            id: "Yp4nRs7tU8vA"
                            status: "pending"
                            clientUrl: "https://app.groundcam.io/l/Yp4nRs7tU8vA?token=def456"
                            agentUrl: "https://app.groundcam.io/dashboard/lobbies/Yp4nRs7tU8vA"
                            createdAt: "2026-03-07T14:30:01.000Z"
                            expiresAt: "2026-03-07T15:30:01.000Z"
                            crmId: null
                            ticketUrl: null
                            metadata: null
                            sessionTags: []
                            sms:
                              status: "skipped"
                      summary:
                        total: 2
                        succeeded: 2
                        failed: 0
        "400":
          description: |
            Invalid request. Two possible response shapes:
            - Structural errors (empty array, more than 10 items): returns an Error object.
            - All sessions failed validation: returns `{ data: { results, summary } }` with per-item errors, same shape as the 201 response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "402":
          description: Insufficient credits for the total number of sessions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Idempotency conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                idempotencyConflict:
                  value:
                    error:
                      message: "A request with this idempotency key is currently being processed"
                      code: "IDEMPOTENCY_CONFLICT"
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /appointments:
    post:
      summary: Create appointment
      description: |
        Schedule a video session appointment. The client will be automatically notified
        (via SMS and/or email) 5 minutes before the scheduled time, and a video session
        will be created automatically.

        **Anti-double-booking**: The API prevents scheduling overlapping appointments
        for the same agent. Returns 409 if a conflict is detected.

        **CRM Integration**: Use `crmId` and `metadata` fields to link appointments
        to your CRM records (Odoo, Salesforce, etc.).
      operationId: createAppointment
      tags: [Appointments]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAppointmentRequest"
      responses:
        "201":
          description: Appointment created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Appointment"
        "400":
          description: Validation error (invalid phone, past date, misaligned time slot, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Scheduling feature not enabled or agent not authorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Time slot conflict — agent already has an appointment at this time
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    get:
      summary: List appointments
      description: |
        List appointments for the organization with optional filters.
        Supports cursor-based pagination and metadata filtering.

        **Metadata filtering**: Use `metadata.key=value` query params to filter
        by custom metadata (e.g., `?metadata.odoo_ticket_id=12345`).

        **Incremental polling**: Use `updatedAfter` (and optionally `updatedBefore`)
        to fetch only appointments modified within a time window. When either is
        provided, results are ordered by `updatedAt` descending and cannot be
        combined with `scheduledAfter`/`scheduledBefore`.
      operationId: listAppointments
      tags: [Appointments]
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [scheduled, notified, lobby_created, completed, cancelled]
          description: Filter by status
        - name: agentId
          in: query
          schema:
            type: string
          description: Filter by agent ID
        - name: crmId
          in: query
          schema:
            type: string
          description: Filter by CRM identifier
        - name: scheduledAfter
          in: query
          schema:
            type: string
            format: date-time
          description: Only appointments scheduled after this date (ISO 8601). Cannot be combined with `updatedAfter`/`updatedBefore`.
        - name: scheduledBefore
          in: query
          schema:
            type: string
            format: date-time
          description: Only appointments scheduled before this date (ISO 8601). Cannot be combined with `updatedAfter`/`updatedBefore`.
        - name: updatedAfter
          in: query
          schema:
            type: string
            format: date-time
          description: |
            Only appointments updated at or after this date (ISO 8601). Results are ordered
            by `updatedAt` descending. Ideal for incremental sync (n8n triggers, CRM polling).
            Cannot be combined with `scheduledAfter`/`scheduledBefore`.
        - name: updatedBefore
          in: query
          schema:
            type: string
            format: date-time
          description: |
            Only appointments updated at or before this date (ISO 8601). Cannot be combined
            with `scheduledAfter`/`scheduledBefore`.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Number of results per page
        - name: startAfter
          in: query
          schema:
            type: string
          description: Cursor for pagination (appointment ID from previous page)
        - name: metadata.*
          in: query
          description: |
            Filter by custom metadata values using dot notation.
            Example: `?metadata.odoo_ticket_id=12345`
            Multiple metadata filters are combined with AND logic.
          schema:
            type: string
          example: "12345"
      responses:
        "200":
          description: List of appointments
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      appointments:
                        type: array
                        items:
                          $ref: "#/components/schemas/Appointment"
                      pagination:
                        type: object
                        properties:
                          limit:
                            type: integer
                          hasMore:
                            type: boolean
                          nextCursor:
                            type: string
                            nullable: true

  /appointments/{id}:
    get:
      summary: Get appointment
      description: Retrieve a single appointment by ID.
      operationId: getAppointment
      tags: [Appointments]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Appointment ID
      responses:
        "200":
          description: Appointment details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Appointment"
        "404":
          description: Appointment not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      summary: Update appointment
      description: |
        Update or reschedule an appointment. Only provided fields are updated.
        Cannot update appointments that are completed, cancelled, or have an active lobby.

        If `scheduledAt` is changed, the anti-double-booking check runs again.
      operationId: updateAppointment
      tags: [Appointments]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                scheduledAt:
                  type: string
                  format: date-time
                durationMinutes:
                  type: integer
                  enum: [5, 10, 15, 20]
                clientPhone:
                  type: string
                clientName:
                  type: string
                clientEmail:
                  type: string
                notifyBy:
                  type: string
                  enum: [sms, email, both]
                notes:
                  type: string
                crmId:
                  type: string
                ticketUrl:
                  type: string
                metadata:
                  type: object
                  additionalProperties:
                    oneOf:
                      - type: string
                      - type: number
                      - type: boolean
                  description: "Flat object only, max 20 keys, less than 4 KB serialized."
      responses:
        "200":
          description: Updated appointment
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Appointment"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Appointment not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                notFound:
                  value:
                    error:
                      message: "Appointment not found"
                      code: "NOT_FOUND"
        "409":
          description: Time slot conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /appointments/{id}/cancel:
    post:
      summary: Cancel appointment
      description: |
        Cancel a scheduled appointment. If a video session was already created
        for this appointment, it will also be cancelled.
      operationId: cancelAppointment
      tags: [Appointments]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Reason for cancellation (optional)
                  example: "Client requested reschedule"
      responses:
        "200":
          description: Appointment cancelled (returns partial appointment data)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/AppointmentCancelResponse"
              examples:
                cancelled:
                  value:
                    data:
                      id: "apt_Xk9mPq2r"
                      status: "cancelled"
                      crmId: "ODOO-12345"
                      metadata: null
                      cancellationReason: "Client requested reschedule"
                      lobbyId: null
                      cancelledAt: "2026-03-20T14:35:00.000Z"
        "409":
          description: Appointment is already completed or cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                alreadyCompleted:
                  value:
                    error:
                      message: "Cannot cancel a completed appointment"
                      code: "INVALID_STATUS"
                alreadyCancelled:
                  value:
                    error:
                      message: "Appointment is already cancelled"
                      code: "INVALID_STATUS"
        "404":
          description: Appointment not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                notFound:
                  value:
                    error:
                      message: "Appointment not found"
                      code: "NOT_FOUND"

  /appointments/available-slots:
    get:
      summary: Check available time slots
      description: |
        Returns available 5-minute time slots for an agent on a given date.
        Working hours are 07:00 to 20:00. Slots that overlap with existing
        non-cancelled appointments are marked as unavailable.

        **Useful for CRM integration**: Call this endpoint before creating an
        appointment to show available times in your booking UI (e.g., Odoo calendar).
      operationId: getAvailableAppointmentSlots
      tags: [Appointments]
      parameters:
        - name: agentId
          in: query
          required: true
          schema:
            type: string
          description: Agent user ID
        - name: date
          in: query
          required: true
          schema:
            type: string
            format: date
          description: Date to check (YYYY-MM-DD)
          example: "2026-03-20"
        - name: durationMinutes
          in: query
          schema:
            type: integer
            enum: [5, 10, 15, 20]
            default: 15
          description: Duration of the appointment to check availability for
      responses:
        "200":
          description: Available time slots
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      slots:
                        type: array
                        items:
                          $ref: "#/components/schemas/AvailableSlot"
        "400":
          description: Missing or invalid parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Agent is not a member of the organization
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                agentNotMember:
                  value:
                    error:
                      message: "Agent is not a member of this organization"
                      code: "AGENT_NOT_MEMBER"
        "404":
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                orgNotFound:
                  value:
                    error:
                      message: "Organization not found"
                      code: "ORG_NOT_FOUND"

tags:
  - name: Authentication
    description: Verify API key and check permissions
  - name: Agents
    description: List organization members and retrieve agent IDs
  - name: Sessions
    description: Manage video assistance sessions
  - name: Credits
    description: Check credit balance and usage
  - name: Webhooks
    description: Configure webhook notifications
  - name: Organization
    description: Retrieve organization info, plan details and feature flags
  - name: Statistics
    description: Aggregated session metrics for dashboard widgets
  - name: Configuration
    description: Retrieve closure types and session tags for dropdown/autocomplete
  - name: Appointments
    description: Schedule, manage, and cancel video session appointments. Includes availability checking for CRM integration.
