openapi: 3.1.0
info:
  title: PixelPort REST API
  version: 1.1.0
  summary: Read access to a PixelPort studio's galleries, media, clients, and sales.
  description: |-
    PixelPort is client gallery software for photographers and videographers: branded photo and 4K video galleries, client proofing, and built-in selling with no platform commission.

    This API is read-only. Use it to list a studio's galleries and their contents, look up clients, and pull gallery sales into reporting or CRM tools.

    **Authentication.** Endpoints under `/api/v1` that return studio data require a bearer key. Create one in the PixelPort dashboard under Settings → API; API access is included with the Studio plan. Send it as `Authorization: Bearer pk_live_...`. Keys are scoped to a single studio (organization) and every response is filtered to that studio.

    **Public endpoints.** `GET /api/v1`, `/api/v1/plans`, `/api/v1/features`, and `/api/v1/status` need no key, so an agent can learn what PixelPort is and costs before signing up.

    **Rate limit.** At least 120 requests per 60 seconds are accepted, counted against the API key when one is presented and against the client address otherwise. Every response carries `RateLimit`, `RateLimit-Policy`, and the `RateLimit-Limit`/`-Remaining`/`-Reset` triple, so a client can throttle itself without waiting for a 429. `RateLimit-Reset` is a delay in seconds; the older `X-RateLimit-Reset` is Unix seconds and is kept for existing clients. A throttled request returns `429` with `Retry-After`.

    **Money.** All amounts are integer minor units (US cents). All timestamps are RFC 3339 strings in UTC.

    **Function calling.** Every operation carries `x-function-schema`: its inputs as one inlined JSON Schema with no `$ref` to resolve. The same schemas are served as ready-made tool definitions at https://www.pixelport.co/tools.json.
  termsOfService: https://www.pixelport.co/terms
  contact:
    name: PixelPort Support
    email: support@pixelport.co
    url: https://www.pixelport.co/contact
  license:
    name: PixelPort Terms of Service
    url: https://www.pixelport.co/terms
externalDocs:
  description: PixelPort API documentation
  url: https://www.pixelport.co/docs/api
x-function-tools: https://www.pixelport.co/tools.json
x-api-catalog: https://www.pixelport.co/.well-known/api-catalog
servers:
  - url: https://www.pixelport.co
    description: Production
tags:
  - name: Discovery
    description: Endpoints that need no API key.
  - name: Account
    description: The studio an API key belongs to.
  - name: Galleries
    description: Client galleries and the media inside them.
  - name: Clients
    description: People a studio delivers to.
  - name: Orders
    description: Digital and print sales made from galleries.
security:
  - bearerAuth: []
