openapi: "3.0.0"
info:
  version: v1.0
  title: Gondola Portfolio API
  description: |
    ## Overview
    The Gondola Portfolio API lets a creator or company expose a token-scoped,
    read-only view of their Gondola profile data for use on personal portfolio
    websites, custom dashboards, and agent workflows.

    This endpoint is designed for both developers and agents:
    - It returns a stable JSON envelope with clear pagination metadata.
    - It exposes account access tier information so callers can adapt to free
      vs PRO tiers without guessing.
    - It returns normalized post and credit data that can be rendered directly
      or transformed into custom site content.

    ## Authentication
    Requests to this endpoint require a **portfolio token** in the `Authorization`
    header using the Bearer scheme:

    ```bash
    curl --request GET \
      --url https://gondola.cc/api/v1/portfolio \
      --header 'Accept: application/json' \
      --header 'Authorization: Bearer <YOUR_PORTFOLIO_TOKEN>'
    ```

    Portfolio tokens are created and rotated separately from this endpoint via
    Gondola's authenticated app APIs:
    - `POST /api/token/portfolio` to create or rotate a token
    - `GET /api/token/portfolio` to inspect token metadata
    - `DELETE /api/token/portfolio` to revoke the token

    ## Access Tiers
    The same endpoint serves both free and PRO users. The token owner's current
    subscription status is checked on every request.

    Free behavior:
    - Returns full profile stats
    - Returns only the 5 most recent portfolio posts
    - Ignores pagination requests beyond the first page
    - Rejects custom sorting with a 403 response
    - Rejects content filters with a 403 response

    PRO behavior:
    - Returns full profile stats
    - Returns a paginated portfolio feed
    - Honors `from` and `size` query parameters
    - Honors `sort` query parameter
    - Honors network, account, collaborator, and role filters

    ## Intended Use
    Common uses include:
    - Rendering a custom portfolio website
    - Creating a "recent credited work" section
    - Syncing Gondola work into a CMS
    - Letting an agent summarize a creator's recent work

    ## Security Guidance
    Portfolio tokens are read-only and this endpoint is CORS-enabled for direct
    browser use. That means visitors can inspect the token if you embed it in a
    public site. Use direct client-side calls only when that trade-off is
    acceptable, and expect Gondola to enforce a rate limit of 60 requests per
    60 seconds per token.

servers:
  - url: https://gondola.cc
    description: Gondola production API
  - url: http://localhost:3000
    description: Local development server

security:
  - BearerPortfolioToken: []

tags:
  - name: Portfolio
    description: Read-only portfolio data for custom websites, automations, and agents.

