openapi: 3.1.0
info:
  title: Passionfroot Partner API
  version: 1.0.0
  description: |
    Access a brand workspace's creators, collaborations, placements, proposals, and conversations.
    Every operation is restricted to the organization that owns the API key.
servers:
  - url: https://workspace.passionfroot.me/api/v1
security:
  - bearerAuth: []
tags:
  - name: Placements
  - name: Creators
  - name: Collaborations
  - name: Conversations
  - name: Proposals
paths:
  /placements:
    get:
      tags: [Placements]
      summary: List placements
      description: Returns scheduled content for the organization. Related creator, channel, collaboration, and campaign objects are opt-in through `include`.
      operationId: listPartnerPlacements
      x-required-scope: read:collaborations
      parameters:
        - { $ref: "#/components/parameters/StartDate" }
        - { $ref: "#/components/parameters/EndDate" }
        - { $ref: "#/components/parameters/MetricsUpdatedAfter" }
        - { $ref: "#/components/parameters/MetricsUpdatedBefore" }
        - name: collaborationId
          in: query
          description: Return placements belonging to this collaboration.
          schema: { type: string }
        - { $ref: "#/components/parameters/Status" }
        - name: include
          in: query
          description: Comma-separated expansions. Supported values are `creator`, `creator.channels`, `collaboration`, and `collaboration.campaign`.
          schema: { type: string }
          example: creator.channels,collaboration.campaign
        - { $ref: "#/components/parameters/Limit250" }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: A page of placements.
          headers:
            RateLimit: { $ref: "#/components/headers/RateLimit" }
            RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PartnerPlacementPage" }
              examples:
                default:
                  value:
                    data:
                      - id: plc_123
                        creatorId: creator_abc
                        creatorName: Jane Creator
                        collaborationId: collab_xyz
                        date: "2026-09-15T00:00:00.000Z"
                        name: Fall launch – Instagram Reel
                        status: confirmed
                        platform: instagram
                        postType: Reel
                        url: https://instagram.com/p/example
                        viewCount: 15000
                        likeCount: 1200
                        commentCount: 45
                        shareCount: 30
                        metricsLastUpdatedAt: "2026-09-20T14:30:00.000Z"
                        priceCents: 15750
                        usdPriceCents: 15750
                        currency: USD
                    pagination: { nextCursor: plc_122, hasMore: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /creators:
    get:
      tags: [Creators]
      summary: List creators
      description: Returns creators with a qualifying collaboration with the organization, including private relationship notes and labels.
      operationId: listPartnerCreators
      x-required-scope: read:collaborations
      parameters:
        - name: labelIds
          in: query
          description: Comma-separated workspace label IDs. A creator matching any label is returned.
          schema: { type: string }
        - name: include
          in: query
          description: Set to `channels` to include channels and channel metrics.
          schema: { type: string, enum: [channels] }
        - { $ref: "#/components/parameters/Limit100" }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: A page of creators.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreatorPage" }
              example:
                data:
                  - id: creator_abc
                    displayName: Jane Creator
                    country: US
                    note: Always delivers early
                    labels: [{ id: clb_abc, name: High performer }]
                pagination: { nextCursor: creator_def, hasMore: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /creators/{creatorId}:
    get:
      tags: [Creators]
      summary: Get a creator
      description: Returns one creator when the organization has a qualifying collaboration with them.
      operationId: getPartnerCreator
      x-required-scope: read:collaborations
      parameters:
        - { $ref: "#/components/parameters/CreatorId" }
        - name: include
          in: query
          description: Set to `channels` to include channels and metrics.
          schema: { type: string, enum: [channels] }
      responses:
        "200": { $ref: "#/components/responses/Creator" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/JsonNotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /labels:
    get:
      tags: [Creators]
      summary: List creator labels
      description: Returns the complete workspace label catalog in display order. This endpoint is not paginated.
      operationId: listPartnerCreatorLabels
      x-required-scope: read:collaborations
      responses:
        "200":
          description: The workspace's creator labels.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Label" } }
              example:
                data:
                  - { id: clb_abc, name: High performer, color: green, position: a0 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /collaborations:
    get:
      tags: [Collaborations]
      summary: List collaborations
      description: Returns collaborations for the organization, optionally expanded with campaign details.
      operationId: listPartnerCollaborations
      x-required-scope: read:collaborations
      parameters:
        - { $ref: "#/components/parameters/Status" }
        - name: creatorId
          in: query
          description: Return collaborations with this creator.
          schema: { type: string }
        - { $ref: "#/components/parameters/IsArchived" }
        - name: include
          in: query
          description: Set to `campaign` to include campaign details.
          schema: { type: string, enum: [campaign] }
        - { $ref: "#/components/parameters/Limit250" }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: A page of collaborations.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PartnerCollaborationPage" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /collaborations/{collaborationId}:
    get:
      tags: [Collaborations]
      summary: Get a collaboration
      description: Returns a collaboration that is accessible to the organization.
      operationId: getPartnerCollaboration
      x-required-scope: read:collaborations
      parameters:
        - { $ref: "#/components/parameters/CollaborationId" }
        - name: include
          in: query
          description: Set to `campaign` to include campaign details.
          schema: { type: string, enum: [campaign] }
      responses:
        "200":
          description: The collaboration.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties: { data: { $ref: "#/components/schemas/PartnerCollaboration" } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/JsonNotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /conversations:
    get:
      tags: [Conversations]
      summary: List conversations
      description: Returns conversations ordered by most recent message activity. Archived and blocked conversations are included unless filtered out.
      operationId: listPartnerConversations
      x-required-scope: read:conversations
      parameters:
        - { $ref: "#/components/parameters/UpdatedAfter" }
        - name: creatorId
          in: query
          description: Return the conversation with this creator, if one exists.
          schema: { type: string }
        - { $ref: "#/components/parameters/IsUnread" }
        - { $ref: "#/components/parameters/IsArchived" }
        - { $ref: "#/components/parameters/IsBlocked" }
        - { $ref: "#/components/parameters/Search" }
        - name: include
          in: query
          description: Set to `creator` to include creator details.
          schema: { type: string, enum: [creator] }
        - { $ref: "#/components/parameters/Limit250" }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: A page of conversations.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PartnerConversationPage" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /conversations/{conversationId}/read:
    post:
      tags: [Conversations]
      summary: Mark a conversation as read
      description: Clears the organization's unread state without changing `lastActivityAt`. The operation is naturally idempotent.
      operationId: markPartnerConversationRead
      x-required-scope: write:conversations
      parameters:
        - { $ref: "#/components/parameters/ConversationId" }
      responses:
        "200": { $ref: "#/components/responses/PartnerConversation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /conversations/{conversationId}/messages:
    get:
      tags: [Conversations]
      summary: List conversation messages
      description: Returns the typed conversation timeline newest first. Reading messages does not mark the conversation as read.
      operationId: listPartnerConversationMessages
      x-required-scope: read:conversations
      parameters:
        - { $ref: "#/components/parameters/ConversationId" }
        - { $ref: "#/components/parameters/CreatedAfter" }
        - { $ref: "#/components/parameters/Limit250" }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: A page of timeline events.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessagePage" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    post:
      tags: [Conversations]
      summary: Send a message
      description: Sends a text message as the member who created the API key and marks the conversation read for the organization.
      operationId: sendPartnerConversationMessage
      x-required-scope: write:conversations
      parameters:
        - { $ref: "#/components/parameters/ConversationId" }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MessageInput" }
            example: { text: "Thanks! Could we move the date to September 15?" }
      responses:
        "201": { $ref: "#/components/responses/SentMessage" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /inquiries:
    post:
      tags: [Conversations]
      summary: Send an inquiry
      description: Sends initial outreach to a creator and creates a conversation and inquiry collaboration when needed.
      operationId: sendPartnerInquiry
      x-required-scope: write:conversations
      parameters:
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [creatorId, text]
              properties:
                creatorId:
                  { type: string, format: uuid, description: Creator receiving the inquiry. }
                text: { type: string, minLength: 1, maxLength: 10000 }
                campaignId:
                  { type: string, format: uuid, description: Optional organization campaign. }
            example:
              {
                creatorId: 418f65a3-8981-4b31-aadf-9b399ab64475,
                text: "We would love to work together on our fall launch.",
              }
      responses:
        "201":
          description: Inquiry sent.
          headers:
            Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [conversationId, messageId, collaborationId]
                    properties:
                      conversationId: { type: string }
                      messageId: { type: string }
                      collaborationId: { type: string }
              example:
                {
                  data:
                    { conversationId: conv_abc, messageId: msg_abc, collaborationId: collab_abc },
                }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /proposals/{proposalId}/accept:
    post:
      tags: [Proposals]
      summary: Accept a creator proposal
      description: Confirms a creator-authored proposal. Accepting an already accepted proposal returns its current state.
      operationId: acceptPartnerProposal
      x-required-scope: write:proposals
      parameters:
        - { $ref: "#/components/parameters/ProposalId" }
      responses:
        "200": { $ref: "#/components/responses/Proposal" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /proposals/{proposalId}/reject:
    post:
      tags: [Proposals]
      summary: Reject a creator proposal
      description: Rejects a creator-authored proposal and cancels the collaboration. Rejection is naturally idempotent.
      operationId: rejectPartnerProposal
      x-required-scope: write:proposals
      parameters:
        - { $ref: "#/components/parameters/ProposalId" }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                comment:
                  {
                    type: string,
                    maxLength: 10000,
                    description: Optional reason shown to the creator.,
                  }
            example: { comment: "The dates no longer work for this launch." }
      responses:
        "200": { $ref: "#/components/responses/Proposal" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Passionfroot API key
      description: Create an API key from Settings → API in your Partner workspace.
  parameters:
    Cursor:
      name: cursor
      in: query
      description: Opaque cursor returned by the previous page.
      schema: { type: string }
    Limit250:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 250, default: 50 }
    Limit100:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    StartDate:
      name: startDate
      in: query
      description: Include placements on or after this calendar date.
      schema: { type: string, format: date }
    EndDate:
      name: endDate
      in: query
      description: Include placements on or before this calendar date.
      schema: { type: string, format: date }
    MetricsUpdatedAfter:
      name: metricsUpdatedAfter
      in: query
      description: Include placements whose reported metrics were updated on or after this date or timestamp.
      schema: { type: string }
    MetricsUpdatedBefore:
      name: metricsUpdatedBefore
      in: query
      description: Include placements whose reported metrics were updated on or before this date or timestamp.
      schema: { type: string }
    Status:
      name: status
      in: query
      description: Comma-separated statuses.
      schema: { type: string }
      example: confirmed,pending
    UpdatedAfter:
      name: updatedAfter
      in: query
      description: Include conversations with message activity on or after this date or timestamp.
      schema: { type: string }
    CreatedAfter:
      name: createdAfter
      in: query
      description: Include messages created on or after this date or timestamp.
      schema: { type: string }
    IsUnread: { name: isUnread, in: query, schema: { type: boolean } }
    IsArchived:
      {
        name: isArchived,
        in: query,
        description: "`true` returns only archived items, `false` only the rest. Omitted, both.",
        schema: { type: boolean },
      }
    IsBlocked: { name: isBlocked, in: query, schema: { type: boolean } }
    Search:
      name: q
      in: query
      description: Case-insensitive search across names and message text.
      schema: { type: string, minLength: 2, maxLength: 200 }
    CreatorId: { name: creatorId, in: path, required: true, schema: { type: string } }
    CollaborationId: { name: collaborationId, in: path, required: true, schema: { type: string } }
    ConversationId: { name: conversationId, in: path, required: true, schema: { type: string } }
    ProposalId: { name: proposalId, in: path, required: true, schema: { type: string } }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Unique key for one logical write. Reuse the key and identical body for retries.
      schema: { type: string, minLength: 1, maxLength: 255 }
  headers:
    IdempotencyReplayed:
      description: Present with the value `true` when this response was replayed from a previous request.
      schema: { type: string, const: "true" }
    RateLimit:
      description: Per-key limit, remaining requests, and seconds until reset.
      schema: { type: string, example: "limit=2, remaining=1, reset=1" }
    RateLimitPolicy:
      description: Per-key rate-limit policy.
      schema: { type: string, example: "2;w=1" }
    RetryAfter:
      description: Seconds to wait before retrying.
      schema: { type: integer, minimum: 1 }
  responses:
    Creator:
      description: The creator.
      content:
        application/json:
          schema:
            type: object
            required: [data]
            properties: { data: { $ref: "#/components/schemas/Creator" } }
    PartnerConversation:
      description: The updated conversation.
      content:
        application/json:
          schema:
            type: object
            required: [data]
            properties: { data: { $ref: "#/components/schemas/PartnerConversation" } }
    SentMessage:
      description: Message sent.
      headers:
        Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" }
      content:
        application/json:
          schema:
            type: object
            required: [data]
            properties: { data: { $ref: "#/components/schemas/SentMessage" } }
          example:
            {
              data:
                { id: msg_abc, conversationId: conv_abc, createdAt: "2026-09-01T09:20:00.000Z" },
            }
    Proposal:
      description: The current proposal state.
      content:
        application/json:
          schema:
            type: object
            required: [data]
            properties: { data: { $ref: "#/components/schemas/Proposal" } }
    BadRequest: { $ref: "#/components/responses/Error400" }
    Unauthorized: { $ref: "#/components/responses/Error401" }
    Forbidden: { $ref: "#/components/responses/Error403" }
    NotFound: { $ref: "#/components/responses/Error404" }
    JsonNotFound:
      {
        description: "Resource not found or not accessible to this organization.",
        content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } },
      }
    Conflict: { $ref: "#/components/responses/Error409" }
    UnprocessableEntity: { $ref: "#/components/responses/Error422" }
    TooManyRequests:
      description: Rate limit exceeded.
      headers: { Retry-After: { $ref: "#/components/headers/RetryAfter" } }
      content:
        {
          application/json:
            {
              schema: { $ref: "#/components/schemas/Error" },
              example:
                {
                  error:
                    {
                      code: RATE_LIMITED,
                      message: "This API key is over its rate limit. Wait for Retry-After before sending more.",
                      details: { limit: 2, windowSeconds: 1 },
                    },
                },
            },
        }
    ServerError: { $ref: "#/components/responses/Error500" }
    ServiceUnavailable:
      description: Key verification is temporarily unavailable. Retry after the indicated delay; the key itself is not at fault.
      headers: { Retry-After: { $ref: "#/components/headers/RetryAfter" } }
      content:
        {
          application/json:
            {
              schema: { $ref: "#/components/schemas/Error" },
              example:
                {
                  error:
                    {
                      code: SERVICE_UNAVAILABLE,
                      message: "API key verification is temporarily unavailable, retry shortly",
                    },
                },
            },
        }
    Error400:
      description: "Invalid parameters, body, ID or cursor. `details` names each invalid field."
      content:
        {
          application/json:
            {
              schema: { $ref: "#/components/schemas/Error" },
              example:
                {
                  error:
                    {
                      code: INVALID_QUERY_PARAMETER,
                      message: "Invalid query parameters",
                      details:
                        [{ field: limit, message: "Number must be less than or equal to 250" }],
                    },
                },
            },
        }
    Error401:
      description: "Missing, invalid, expired, or revoked API key."
      content:
        {
          application/json:
            {
              schema: { $ref: "#/components/schemas/Error" },
              example: { error: { code: UNAUTHENTICATED, message: "Invalid or expired API key" } },
            },
        }
    Error403:
      description: "The key lacks the required scope (`MISSING_SCOPE`), or the organization's plan does not include API access (`PLAN_RESTRICTED`)."
      content:
        {
          application/json:
            {
              schema: { $ref: "#/components/schemas/Error" },
              example:
                {
                  error:
                    {
                      code: MISSING_SCOPE,
                      message: "API key missing required scope: write:conversations",
                    },
                },
            },
        }
    Error404:
      description: "Resource not found or not accessible to this organization."
      content:
        {
          application/json:
            {
              schema: { $ref: "#/components/schemas/Error" },
              example: { error: { code: NOT_FOUND, message: "Conversation not found" } },
            },
        }
    Error409:
      {
        description: "The operation conflicts with current state or an identical write is in progress.",
        content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } },
      }
    Error422:
      {
        description: "The request is well formed but cannot be processed.",
        content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } },
      }
    Error500:
      description: "Unexpected server failure. Re-read state before retrying a write, and quote the `X-PF-Trace-Id` response header to support."
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  schemas:
    Error:
      type: object
      additionalProperties: false
      required: [error]
      description: The body of every error response. Branch on `code`; messages can be reworded.
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable, machine-readable reason. New codes may be added; existing ones never change meaning.
              enum:
                [
                  INVALID_REQUEST,
                  INVALID_QUERY_PARAMETER,
                  INVALID_BODY,
                  INVALID_ID,
                  INVALID_CURSOR,
                  INVALID_IDEMPOTENCY_KEY,
                  UNAUTHENTICATED,
                  MISSING_SCOPE,
                  PLAN_RESTRICTED,
                  NOT_FOUND,
                  METHOD_NOT_ALLOWED,
                  CONFLICT,
                  IDEMPOTENCY_IN_PROGRESS,
                  UNPROCESSABLE,
                  IDEMPOTENCY_KEY_REUSED,
                  RATE_LIMITED,
                  INTERNAL_ERROR,
                  SERVICE_UNAVAILABLE,
                ]
            message:
              type: string
              description: What went wrong and what to do next, written for a developer or agent.
            details:
              description: Present only when there is something structured to act on, such as one entry per invalid field (`{ field, message }`).
    Pagination:
      type: object
      required: [hasMore]
      properties:
        nextCursor: { type: [string, "null"] }
        hasMore: { type: boolean }
    Label:
      type: object
      required: [id, name]
      properties:
        id: { type: string }
        name: { type: string }
        color: { type: string }
        position: { type: string }
    Metrics:
      type: object
      properties:
        reach: { type: [integer, "null"] }
        views: { type: [integer, "null"] }
        engagement: { type: [integer, "null"] }
        engagementRate: { type: [number, "null"] }
        metricsUpdatedAt: { type: string, format: date-time }
    Channel:
      type: object
      required: [id, title, platformType]
      properties:
        id: { type: string }
        title: { type: string }
        url: { type: [string, "null"], format: uri }
        platformType: { type: string }
        metrics: { anyOf: [{ $ref: "#/components/schemas/Metrics" }, { type: "null" }] }
    Creator:
      type: object
      required: [id, displayName, country, note, labels]
      properties:
        id: { type: string }
        displayName: { type: string }
        country: { type: [string, "null"] }
        note: { type: [string, "null"], description: Plain-text private workspace note. }
        labels: { type: array, items: { $ref: "#/components/schemas/Label" } }
        channels: { type: array, items: { $ref: "#/components/schemas/Channel" } }
    Campaign:
      type: object
      required: [id, name]
      properties:
        id: { type: [string, "null"] }
        name: { type: string }
        description: { type: [string, "null"] }
        budgetAmountCents: { type: [integer, "null"] }
        budgetCurrency: { type: [string, "null"] }
    PartnerCollaboration:
      type: object
      required:
        [
          id,
          name,
          creatorId,
          campaignId,
          status,
          priceCents,
          usdPriceCents,
          currency,
          createdAt,
          statusUpdatedAt,
          archivedAt,
          isExternal,
        ]
      properties:
        id: { type: string }
        name: { type: string }
        creatorId: { type: string }
        campaignId: { type: [string, "null"] }
        status: { type: string, enum: [pending, confirmed, cancelled] }
        priceCents: { type: [integer, "null"] }
        usdPriceCents: { type: [integer, "null"] }
        currency: { type: string }
        createdAt: { type: string, format: date-time }
        statusUpdatedAt: { type: string, format: date-time }
        archivedAt:
          type: [string, "null"]
          format: date-time
          description: When your team archived the collaboration in Passionfroot, or null if it isn't archived.
        isExternal:
          type: boolean
          description: "Whether the collaboration is outside Passionfroot: its creator hasn't joined Passionfroot yet, or it's a post your team imported to track."
        campaign: { anyOf: [{ $ref: "#/components/schemas/Campaign" }, { type: "null" }] }
    PartnerPlacement:
      type: object
      required:
        [
          id,
          creatorId,
          creatorName,
          collaborationId,
          date,
          name,
          status,
          platform,
          postType,
          url,
          viewCount,
          likeCount,
          commentCount,
          shareCount,
          metricsLastUpdatedAt,
          priceCents,
          usdPriceCents,
          currency,
        ]
      properties:
        id: { type: string }
        creatorId: { type: string }
        creatorName: { type: string, deprecated: true }
        collaborationId: { type: [string, "null"] }
        date: { type: string, format: date-time }
        name: { type: string }
        status: { type: string, enum: [pending, confirmed, cancelled] }
        platform: { type: [string, "null"] }
        postType: { type: [string, "null"] }
        url: { type: [string, "null"], format: uri }
        viewCount: { type: [integer, "null"] }
        likeCount: { type: [integer, "null"] }
        commentCount: { type: [integer, "null"] }
        shareCount: { type: [integer, "null"] }
        metricsLastUpdatedAt: { type: [string, "null"], format: date-time }
        priceCents: { type: [integer, "null"] }
        usdPriceCents: { type: [integer, "null"] }
        currency: { type: [string, "null"] }
        creator: { $ref: "#/components/schemas/Creator" }
        collaboration:
          { anyOf: [{ $ref: "#/components/schemas/PartnerCollaboration" }, { type: "null" }] }
    PartnerConversation:
      type: object
      required: [id, creatorId, isUnread, isArchived, isBlocked, lastActivityAt, createdAt]
      properties:
        id: { type: string }
        creatorId: { type: string }
        isUnread: { type: boolean }
        isArchived: { type: boolean }
        isBlocked: { type: boolean }
        lastActivityAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
        creator: { $ref: "#/components/schemas/Creator" }
    Attachment:
      type: object
      required: [fileName, fileSize, mimeType, downloadUrl]
      properties:
        fileName: { type: string }
        fileSize: { type: integer }
        mimeType: { type: string }
        downloadUrl:
          {
            type: [string, "null"],
            format: uri,
            description: Signed URL that expires after one hour,
            or `null` if signing fails.,
          }
    ProposalPlacement:
      type: object
      required: [date, postType, platform]
      properties:
        date: { type: string, format: date-time }
        postType: { type: [string, "null"] }
        platform: { type: [string, "null"] }
    Proposal:
      type: object
      required:
        [
          proposalId,
          collaborationId,
          requestId,
          status,
          createdBy,
          priceCents,
          currency,
          packageName,
          placements,
        ]
      properties:
        proposalId: { type: [string, "null"] }
        collaborationId: { type: string }
        requestId: { type: string }
        status: { type: string, enum: [offer_pending, pending, accepted, rejected, cancelled] }
        createdBy: { type: string, enum: [creator, brand] }
        priceCents: { type: [integer, "null"] }
        currency: { type: [string, "null"] }
        packageName: { type: string }
        comment: { type: string }
        placements: { type: array, items: { $ref: "#/components/schemas/ProposalPlacement" } }
    Message:
      type: object
      required: [id, type, actor, collaborationId, createdAt]
      properties:
        id: { type: string }
        type: { type: string, description: Public timeline event type. }
        actor: { type: string, enum: [creator, brand, system] }
        collaborationId: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        text: { type: string, description: May contain basic HTML. }
        comment: { type: string }
        sender:
          type: [object, "null"]
          properties:
            { id: { type: string }, name: { type: string }, email: { type: string, format: email } }
        attachments: { type: array, items: { $ref: "#/components/schemas/Attachment" } }
        proposal: { anyOf: [{ $ref: "#/components/schemas/Proposal" }, { type: "null" }] }
        stats:
          type: object
          properties:
            viewCount: { type: [integer, "null"] }
            likeCount: { type: [integer, "null"] }
            commentCount: { type: [integer, "null"] }
            shareCount: { type: [integer, "null"] }
            contentUrl: { type: [string, "null"] }
            contentTitle: { type: [string, "null"] }
            platform: { type: [string, "null"] }
    MessageInput:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        {
          text:
            {
              type: string,
              minLength: 1,
              maxLength: 10000,
              description: Plain text or allowed basic HTML.,
            },
        }
    SentMessage:
      type: object
      required: [id, conversationId, createdAt]
      properties:
        id: { type: string }
        conversationId: { type: string }
        createdAt: { type: string, format: date-time }
    PartnerPlacementPage:
      type: object
      required: [data, pagination]
      properties:
        {
          data: { type: array, items: { $ref: "#/components/schemas/PartnerPlacement" } },
          pagination: { $ref: "#/components/schemas/Pagination" },
        }
    CreatorPage:
      type: object
      required: [data, pagination]
      properties:
        {
          data: { type: array, items: { $ref: "#/components/schemas/Creator" } },
          pagination: { $ref: "#/components/schemas/Pagination" },
        }
    PartnerCollaborationPage:
      type: object
      required: [data, pagination]
      properties:
        {
          data: { type: array, items: { $ref: "#/components/schemas/PartnerCollaboration" } },
          pagination: { $ref: "#/components/schemas/Pagination" },
        }
    PartnerConversationPage:
      type: object
      required: [data, pagination]
      properties:
        {
          data: { type: array, items: { $ref: "#/components/schemas/PartnerConversation" } },
          pagination: { $ref: "#/components/schemas/Pagination" },
        }
    MessagePage:
      type: object
      required: [data, pagination]
      properties:
        {
          data: { type: array, items: { $ref: "#/components/schemas/Message" } },
          pagination: { $ref: "#/components/schemas/Pagination" },
        }