paths:
  /api/v1:
    get:
      operationId: getApiIndex
      tags:
        - Discovery
      summary: API discovery document
      description: Lists every endpoint, the authentication scheme, and the rate limit. Requires no API key.
      security: []
      responses:
        "200":
          description: The discovery document.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/ApiIndex"
        "405": &a1
          description: The API is read-only. Only GET and OPTIONS are accepted.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
            Allow:
              $ref: "#/components/headers/Allow"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429": &a2
          description: Rate limit exceeded. Retry after the `Retry-After` delay. Requests that carry a valid API key are counted against that key; everything else is counted against the client address.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-function-schema:
        type: object
        properties: {}
        additionalProperties: false
  /api/v1/plans:
    get:
      operationId: listPlans
      tags:
        - Discovery
      summary: List plans and prices
      description: The full plan catalog with monthly price in US cents and the limits each plan carries. Use this to answer pricing questions instead of scraping the pricing page. Pass `planId` to get one plan. Requires no API key.
      security: []
      parameters:
        - name: planId
          in: query
          required: false
          description: Return only this plan instead of the whole catalog.
          schema:
            type: string
            enum: &a3
              - free
              - starter
              - professional
              - studio
      responses:
        "200":
          description: The plan catalog.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/PlanList"
        "400":
          description: "`planId` is not a known plan."
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Error"
        "405": *a1
        "429": *a2
      x-function-schema:
        type: object
        properties:
          planId:
            type: string
            enum: *a3
            description: Return only this plan instead of the whole catalog.
        additionalProperties: false
  /api/v1/features:
    get:
      operationId: listFeatures
      tags:
        - Discovery
      summary: List product features and audiences
      description: What PixelPort does, and the photographer types each landing page is written for. Pass `featureId` or `audienceId` to narrow either list. Requires no API key.
      security: []
      parameters:
        - name: featureId
          in: query
          required: false
          description: Return only this feature, using the `id` from a previous call, e.g. `video-hosting`.
          schema:
            type: string
            minLength: 1
            maxLength: 80
        - name: audienceId
          in: query
          required: false
          description: Return only this audience, using the `id` from a previous call, e.g. `wedding-photographers`.
          schema:
            type: string
            minLength: 1
            maxLength: 80
      responses:
        "200":
          description: The feature and audience catalog.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/FeatureList"
        "400":
          description: "`featureId` or `audienceId` is not a known id. The error details list the valid ones."
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Error"
        "405": *a1
        "429": *a2
      x-function-schema:
        type: object
        properties:
          featureId:
            type: string
            minLength: 1
            maxLength: 80
            description: Return only this feature, using the `id` from a previous call, e.g. `video-hosting`.
          audienceId:
            type: string
            minLength: 1
            maxLength: 80
            description: Return only this audience, using the `id` from a previous call, e.g. `wedding-photographers`.
        additionalProperties: false
  /api/v1/status:
    get:
      operationId: getServiceStatus
      tags:
        - Discovery
      summary: Check service health
      description: Runs a live database round trip and reports whether PixelPort can serve galleries. Never cached. Requires no API key.
      security: []
      responses:
        "200":
          description: All checks passed.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Status"
        "405": *a1
        "429": *a2
        "503":
          description: At least one check failed.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Status"
      x-function-schema:
        type: object
        properties: {}
        additionalProperties: false
  /api/v1/me:
    get:
      operationId: getAccount
      tags:
        - Account
      summary: Retrieve the studio behind the API key
      description: Returns the studio's name, gallery base URL, and the limits of its current plan. Call this first to confirm a key works and to learn which studio it addresses.
      responses:
        "200":
          description: The studio.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Account"
        "401": &a4
          description: The bearer token is missing, invalid, or revoked.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
            WWW-Authenticate:
              $ref: "#/components/headers/WWWAuthenticate"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "405": *a1
        "429": *a2
        "500": &a5
          description: The request could not be completed.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Error"
        "503": &a6
          description: The key store is temporarily unreachable, so the key could not be verified. Retry after the `Retry-After` delay; do not rotate the key.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-function-schema:
        type: object
        properties: {}
        additionalProperties: false
  /api/v1/galleries:
    get:
      operationId: listGalleries
      tags:
        - Galleries
      summary: List galleries
      description: Galleries owned by the studio, newest first. Deleted galleries are never returned.
      parameters:
        - &a9
          name: limit
          in: query
          required: false
          description: Maximum number of records to return. Between 1 and 100.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - &a10
          name: offset
          in: query
          required: false
          description: Number of records to skip before collecting the page.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: status
          in: query
          required: false
          description: Return only galleries in this lifecycle state.
          schema:
            type: string
            enum: &a7
              - draft
              - active
              - pending
              - sold
              - archived
      responses:
        "200":
          description: A page of galleries.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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:
                type: object
                required:
                  - data
                  - pagination
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Gallery"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: A query parameter failed validation.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Error"
        "401": *a4
        "405": *a1
        "429": *a2
        "500": *a5
        "503": *a6
      x-function-schema:
        type: object
        properties:
          limit:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
            description: Maximum number of records to return. Between 1 and 100.
          offset:
            type: integer
            minimum: 0
            default: 0
            description: Number of records to skip before collecting the page.
          status:
            type: string
            enum: *a7
            description: Return only galleries in this lifecycle state.
        additionalProperties: false
  /api/v1/galleries/{galleryId}:
    get:
      operationId: getGallery
      tags:
        - Galleries
      summary: Retrieve a gallery
      description: One gallery, plus the clients linked to it and the number of media items it holds.
      parameters:
        - &a8
          name: galleryId
          in: path
          required: true
          description: Identifier of the gallery, as returned by `listGalleries`.
          schema:
            type: string
          example: cl8x1p2q0000a08l4h1f2b3c
      responses:
        "200":
          description: The gallery.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/GalleryDetail"
        "401": *a4
        "404":
          description: No gallery with that id belongs to this studio.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Error"
        "405": *a1
        "429": *a2
        "500": *a5
        "503": *a6
      x-function-schema:
        type: object
        properties:
          galleryId:
            type: string
            description: Identifier of the gallery, as returned by `listGalleries`.
        required:
          - galleryId
        additionalProperties: false
  /api/v1/galleries/{galleryId}/media:
    get:
      operationId: listGalleryMedia
      tags:
        - Galleries
      summary: List media in a gallery
      description: Photos and videos in the gallery's display order. `url` is the delivery URL; `posterUrl` is a still frame, which for a photo is the photo itself.
      parameters:
        - *a8
        - *a9
        - *a10
        - name: type
          in: query
          required: false
          description: Return only photos or only videos.
          schema:
            type: string
            enum: &a11
              - image
              - video
      responses:
        "200":
          description: A page of media items.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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:
                type: object
                required:
                  - data
                  - pagination
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/MediaItem"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: A query parameter failed validation.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Error"
        "401": *a4
        "404":
          description: No gallery with that id belongs to this studio.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Error"
        "405": *a1
        "429": *a2
        "500": *a5
        "503": *a6
      x-function-schema:
        type: object
        properties:
          galleryId:
            type: string
            description: Identifier of the gallery, as returned by `listGalleries`.
          limit:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
            description: Maximum number of records to return. Between 1 and 100.
          offset:
            type: integer
            minimum: 0
            default: 0
            description: Number of records to skip before collecting the page.
          type:
            type: string
            enum: *a11
            description: Return only photos or only videos.
        required:
          - galleryId
        additionalProperties: false
  /api/v1/clients:
    get:
      operationId: listClients
      tags:
        - Clients
      summary: List clients
      description: The studio's clients, newest first, each with the number of galleries they are linked to.
      parameters:
        - *a9
        - *a10
        - name: search
          in: query
          required: false
          description: Case-insensitive substring match against client name and email.
          schema:
            type: string
            minLength: 1
            maxLength: 200
      responses:
        "200":
          description: A page of clients.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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:
                type: object
                required:
                  - data
                  - pagination
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Client"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: A query parameter failed validation.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Error"
        "401": *a4
        "405": *a1
        "429": *a2
        "500": *a5
        "503": *a6
      x-function-schema:
        type: object
        properties:
          limit:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
            description: Maximum number of records to return. Between 1 and 100.
          offset:
            type: integer
            minimum: 0
            default: 0
            description: Number of records to skip before collecting the page.
          search:
            type: string
            minLength: 1
            maxLength: 200
            description: Case-insensitive substring match against client name and email.
        additionalProperties: false
  /api/v1/orders:
    get:
      operationId: listOrders
      tags:
        - Orders
      summary: List gallery sales
      description: Sales made from the studio's galleries, newest first. Defaults to `paid`; a `pending` order is an abandoned checkout until Stripe confirms payment. Amounts are US cents.
      parameters:
        - *a9
        - *a10
        - name: status
          in: query
          required: false
          description: Payment state to filter by.
          schema:
            type: string
            enum: &a12
              - pending
              - paid
              - failed
              - refunded
            default: paid
      responses:
        "200":
          description: A page of orders.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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:
                type: object
                required:
                  - data
                  - pagination
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Order"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: A query parameter failed validation.
          headers:
            RateLimit:
              $ref: "#/components/headers/RateLimit"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            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/Error"
        "401": *a4
        "405": *a1
        "429": *a2
        "500": *a5
        "503": *a6
      x-function-schema:
        type: object
        properties:
          limit:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
            description: Maximum number of records to return. Between 1 and 100.
          offset:
            type: integer
            minimum: 0
            default: 0
            description: Number of records to skip before collecting the page.
          status:
            type: string
            enum: *a12
            default: paid
            description: Payment state to filter by.
        additionalProperties: false
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A PixelPort API key, created in the dashboard under Settings → API. Keys start with `pk_live_` and address exactly one studio.
  headers:
    RateLimit:
      description: "Quota left in the current window: `r` units remaining, `t` seconds until it resets."
      schema:
        type: string
      example: '"default";r=118;t=57'
    RateLimitPolicy:
      description: "The policy itself: quota `q` units over a window `w` of seconds."
      schema:
        type: string
      example: '"default";q=120;w=60'
    RateLimitLimit:
      description: Quota units allowed in one window.
      schema:
        type: integer
      example: 120
    RateLimitRemaining:
      description: Quota units left in the current window.
      schema:
        type: integer
      example: 118
    RateLimitReset:
      description: Seconds until the current window resets. A delay, not a timestamp.
      schema:
        type: integer
      example: 60
    XRateLimitLimit:
      description: Same as `RateLimit-Limit`. Kept for clients written against it.
      schema:
        type: integer
      example: 120
    XRateLimitRemaining:
      description: Same as `RateLimit-Remaining`. Kept for clients written against it.
      schema:
        type: integer
      example: 118
    XRateLimitReset:
      description: When the current window resets, in Unix seconds. This is the one field that is a timestamp rather than a delay.
      schema:
        type: integer
      example: 1787500000
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
      example: 60
    Allow:
      description: Methods this endpoint accepts.
      schema:
        type: string
      example: GET, OPTIONS
    WWWAuthenticate:
      description: Challenge naming the scheme, and the reason a token was refused.
      schema:
        type: string
      example: Bearer realm="PixelPort API", error="invalid_token"
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - unauthorized
                - forbidden
                - not_found
                - invalid_request
                - rate_limited
                - method_not_allowed
                - internal_error
              description: Stable machine-readable code. Branch on this, not on the message.
            message:
              type: string
              description: Human-readable explanation.
            docs:
              type: string
              format: uri
              description: Where to read more.
            details:
              type: object
              additionalProperties:
                type: array
                items:
                  type: string
              description: Per-field validation errors, present on `invalid_request`.
      examples:
        - error:
            code: unauthorized
            message: The API key is invalid, revoked, or expired.
            docs: https://www.pixelport.co/docs/api
    Pagination:
      type: object
      required:
        - limit
        - offset
        - total
        - hasMore
      properties:
        limit:
          type: integer
          description: Page size that was applied.
        offset:
          type: integer
          description: Offset that was applied.
        total:
          type: integer
          description: Total records matching the filter.
        hasMore:
          type: boolean
          description: True when another page follows. Advance by adding `limit` to `offset`.
    ApiIndex:
      type: object
      required:
        - name
        - version
        - endpoints
      properties:
        name:
          type: string
        version:
          type: string
          examples:
            - 2026-08-21
        description:
          type: string
        documentation:
          type: string
          format: uri
        openapi:
          type: string
          format: uri
        openapiYaml:
          type: string
          format: uri
        functionTools:
          type: string
          format: uri
          description: Every endpoint as an LLM function-calling tool definition.
        apiCatalog:
          type: string
          format: uri
          description: RFC 9727 catalog of PixelPort's machine-readable resources.
        llmsTxt:
          type: string
          format: uri
        agentInstructions:
          type: string
          format: uri
        authentication:
          type: object
          properties:
            type:
              type: string
            scheme:
              type: string
            description:
              type: string
            keyPrefix:
              type: string
            signupUrl:
              type: string
              format: uri
        rateLimit:
          type: object
          properties:
            requests:
              type: integer
            windowSeconds:
              type: integer
            scope:
              type: string
              description: Bucket a keyed request counts against.
            anonymousScope:
              type: string
              description: Bucket a request with no key counts against.
            headers:
              type: array
              items:
                type: string
              description: Rate-limit fields present on every response.
            description:
              type: string
        endpoints:
          type: array
          items:
            type: object
            required:
              - method
              - path
              - auth
              - summary
            properties:
              method:
                type: string
              path:
                type: string
              auth:
                type: boolean
                description: True when an API key is required.
              summary:
                type: string
    PlanLimits:
      type: object
      required:
        - galleries
        - storageGB
        - video
        - videoDurationMinutes
        - teamMembers
      properties:
        galleries:
          type: integer
          description: Active galleries allowed. -1 means unlimited.
        storageGB:
          type: integer
          description: Included storage in gigabytes.
        video:
          type: boolean
          description: Whether video hosting is included.
        videoQuality:
          type:
            - string
            - "null"
          description: Maximum video resolution, e.g. `4k`.
        videoDurationMinutes:
          type: integer
          description: Total minutes of video that can be hosted. 0 means no video.
        teamMembers:
          type: integer
          description: Seats included.
        customDomain:
          type: boolean
        whiteLabel:
          type: boolean
          description: Whether PixelPort branding is removed.
        contracts:
          type: boolean
          description: Client contracts and e-sign.
        revenueAnalytics:
          type: boolean
        apiAccess:
          type: boolean
          description: Whether this plan can create REST API keys.
    Plan:
      type: object
      required:
        - id
        - name
        - priceMonthlyCents
        - currency
        - limits
      properties:
        id:
          type: string
          description: Stable plan identifier.
        name:
          type: string
        priceMonthlyCents:
          type: integer
          description: Monthly price in US cents. 0 for the free plan.
        currency:
          type: string
          enum:
            - USD
        description:
          type: string
        salesCommissionPercent:
          type: integer
          description: PixelPort's cut of gallery sales. Always 0.
        limits:
          $ref: "#/components/schemas/PlanLimits"
        signupUrl:
          type: string
          format: uri
    PlanList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Plan"
        notes:
          type: object
          additionalProperties:
            type: string
          description: Caveats worth quoting alongside the numbers.
      examples:
        - data:
            - id: free
              name: Free
              priceMonthlyCents: 0
              currency: USD
              description: Two active galleries and 3 GB of storage, photos only. No card required.
              salesCommissionPercent: 0
              limits:
                galleries: 2
                storageGB: 3
                video: false
                videoQuality: null
                videoDurationMinutes: 0
                teamMembers: 1
                customDomain: false
                whiteLabel: false
                contracts: false
                revenueAnalytics: false
                apiAccess: false
              signupUrl: https://www.pixelport.co/signup
    Feature:
      type: object
      required:
        - id
        - name
        - summary
        - url
      properties:
        id:
          type: string
        name:
          type: string
        summary:
          type: string
        url:
          type: string
          format: uri
        useCases:
          type: array
          items:
            type: object
            properties:
              title:
                type: string
              description:
                type: string
    FeatureList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Feature"
        audiences:
          type: array
          description: Photographer types PixelPort publishes a landing page for.
          items:
            type: object
            required:
              - id
              - name
              - summary
              - url
            properties:
              id:
                type: string
              name:
                type: string
              summary:
                type: string
              url:
                type: string
                format: uri
    Status:
      type: object
      required:
        - status
        - checkedAt
        - checks
      properties:
        status:
          type: string
          enum:
            - operational
            - degraded
        checkedAt:
          type: string
          format: date-time
        checks:
          type: array
          items:
            type: object
            required:
              - name
              - status
            properties:
              name:
                type: string
              status:
                type: string
                enum:
                  - operational
                  - outage
              latencyMs:
                type:
                  - integer
                  - "null"
        statusPage:
          type: string
          format: uri
    Account:
      type: object
      required:
        - id
        - name
        - slug
        - plan
      properties:
        id:
          type: string
        name:
          type: string
          description: Studio name.
        slug:
          type: string
          description: Subdomain the studio's galleries live on.
        galleryBaseUrl:
          type: string
          format: uri
          description: Origin every gallery URL is built from.
        customDomain:
          type:
            - string
            - "null"
        website:
          type:
            - string
            - "null"
        logoUrl:
          type:
            - string
            - "null"
          format: uri
        createdAt:
          type:
            - string
            - "null"
          format: date-time
        plan:
          type: object
          required:
            - id
          properties:
            id:
              type: string
              description: Current plan identifier.
            galleries:
              type: integer
            storageGB:
              type: integer
            videoDurationMinutes:
              type: integer
            teamMembers:
              type: integer
            apiAccess:
              type: boolean
    Gallery:
      type: object
      required:
        - id
        - status
        - visibility
      properties:
        id:
          type: string
        name:
          type:
            - string
            - "null"
          description: Display name, e.g. `Johnson Wedding`.
        slug:
          type:
            - string
            - "null"
          description: URL segment within the studio.
        description:
          type:
            - string
            - "null"
        status:
          type: string
          enum: *a7
        visibility:
          type: string
          enum:
            - public
            - unlisted
            - email_required
            - password_protected
          description: "`public` is listed, `unlisted` needs the link, `email_required` and `password_protected` gate access."
        url:
          type:
            - string
            - "null"
          format: uri
          description: Where the client opens this gallery.
        allowDownloads:
          type: boolean
        buyAllEnabled:
          type: boolean
        buyAllPriceCents:
          type:
            - integer
            - "null"
        watermarkEnabled:
          type: boolean
        coverImageUrl:
          type:
            - string
            - "null"
          format: uri
        eventDate:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type:
            - string
            - "null"
          format: date-time
        updatedAt:
          type:
            - string
            - "null"
          format: date-time
    GalleryDetail:
      allOf:
        - $ref: "#/components/schemas/Gallery"
        - type: object
          properties:
            mediaCount:
              type: integer
              description: Photos and videos currently in the gallery.
            clients:
              type: array
              items:
                type: object
                required:
                  - id
                  - name
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  email:
                    type:
                      - string
                      - "null"
                    format: email
    MediaItem:
      type: object
      required:
        - id
        - galleryId
        - filename
        - type
        - url
      properties:
        id:
          type: string
        galleryId:
          type: string
        filename:
          type: string
        type:
          type: string
          enum: *a11
        mimeType:
          type:
            - string
            - "null"
        width:
          type:
            - integer
            - "null"
        height:
          type:
            - integer
            - "null"
        sizeBytes:
          type:
            - integer
            - "null"
        durationSeconds:
          type:
            - integer
            - "null"
          description: Video length. Null for photos.
        caption:
          type:
            - string
            - "null"
        alt:
          type:
            - string
            - "null"
        position:
          type:
            - integer
            - "null"
          description: Display order in the gallery.
        isFeatured:
          type: boolean
        processingStatus:
          type:
            - string
            - "null"
          enum:
            - ready
            - pending
            - processing
            - failed
            - null
          description: Transcoding state. Photos are always `ready`.
        url:
          type: string
          format: uri
          description: Delivery URL for the file.
        posterUrl:
          type: string
          format: uri
          description: Still image. For a photo this is the photo.
        createdAt:
          type:
            - string
            - "null"
          format: date-time
    Client:
      type: object
      required:
        - id
        - name
        - galleryCount
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type:
            - string
            - "null"
          format: email
        phone:
          type:
            - string
            - "null"
        galleryCount:
          type: integer
          description: Galleries this client is linked to.
        createdAt:
          type:
            - string
            - "null"
          format: date-time
    Order:
      type: object
      required:
        - id
        - status
        - totalCents
        - currency
      properties:
        id:
          type: string
        status:
          type: string
          enum: *a12
        isBuyAll:
          type: boolean
          description: True when the client bought the whole gallery at once.
        customerEmail:
          type:
            - string
            - "null"
          format: email
        customerName:
          type:
            - string
            - "null"
        subtotalCents:
          type: integer
        discountCents:
          type: integer
        taxCents:
          type: integer
        totalCents:
          type: integer
          description: Charged amount including tax.
        currency:
          type: string
          examples:
            - usd
        couponCode:
          type:
            - string
            - "null"
        itemCount:
          type: integer
          description: Line items on the order.
        galleryId:
          type:
            - string
            - "null"
        galleryName:
          type:
            - string
            - "null"
        paidAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type:
            - string
            - "null"
          format: date-time