paths:
  /api/v1/portfolio:
    get:
      tags:
        - Portfolio
      summary: Get portfolio feed and stats
      operationId: getPortfolioFeed
      description: |
        Returns the token owner's profile summary, aggregate portfolio stats,
        a recent or paginated feed of public portfolio posts, and access-tier
        metadata describing whether the token currently has free or PRO behavior.

        Stats behavior:
        - `data.stats` always reflects the current request criteria.
        - An unfiltered request returns overall portfolio totals.
        - A filtered request returns stats for that filtered subset only.
        - If you want to display all-time overall stats at the top of a
          portfolio page, fetch them from an unfiltered request and do not
          replace them with stats from later filtered requests.

        Agent guidance:
        - This endpoint is CORS-enabled. Direct browser `fetch()` works from
          any origin.
        - Embed the portfolio token as a JavaScript string constant. Never use
          `process.env` in browser code — environment variables are not
          available in the browser.
        - Fetch once on page load and reuse the response. Do not fetch on
          every component render or on a timer interval.
        - All response data is inside the `data` property. Access fields as
          `data.user`, `data.stats`, `data.posts` — not as top-level
          properties on the response object.
        - Do NOT send `sort`, `network`, `account`, `collaborator`, or `role`
          query parameters unless `data.access.isPro` is `true`. The server
          returns HTTP 403 for free-tier tokens that use these parameters —
          it does not silently ignore them.
        - If the response is HTTP 429 (rate limited), show cached or
          placeholder content. Read the `Retry-After` header for when to
          retry.
        - `data.posts[].thumbnailUrl` and `data.user.avatarUrl` are complete
          URLs ready to use in `<img src>`.
        - `data.posts[].credits` lists the people who worked on the post.
          Each credit has a `.user` (name, avatarUrl) and `.role` (name).
        - Use `data.paging.nextFrom` to continue paging for PRO users.
        - Do not infer hidden or private information from missing data.
      parameters:
        - name: from
          in: query
          required: false
          description: |
            Zero-based offset for pagination. Only meaningful for PRO users.
            Free users are always limited to the first page.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 25
        - name: size
          in: query
          required: false
          description: |
            Number of posts to return. Only meaningful for PRO users.
            Free users are always limited to 5 posts.
          schema:
            type: integer
            minimum: 0
            default: 25
          example: 10
        - name: sort
          in: query
          required: false
          description: |
            Sort order for PRO users. Free users default to `postedAt` and
            explicit sort requests are rejected with a 403 response.
          schema:
            type: string
            enum:
              - postedAt
              - currentEngagements
            default: postedAt
          example: currentEngagements
        - name: network
          in: query
          required: false
          description: |
            Comma-separated network filters for PRO users. Accepts Gondola network
            ids or names such as `twitter`, `instagram`, or `youtube`.
          schema:
            type: string
          example: instagram,twitter
        - name: account
          in: query
          required: false
          description: |
            Comma-separated posting account filters for PRO users.
          schema:
            type: string
          example: espn,houseofhighlights
        - name: collaborator
          in: query
          required: false
          description: |
            Comma-separated collaborator filters for PRO users. Accepts Gondola
            user ids or usernames.
          schema:
            type: string
          example: nhl,janedoe
        - name: role
          in: query
          required: false
          description: |
            Comma-separated role filters for PRO users. Accepts Gondola role ids
            or exact role names.
          schema:
            type: string
          example: Producer,Photographer
      responses:
        200:
          description: Portfolio feed and aggregate stats
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PortfolioResponseEnvelope"
              examples:
                freeTier:
                  summary: Free user response
                  value:
                    success: 1
                    error: 0
                    data:
                      access:
                        tier: free
                        isPro: false
                        canPaginate: false
                        maxPosts: 5
                      user:
                        id: 123
                        name: Jane Doe
                        username: janedoe
                        avatarUrl: https://img.gondola.cc/tr:w-400,h-400,fo-auto/avatars/janedoe.jpg
                        blurb: The best producer in the world.
                        defaultRole:
                          id: 9
                          name: Producer
                        profileUrl: https://gondola.cc/janedoe
                      stats:
                        postCount: 6
                        views: 42000
                        likes: 2500
                        shares: 320
                        comments: 180
                      posts:
                        - id: 456
                          url: https://twitter.com/t/status/example-post
                          gondolaUrl: https://gondola.cc/posts/456
                          account: janedoe
                          network: twitter
                          type: image
                          postedAt: "2026-03-01T14:30:00.000Z"
                          thumbnailUrl: https://img.gondola.cc/tr:w-,h-,fo-auto/thumbnails/example.jpg
                          views: 12000
                          likes: 950
                          shares: 130
                          comments: 40
                          credits:
                            - id: 7001
                              user:
                                id: 123
                                name: Jane Doe
                                username: janedoe
                                avatarUrl: https://img.gondola.cc/tr:w-400,h-400,fo-auto/avatars/janedoe.jpg
                              role:
                                id: 9
                                name: Producer
                      paging:
                        from: 0
                        size: 5
                        returned: 5
                        nextFrom: null
                        count: 6
                proTier:
                  summary: PRO user response with pagination
                  value:
                    success: 1
                    error: 0
                    data:
                      access:
                        tier: pro
                        isPro: true
                        canPaginate: true
                        maxPosts: null
                      user:
                        id: 123
                        name: Jane Doe
                        username: janedoe
                        avatarUrl: https://img.gondola.cc/tr:w-400,h-400,fo-auto/avatars/janedoe.jpg
                        blurb: The best producer in the world.
                        defaultRole:
                          id: 9
                          name: Producer
                        profileUrl: https://gondola.cc/janedoe
                      stats:
                        postCount: 124
                        views: 1220000
                        likes: 84000
                        shares: 5400
                        comments: 4100
                      posts: []
                      paging:
                        from: 25
                        size: 25
                        returned: 25
                        nextFrom: 50
                        count: 124
        400:
          $ref: "#/components/responses/BadRequest"
        401:
          $ref: "#/components/responses/Unauthorized"
        403:
          $ref: "#/components/responses/Forbidden"
        429:
          $ref: "#/components/responses/RateLimited"
        500:
          $ref: "#/components/responses/ServerError"

