openapi: 3.1.0
info:
  title: Folio Public API
  version: v1
  description: Read-only, key-authenticated, tenant-scoped access to your published portfolio data. Every request must present an API key as a bearer token; the key resolves the tenant, so there is no tenant id in the path. Responses are never cached by shared proxies and are rate limited per key.
  contact:
    name: Folio
    url: https://portfolio.wrxstack.com
  license:
    name: Proprietary
servers:
  - url: https://portfolio.wrxstack.com
    description: Production base URL.
tags:
  - name: Portfolio
    description: The complete published portfolio document in one call.
  - name: Profile
    description: The published profile (hero) details.
  - name: Outcomes
    description: Published case-study outcomes.
  - name: Posts
    description: Published blog posts.
  - name: Pages
    description: Published custom pages.
  - name: Experience
    description: Work-experience timeline.
  - name: Skills
    description: Published skills.
  - name: Stats
    description: Headline stats / metrics.
  - name: Awards
    description: Awards and recognitions.
  - name: Education
    description: Education and certification credentials.
  - name: Testimonials
    description: Published testimonials.
  - name: Links
    description: Social and contact links.
  - name: Meta
    description: Machine-readable spec + tooling downloads.
security:
  - apiKey: []
paths:
  /api/v1/portfolio:
    get:
      operationId: getPortfolio
      tags:
        - Portfolio
      summary: Get the full published portfolio
      description: "Returns the entire published portfolio document for the authenticated tenant in a single response: profile, outcomes, posts, experience, skills, stats, awards, education, testimonials, links, and pages."
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PortfolioResponse"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/profile:
    get:
      operationId: getProfile
      tags:
        - Profile
      summary: Get the public profile
      description: "Returns the published profile: name, headline, bio summary, location, and avatar."
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProfileItem"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /api/v1/outcomes:
    get:
      operationId: getOutcomes
      tags:
        - Outcomes
      summary: List published outcomes
      description: Returns the published case-study outcomes, paginated and in display order.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutcomeCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/outcomes/{slug}:
    get:
      operationId: getOutcomesBySlug
      tags:
        - Outcomes
      summary: Get one outcome by slug
      description: Returns a single published outcome identified by its slug.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutcomeItem"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: The requested item does not exist or is unpublished.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: slug
          in: path
          required: true
          description: The slug of the item to fetch.
          schema:
            type: string
          example: example-slug
  /api/v1/posts:
    get:
      operationId: getPosts
      tags:
        - Posts
      summary: List published blog posts
      description: Returns the published blog posts, paginated and in display order.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/posts/{slug}:
    get:
      operationId: getPostsBySlug
      tags:
        - Posts
      summary: Get one blog post by slug
      description: Returns a single published blog post identified by its slug.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostItem"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: The requested item does not exist or is unpublished.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: slug
          in: path
          required: true
          description: The slug of the item to fetch.
          schema:
            type: string
          example: example-slug
  /api/v1/pages:
    get:
      operationId: getPages
      tags:
        - Pages
      summary: List published custom pages
      description: Returns the published custom pages built with the page builder, in display order.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PageCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/pages/{slug}:
    get:
      operationId: getPagesBySlug
      tags:
        - Pages
      summary: Get one custom page by slug
      description: Returns a single published custom page, including its sanitized, asset-resolved blocks.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PageItem"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: The requested item does not exist or is unpublished.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: slug
          in: path
          required: true
          description: The slug of the item to fetch.
          schema:
            type: string
          example: example-slug
  /api/v1/experience:
    get:
      operationId: getExperience
      tags:
        - Experience
      summary: List experience entries
      description: Returns the work-experience timeline, paginated and in display order.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExperienceCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/skills:
    get:
      operationId: getSkills
      tags:
        - Skills
      summary: List skills
      description: Returns the published skills with their tags, paginated and in display order.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SkillCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/stats:
    get:
      operationId: getStats
      tags:
        - Stats
      summary: List headline stats
      description: Returns the headline metrics/stats, paginated and in display order.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/awards:
    get:
      operationId: getAwards
      tags:
        - Awards
      summary: List awards
      description: Returns the published awards and recognitions, paginated and in display order.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AwardCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/education:
    get:
      operationId: getEducation
      tags:
        - Education
      summary: List education and certifications
      description: Returns education and certification credentials, paginated and in display order. Each item carries a `kind` of `education` or `certification`.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CredentialCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/testimonials:
    get:
      operationId: getTestimonials
      tags:
        - Testimonials
      summary: List testimonials
      description: Returns the published testimonials, paginated and in display order.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TestimonialCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/links:
    get:
      operationId: getLinks
      tags:
        - Links
      summary: List social and contact links
      description: Returns the published social/contact links, paginated and in display order.
      responses:
        "200":
          description: Success.
          headers:
            X-RateLimit-Limit:
              description: Requests allowed per window.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: integer
            X-RateLimit-Reset:
              description: Unix epoch seconds when the window resets.
              schema:
                type: integer
            X-Request-Id:
              description: Correlation id echoed for the request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkCollection"
        "400":
          description: Invalid limit or offset query parameter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "403":
          description: The API key has been revoked or has expired.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: No published portfolio for this tenant, or item not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          description: Rate limit exceeded. Inspect Retry-After.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size (1 to 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: offset
          in: query
          required: false
          description: Zero-based item offset.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
  /api/v1/openapi.json:
    get:
      operationId: getOpenapiJson
      tags:
        - Meta
      summary: Download the OpenAPI 3.1 document (JSON)
      description: "Returns the machine-readable OpenAPI 3.1 specification for this API as JSON. Unauthenticated: the spec itself is public documentation."
      responses:
        "200":
          description: The requested document.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
      security: []
  /api/v1/openapi.yaml:
    get:
      operationId: getOpenapiYaml
      tags:
        - Meta
      summary: Download the OpenAPI 3.1 document (YAML)
      description: "Returns the machine-readable OpenAPI 3.1 specification for this API as YAML. Unauthenticated: the spec itself is public documentation."
      responses:
        "200":
          description: The requested document.
          content:
            application/yaml:
              schema:
                type: string
      security: []
  /api/v1/postman.json:
    get:
      operationId: getPostmanJson
      tags:
        - Meta
      summary: Download a Postman v2.1 collection
      description: Returns a ready-to-import Postman v2.1 collection generated from the OpenAPI spec, with a bearer-token variable and a base-URL variable. Unauthenticated.
      responses:
        "200":
          description: The requested document.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
      security: []
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: Folio API key (pk_live_...)
      description: "Present your API key as `Authorization: Bearer pk_live_...`. Keys are minted in the workspace API settings and are shown only once at creation. A key may be given an optional expiry; once it expires it stops working and requests return 403."
  schemas:
    Profile:
      type: object
      description: The published profile shown in the portfolio hero.
      required:
        - name
        - headline
      properties:
        name:
          type: string
          description: Display name of the portfolio owner / workspace.
        headline:
          type: string
          description: Primary hero headline.
        subheadline:
          type: string
          nullable: true
          description: Secondary hero line or short bio summary.
        location:
          type: string
          nullable: true
          description: Free-text location.
        avatarUrl:
          type: string
          nullable: true
          description: Public URL of the profile portrait, if set.
        introVideoUrl:
          type: string
          nullable: true
          description: Public URL of an uploaded intro video, if set.
        pronunciationAudioUrl:
          type: string
          nullable: true
          description: Public URL of a name-pronunciation clip, if set.
        primaryCta:
          type: object
          nullable: true
          description: Optional primary call to action.
          properties:
            label:
              type: string
              description: CTA label.
            href:
              type: string
              description: CTA target URL.
    Outcome:
      type: object
      description: A published case-study outcome.
      required:
        - slug
        - title
        - summary
        - metrics
      properties:
        slug:
          type: string
          description: URL-safe identifier, unique within the tenant.
        title:
          type: string
          description: Outcome title.
        client:
          type: string
          nullable: true
          description: Client name, if disclosed.
        summary:
          type: string
          description: Short summary line.
        seoTitle:
          type: string
          nullable: true
          description: SEO title override.
        seoDescription:
          type: string
          nullable: true
          description: SEO description override.
        heroImageUrl:
          type: string
          nullable: true
          description: Public hero image URL.
        ogImageUrl:
          type: string
          nullable: true
          description: Public Open Graph image URL.
        problem:
          type: string
          nullable: true
          description: Plain-text problem statement.
        approach:
          type: string
          nullable: true
          description: Plain-text approach.
        result:
          type: string
          nullable: true
          description: Plain-text result.
        metrics:
          type: array
          description: Headline metrics.
          items: &a1
            type: object
            required:
              - label
              - value
            properties:
              label:
                type: string
                description: Metric label.
              value:
                type: string
                description: Metric value (already formatted).
        isIndexable:
          type: boolean
          description: Whether the outcome is indexable.
        publishedAt:
          type: string
          nullable: true
          description: ISO-8601 publish timestamp.
        updatedAt:
          type: string
          nullable: true
          description: ISO-8601 last-updated timestamp.
        position:
          type: integer
          description: Display order.
    Post:
      type: object
      description: A published blog post.
      required:
        - slug
        - title
        - excerpt
      properties:
        slug:
          type: string
          description: URL-safe identifier, unique within the tenant.
        title:
          type: string
          description: Post title.
        excerpt:
          type: string
          description: Short excerpt.
        seoTitle:
          type: string
          nullable: true
          description: SEO title override.
        seoDescription:
          type: string
          nullable: true
          description: SEO description override.
        coverImageUrl:
          type: string
          nullable: true
          description: Public cover image URL.
        body:
          type: string
          nullable: true
          description: Plain-text body.
        publishedAt:
          type: string
          nullable: true
          description: ISO-8601 publish timestamp.
        updatedAt:
          type: string
          nullable: true
          description: ISO-8601 last-updated timestamp.
        position:
          type: integer
          description: Display order.
    Page:
      type: object
      description: A published custom page from the page builder.
      required:
        - slug
        - title
        - blocks
        - showInNav
      properties:
        slug:
          type: string
          description: URL-safe identifier, unique within the tenant.
        title:
          type: string
          description: Page title.
        blocks:
          type: array
          description: Sanitized, asset-resolved content blocks.
          items:
            type: object
            additionalProperties: true
        showInNav:
          type: boolean
          description: Whether the page appears in the public nav.
        seoTitle:
          type: string
          nullable: true
          description: SEO title override.
        seoDescription:
          type: string
          nullable: true
          description: SEO description override.
        ogImageUrl:
          type: string
          nullable: true
          description: Public Open Graph image URL.
        publishedAt:
          type: string
          nullable: true
          description: ISO-8601 publish timestamp.
        updatedAt:
          type: string
          nullable: true
          description: ISO-8601 last-updated timestamp.
        position:
          type: integer
          description: Display order.
    Experience:
      type: object
      description: A work-experience timeline entry.
      required:
        - company
        - role
      properties:
        company:
          type: string
          description: Company name.
        role:
          type: string
          description: Role / title held.
        startDate:
          type: string
          nullable: true
          description: Start date (YYYY-MM-DD).
        endDate:
          type: string
          nullable: true
          description: End date (YYYY-MM-DD); absent if current.
        description:
          type: string
          nullable: true
          description: Plain-text description.
        logoUrl:
          type: string
          nullable: true
          description: Public company logo URL.
        position:
          type: integer
          description: Display order.
    Skill:
      type: object
      description: A published skill with optional tags.
      required:
        - name
        - tags
      properties:
        name:
          type: string
          description: Skill name.
        description:
          type: string
          nullable: true
          description: Optional description.
        tags:
          type: array
          description: Associated tags.
          items:
            type: string
        position:
          type: integer
          description: Display order.
    Stat:
      type: object
      description: A headline stat / metric.
      required:
        - value
        - label
      properties:
        value:
          type: string
          description: Stat value (already formatted).
        label:
          type: string
          description: Stat label.
        prefix:
          type: string
          nullable: true
          description: Optional leading unit composed before the figure (e.g. "$").
        suffix:
          type: string
          nullable: true
          description: Optional trailing unit composed after the figure (e.g. "%", "+").
        description:
          type: string
          nullable: true
          description: Optional supporting context line for the metric.
        icon:
          type: string
          nullable: true
          description: Optional allowlisted lucide icon key (e.g. "TrendingUp").
        position:
          type: integer
          description: Display order.
    Award:
      type: object
      description: A published award or recognition.
      required:
        - title
      properties:
        title:
          type: string
          description: Award title.
        issuer:
          type: string
          nullable: true
          description: Issuing organization.
        dateText:
          type: string
          nullable: true
          description: Free-text date label.
        category:
          type: string
          nullable: true
          description: Optional recognition category (e.g. "Industry").
        credentialUrl:
          type: string
          nullable: true
          description: Validated https link to the credential / announcement.
        description:
          type: string
          nullable: true
          description: Optional description.
        position:
          type: integer
          description: Display order.
    Credential:
      type: object
      description: An education or certification credential.
      required:
        - kind
        - title
      properties:
        kind:
          type: string
          enum:
            - education
            - certification
          description: Credential category.
        title:
          type: string
          description: Credential title (e.g. degree or certificate).
        subtitle:
          type: string
          nullable: true
          description: Institution / sub-line.
        periodText:
          type: string
          nullable: true
          description: Free-text period label.
        fieldOfStudy:
          type: string
          nullable: true
          description: Optional field of study / specialization.
        grade:
          type: string
          nullable: true
          description: Optional grade / result.
        location:
          type: string
          nullable: true
          description: Optional institution location.
        credentialUrl:
          type: string
          nullable: true
          description: Validated https link to verify the credential.
        ongoing:
          type: boolean
          description: Whether the period is open-ended (in progress).
        position:
          type: integer
          description: Display order.
    Testimonial:
      type: object
      description: A published testimonial.
      required:
        - quote
        - authorName
      properties:
        quote:
          type: string
          description: Plain-text quote.
        authorName:
          type: string
          description: Author name.
        authorRole:
          type: string
          nullable: true
          description: Author role / title.
        authorCompany:
          type: string
          nullable: true
          description: Author company.
        authorPhotoUrl:
          type: string
          nullable: true
          description: Public author photo URL.
        rating:
          type: integer
          description: 1-5 star rating, when set.
        source:
          type: string
          nullable: true
          description: Allowlisted source key (e.g. linkedin).
        sourceLabel:
          type: string
          nullable: true
          description: Human-readable source label.
        sourceUrl:
          type: string
          nullable: true
          description: Link back to the original review.
        dateText:
          type: string
          nullable: true
          description: Friendly display date of the quote.
        featured:
          type: boolean
          description: Whether the quote is featured.
    Link:
      type: object
      description: A social or contact link.
      required:
        - label
        - href
      properties:
        label:
          type: string
          description: Human-readable label.
        href:
          type: string
          description: Target URL.
        icon:
          type: string
          nullable: true
          description: Allowlisted icon key (brand or generic).
        description:
          type: string
          nullable: true
          description: Short supporting subtitle.
        featured:
          type: boolean
          description: Whether the link is featured.
    Metric: *a1
    PortfolioDocument:
      type: object
      description: The full published portfolio view model.
      required:
        - profile
        - outcomes
        - experience
        - testimonials
        - contact
      properties:
        profile:
          $ref: "#/components/schemas/Profile"
        outcomes:
          type: array
          items:
            $ref: "#/components/schemas/Outcome"
        posts:
          type: array
          items:
            $ref: "#/components/schemas/Post"
        pages:
          type: array
          items:
            $ref: "#/components/schemas/Page"
        experience:
          type: array
          items:
            $ref: "#/components/schemas/Experience"
        skills:
          type: array
          items:
            $ref: "#/components/schemas/Skill"
        stats:
          type: array
          items:
            $ref: "#/components/schemas/Stat"
        awards:
          type: array
          items:
            $ref: "#/components/schemas/Award"
        credentials:
          type: array
          items:
            $ref: "#/components/schemas/Credential"
        testimonials:
          type: array
          items:
            $ref: "#/components/schemas/Testimonial"
        contact:
          type: object
          additionalProperties: true
          description: Contact block.
        gallery:
          type: array
          items:
            type: object
            additionalProperties: true
    TenantRef:
      type: object
      description: The tenant the response is scoped to.
      required:
        - slug
        - name
      properties:
        slug:
          type: string
          description: Tenant slug.
        name:
          type: string
          description: Tenant display name.
    Pagination:
      type: object
      description: Pagination metadata for collection responses.
      required:
        - total
        - limit
        - offset
        - hasMore
      properties:
        total:
          type: integer
          description: Total items available.
        limit:
          type: integer
          description: Page size applied.
        offset:
          type: integer
          description: Offset applied.
        hasMore:
          type: boolean
          description: Whether more items follow this page.
    ErrorEnvelope:
      type: object
      description: Stable error envelope returned for every non-2xx response.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - bad_request
                - unauthorized
                - forbidden
                - rate_limited
                - not_found
                - internal_error
              description: Machine-readable error code.
            message:
              type: string
              description: Human-readable error message.
    ProfileItem:
      type: object
      description: A single Profile wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: &a2
          $ref: "#/components/schemas/TenantRef"
        data: &a3
          $ref: "#/components/schemas/Profile"
    ProfileCollection:
      type: object
      description: A paginated collection of Profile items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: &a4
          $ref: "#/components/schemas/Pagination"
        data:
          type: array
          items: *a3
    OutcomeItem:
      type: object
      description: A single Outcome wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a5
          $ref: "#/components/schemas/Outcome"
    OutcomeCollection:
      type: object
      description: A paginated collection of Outcome items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a5
    PostItem:
      type: object
      description: A single Post wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a6
          $ref: "#/components/schemas/Post"
    PostCollection:
      type: object
      description: A paginated collection of Post items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a6
    PageItem:
      type: object
      description: A single Page wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a7
          $ref: "#/components/schemas/Page"
    PageCollection:
      type: object
      description: A paginated collection of Page items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a7
    ExperienceItem:
      type: object
      description: A single Experience wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a8
          $ref: "#/components/schemas/Experience"
    ExperienceCollection:
      type: object
      description: A paginated collection of Experience items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a8
    SkillItem:
      type: object
      description: A single Skill wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a9
          $ref: "#/components/schemas/Skill"
    SkillCollection:
      type: object
      description: A paginated collection of Skill items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a9
    StatItem:
      type: object
      description: A single Stat wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a10
          $ref: "#/components/schemas/Stat"
    StatCollection:
      type: object
      description: A paginated collection of Stat items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a10
    AwardItem:
      type: object
      description: A single Award wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a11
          $ref: "#/components/schemas/Award"
    AwardCollection:
      type: object
      description: A paginated collection of Award items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a11
    CredentialItem:
      type: object
      description: A single Credential wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a12
          $ref: "#/components/schemas/Credential"
    CredentialCollection:
      type: object
      description: A paginated collection of Credential items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a12
    TestimonialItem:
      type: object
      description: A single Testimonial wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a13
          $ref: "#/components/schemas/Testimonial"
    TestimonialCollection:
      type: object
      description: A paginated collection of Testimonial items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a13
    LinkItem:
      type: object
      description: A single Link wrapped in the standard envelope.
      required:
        - apiVersion
        - tenant
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        data: &a14
          $ref: "#/components/schemas/Link"
    LinkCollection:
      type: object
      description: A paginated collection of Link items.
      required:
        - apiVersion
        - tenant
        - pagination
        - data
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        pagination: *a4
        data:
          type: array
          items: *a14
    PortfolioResponse:
      type: object
      description: The full portfolio document with tenant + theme + SEO metadata.
      required:
        - apiVersion
        - tenant
        - portfolio
      properties:
        apiVersion:
          type: string
          enum:
            - v1
        tenant: *a2
        theme:
          type: object
          properties:
            id:
              type: string
        seo:
          type: object
          properties:
            title:
              type: string
            description:
              type: string
        canonicalUrl:
          type: string
          nullable: true
        updatedAt:
          type: string
          nullable: true
        portfolio:
          $ref: "#/components/schemas/PortfolioDocument"