components:
  headers:
    XRateLimitLimit:
      description: Maximum number of portfolio requests allowed per token within the current rate limit window.
      schema:
        type: integer
        example: 60
    XRateLimitRemaining:
      description: Remaining portfolio requests available for the current token in the current rate limit window.
      schema:
        type: integer
        example: 59
    XRateLimitReset:
      description: Number of seconds until the current token regains request capacity.
      schema:
        type: integer
        example: 60
    RetryAfter:
      description: Number of seconds a client should wait before retrying after a rate-limited response.
      schema:
        type: integer
        example: 12
  securitySchemes:
    BearerPortfolioToken:
      type: http
      scheme: bearer
      bearerFormat: portfolio-token
      description: |
        A Gondola portfolio token created via `POST /api/token/portfolio`.
        Send it as `Authorization: Bearer <token>`.

  responses:
    BadRequest:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            invalidQuery:
              value:
                success: 0
                error: 1
                message: size must be a non-negative integer
    Unauthorized:
      description: Portfolio token is missing or invalid
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            missingOrInvalidToken:
              value:
                success: 0
                error: 1
                message: Invalid portfolio token
    Forbidden:
      description: Request is authenticated but not allowed for this access tier
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            freeTierFilters:
              value:
                success: 0
                error: 1
                message: Portfolio API filters require a PRO subscription
            freeTierSort:
              value:
                success: 0
                error: 1
                message: Portfolio API sorting requires a PRO subscription
    RateLimited:
      description: Portfolio token has exceeded the rate limit for the current sliding window
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            tooManyRequests:
              value:
                success: 0
                error: 1
                message: Portfolio API rate limit exceeded
    ServerError:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"

  schemas:
    PortfolioResponseEnvelope:
      type: object
      required:
        - success
        - error
        - data
      properties:
        success:
          type: integer
          format: int32
          example: 1
        error:
          type: integer
          format: int32
          example: 0
        data:
          $ref: "#/components/schemas/PortfolioResponse"

    ErrorEnvelope:
      type: object
      properties:
        success:
          type: integer
          format: int32
          example: 0
        error:
          type: integer
          format: int32
          example: 1
        message:
          type: string
          example: Invalid portfolio token

    PortfolioResponse:
      type: object
      required:
        - access
        - user
        - stats
        - posts
        - paging
      properties:
        access:
          $ref: "#/components/schemas/PortfolioAccess"
        user:
          $ref: "#/components/schemas/PortfolioUser"
        stats:
          $ref: "#/components/schemas/PortfolioStats"
        posts:
          type: array
          items:
            $ref: "#/components/schemas/PortfolioPost"
        paging:
          $ref: "#/components/schemas/PortfolioPaging"

    PortfolioAccess:
      type: object
      required:
        - tier
        - isPro
        - canPaginate
        - maxPosts
      properties:
        tier:
          type: string
          enum:
            - free
            - pro
          description: Current behavior tier applied to this token owner.
        isPro:
          type: boolean
          description: Whether the token owner currently has PRO access.
        canPaginate:
          type: boolean
          description: Whether `from` and `size` pagination controls are honored.
        maxPosts:
          type: integer
          nullable: true
          description: |
            Maximum posts returned for non-paginated access tiers.
            `null` means no special cap beyond normal page size behavior.

    PortfolioUser:
      type: object
      required:
        - id
        - name
        - username
        - avatarUrl
        - defaultRole
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        username:
          type: string
          nullable: true
        avatarUrl:
          type: string
          description: Fully qualified avatar URL suitable for rendering.
        blurb:
          type: string
          nullable: true
        defaultRole:
          allOf:
            - $ref: "#/components/schemas/PortfolioRole"
          nullable: true
        profileUrl:
          type: string
          nullable: true
          description: Absolute URL for the user's Gondola profile.

    PortfolioStats:
      type: object
      required:
        - postCount
        - views
        - likes
        - shares
        - comments
      properties:
        postCount:
          type: integer
        views:
          type: integer
        likes:
          type: integer
        shares:
          type: integer
        comments:
          type: integer

    PortfolioPost:
      type: object
      required:
        - id
        - gondolaUrl
        - account
        - network
        - type
        - postedAt
        - thumbnailUrl
        - views
        - likes
        - shares
        - comments
        - credits
      properties:
        id:
          type: integer
          format: int64
        url:
          type: string
          nullable: true
          description: Original social post URL when available.
        gondolaUrl:
          type: string
          description: Absolute Gondola URL for the post.
        account:
          type: string
        network:
          type: string
          enum:
            - twitter
            - instagram
            - facebook
            - youtube
            - tiktok
            - behance
            - personal
            - linkedin
            - twitch
            - threads
            - snapchat
          description: Social network the post was published on.
        type:
          type: string
          enum:
            - video
            - gif
            - image
            - text
            - story
          description: Content type of the post.
        postedAt:
          type: string
          format: date-time
        thumbnailUrl:
          type: string
          description: Fully qualified thumbnail URL suitable for rendering.
        views:
          type: integer
        likes:
          type: integer
        shares:
          type: integer
        comments:
          type: integer
        credits:
          type: array
          items:
            $ref: "#/components/schemas/PortfolioCredit"

    PortfolioCredit:
      type: object
      required:
        - id
        - user
        - role
      properties:
        id:
          type: integer
          format: int64
        user:
          allOf:
            - $ref: "#/components/schemas/PortfolioCreditUser"
          nullable: true
        role:
          allOf:
            - $ref: "#/components/schemas/PortfolioRole"
          nullable: true

    PortfolioCreditUser:
      type: object
      required:
        - id
        - name
        - username
        - avatarUrl
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        username:
          type: string
          nullable: true
        avatarUrl:
          type: string
          description: Fully qualified avatar URL suitable for rendering.

    PortfolioRole:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string

    PortfolioPaging:
      type: object
      required:
        - from
        - size
        - returned
        - nextFrom
        - count
      properties:
        from:
          type: integer
        size:
          type: integer
        returned:
          type: integer
        nextFrom:
          type: integer
          nullable: true
          description: |
            Offset for the next page. `null` means there is no next page or
            pagination is not available for this access tier.
        count:
          type: integer
