openapi: 3.0.3
info:
  title: MediaOS API
  description: >
    The MediaOS REST API gives you programmatic access to your MediaOS account
    data — advertisers, contracts, invoices, payments, and more. Build
    integrations, automate workflows, and sync data across your tools.
  version: "1.0"
servers:
  - url: https://api.mediaos.com

security:
  - basicAuth: []

components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic
      description: Use your API key as the username and API password as the password.

  parameters:
    idPath:
      name: id
      in: path
      required: true
      schema:
        type: integer
    perPage:
      name: per_page
      in: query
      schema:
        type: integer
        default: 25
        maximum: 100
      description: Results per page.
    page:
      name: page
      in: query
      schema:
        type: integer
        default: 1
      description: Page number.
    sortBy:
      name: sort_by
      in: query
      schema:
        type: string
      description: Field to sort by.
    sortOrder:
      name: sort_order
      in: query
      schema:
        type: string
        enum: [asc, desc]
      description: Sort direction.

  schemas:
    Account:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        domain:
          type: string
        plan:
          type: string
        timezone:
          type: string

    Advertiser:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        is_lead:
          type: boolean
        active:
          type: boolean
        owned_by_user_id:
          type: integer
        date_created:
          type: string
          format: date-time

    Agency:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        active:
          type: boolean
        date_created:
          type: string
          format: date-time

    Contact:
      type: object
      properties:
        id:
          type: integer
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        advertiser_id:
          type: integer
        agency_id:
          type: integer
        active:
          type: boolean
        date_created:
          type: string
          format: date-time
        primary_email:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/ContactData"
          description: The contact's primary email (full ContactData object) or null if unset.
        primary_phone:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/ContactData"
          description: The contact's primary phone (full ContactData object) or null if unset.
        primary_address:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/Address"
          description: The contact's primary address (full Address object) or null if unset.

    ContactDataType:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string

    ContactData:
      type: object
      properties:
        id:
          type: integer
        contact_id:
          type: integer
        contact_data_type_id:
          type: integer
        value:
          type: string

    Address:
      type: object
      properties:
        id:
          type: integer
        advertiser_id:
          type: integer
        address1:
          type: string
        address2:
          type: string
        city:
          type: string
        state:
          type: string
        zip:
          type: string
        country:
          type: string

    User:
      type: object
      properties:
        id:
          type: integer
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        active:
          type: boolean

    Product:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        active:
          type: boolean

    AdSize:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        width:
          type: integer
        height:
          type: integer

    AdRate:
      type: object
      properties:
        id:
          type: integer
        product_id:
          type: integer
        ad_size_id:
          type: integer
        rate:
          type: number
          format: float

    Issue:
      type: object
      properties:
        id:
          type: integer
        product_id:
          type: integer
        name:
          type: string
        issue_date:
          type: string
          format: date
        close_date:
          type: string
          format: date

    Location:
      type: object
      properties:
        id:
          type: integer
        product_id:
          type: integer
        name:
          type: string

    Section:
      type: object
      properties:
        id:
          type: integer
        product_id:
          type: integer
        name:
          type: string

    Contract:
      type: object
      properties:
        id:
          type: integer
        advertiser_id:
          type: integer
        agency_id:
          type: integer
        owned_by_user_id:
          type: integer
        status:
          type: string
        total:
          type: number
          format: float
        date_created:
          type: string
          format: date-time

    ContractLineItem:
      type: object
      properties:
        id:
          type: integer
        contract_id:
          type: integer
        line_item_type_id:
          type: integer
        description:
          type: string
        amount:
          type: number
          format: float

    ContractLineItemType:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string

    Insertion:
      type: object
      properties:
        id:
          type: integer
        contract_id:
          type: integer
        issue_id:
          type: integer
        ad_size_id:
          type: integer
        location_id:
          type: integer
        section_id:
          type: integer
        date_created:
          type: string
          format: date-time

    Invoice:
      type: object
      properties:
        id:
          type: integer
        contract_id:
          type: integer
        advertiser_id:
          type: integer
        total:
          type: number
          format: float
        balance:
          type: number
          format: float
        status:
          type: string
        date_created:
          type: string
          format: date-time

    InvoiceLineItem:
      type: object
      properties:
        id:
          type: integer
        invoice_id:
          type: integer
        description:
          type: string
        amount:
          type: number
          format: float

    InvoiceLineItemType:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string

    Payment:
      type: object
      properties:
        id:
          type: integer
        invoice_id:
          type: integer
        advertiser_id:
          type: integer
        amount:
          type: number
          format: float
        payment_date:
          type: string
          format: date
        date_created:
          type: string
          format: date-time

    AdRequest:
      type: object
      properties:
        id:
          type: integer
        advertiser_id:
          type: integer
        status:
          type: string
        date_created:
          type: string
          format: date-time

    Ad:
      type: object
      properties:
        id:
          type: integer
        advertiser_id:
          type: integer
        ad_size_id:
          type: integer
        status:
          type: string
        file_url:
          type: string

    Membership:
      type: object
      properties:
        id:
          type: integer
        contact_id:
          type: integer
        email:
          type: string
        active:
          type: boolean
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date

    MembershipType:
      type: object
      properties:
        membershipTypeID:
          type: integer
        membershipTypeName:
          type: string
        membershipTypeMethod:
          type: string
          enum: [Issue, Time]
        membershipTierTypeID:
          type: integer
        membershipTierTypeName:
          type: string
          nullable: true
        emailAdFree:
          type: integer
        webAdFree:
          type: integer
        publicationID:
          type: integer
          nullable: true
        productName:
          type: string
          nullable: true
        isIssueBased:
          type: boolean
        isTimeBased:
          type: boolean
        websiteID:
          type: integer
          nullable: true
        membershipTypeRenewAt:
          type: integer
          nullable: true

    MembershipLevel:
      type: object
      properties:
        membershipTierLevelID:
          type: integer
        membershipTierTypeID:
          type: integer
        tierName:
          type: string
        tierLevel:
          type: integer

    MembershipLevelGroup:
      type: object
      description: Membership levels grouped by tier type.
      properties:
        membershipTierTypeID:
          type: integer
        name:
          type: string
        tierLevels:
          type: array
          items:
            $ref: "#/components/schemas/MembershipLevel"

    Audience:
      type: object
      description: A member audience filter — a saved segment of contacts that match a set of criteria.
      properties:
        id:
          type: integer
        parent_id:
          type: integer
          nullable: true
        name:
          type: string
        description:
          type: string
          nullable: true
        type:
          type: string
        status:
          type: string
        process_status:
          type: string
          description: Processing status of the last audience run (e.g. Processing, Complete).
        match_count:
          type: integer
          nullable: true
          description: Number of contacts currently matching this audience.
        saved_by_user_id:
          type: integer
          nullable: true
        date_created:
          type: string
          format: date-time
          nullable: true
        date_deleted:
          type: string
          format: date-time
          nullable: true
        last_processed:
          type: string
          format: date-time
          nullable: true

    TouchPoint:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
          enum: [Appointment, Email, Follow-up, Phone, SMS]
        advertiser_id:
          type: integer
          nullable: true
        agency_id:
          type: integer
          nullable: true
        contact_id:
          type: integer
          nullable: true
        sent_by_user_id:
          type: integer
          nullable: true
        subject:
          type: string
        body:
          type: string
        date:
          type: string
          format: date-time
        call_result:
          type: string
          nullable: true
        call_direction:
          type: string
          nullable: true
        call_duration:
          type: integer
          nullable: true
          description: Call duration in seconds
        appointment_duration:
          type: integer
          nullable: true
          description: Appointment duration in minutes

    Post:
      type: object
      description: A post board item — a task or a note attached to an object.
      properties:
        id:
          type: integer
        type:
          type: string
          enum: [task, note]
        subject:
          type: string
          nullable: true
        message:
          type: string
        object_type:
          type: string
          description: The type of object this post is attached to (e.g. advertisers, agencies, contacts, contracts, invoices).
        object_id:
          type: integer
          nullable: true
        created_by_user_id:
          type: integer
          nullable: true
        assigned_to_user_id:
          type: integer
          nullable: true
        due_date:
          type: string
          format: date
          nullable: true
        start_date:
          type: string
          format: date
          nullable: true
        status:
          type: string
          nullable: true
        percent_complete:
          type: integer
          nullable: true
        created_date:
          type: string
          format: date-time

    Error:
      type: object
      properties:
        error:
          type: string

paths:

  # ── Account ────────────────────────────────────────────────────
  /account/:
    get:
      operationId: getAccount
      summary: Get account details
      tags: [Account]
      responses:
        "200":
          description: Account details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Account"

  # ── Advertisers ────────────────────────────────────────────────
  /advertisers/{id}:
    get:
      operationId: getAdvertiser
      summary: Get a single advertiser
      tags: [Advertisers]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Advertiser record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Advertiser"
    post:
      operationId: updateAdvertiser
      summary: Update an advertiser
      tags: [Advertisers]
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Advertiser"
      responses:
        "200":
          description: Updated advertiser
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Advertiser"
    delete:
      operationId: deleteAdvertiser
      summary: Delete an advertiser
      tags: [Advertisers]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "204":
          description: Advertiser deleted

  /advertisers/:
    get:
      operationId: listAdvertisers
      summary: List / search advertisers
      tags: [Advertisers]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - name: name
          in: query
          schema:
            type: string
          description: Filter by name (partial match)
        - name: is_lead
          in: query
          schema:
            type: boolean
        - name: active
          in: query
          schema:
            type: boolean
        - name: owned_by_user_id
          in: query
          schema:
            type: integer
        - name: created_by_user_id
          in: query
          schema:
            type: integer
        - name: date_created_before
          in: query
          schema:
            type: string
            format: date-time
        - name: date_created_after
          in: query
          schema:
            type: string
            format: date-time
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of advertisers
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Advertiser"
    put:
      operationId: createAdvertiser
      summary: Create an advertiser
      tags: [Advertisers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                created_by_user_id:
                  type: integer
                owned_by_user_id:
                  type: integer
                is_lead:
                  type: boolean
                sales_lock:
                  type: boolean
                description:
                  type: string
                general_ledger_account_code:
                  type: string
      responses:
        "200":
          description: Created advertiser
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Advertiser"

  # ── Agencies ───────────────────────────────────────────────────
  /agencies/{id}:
    get:
      operationId: getAgency
      summary: Get a single agency
      tags: [Agencies]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Agency record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agency"
    post:
      operationId: updateAgency
      summary: Update an agency
      tags: [Agencies]
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Agency"
      responses:
        "200":
          description: Updated agency
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agency"

  /agencies/:
    get:
      operationId: listAgencies
      summary: List / search agencies
      tags: [Agencies]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - name: name
          in: query
          schema:
            type: string
        - name: active
          in: query
          schema:
            type: boolean
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of agencies
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Agency"
    put:
      operationId: createAgency
      summary: Create an agency
      tags: [Agencies]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                active:
                  type: boolean
      responses:
        "200":
          description: Created agency
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agency"

  # ── Contacts ───────────────────────────────────────────────────
  /contacts/{id}:
    get:
      operationId: getContact
      summary: Get a single contact
      tags: [Contacts]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Contact record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"
    post:
      operationId: updateContact
      summary: Update a contact
      tags: [Contacts]
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Contact"
      responses:
        "200":
          description: Updated contact
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"

  /contacts/:
    get:
      operationId: listContacts
      summary: Search contacts
      tags: [Contacts]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - name: first_name
          in: query
          schema:
            type: string
        - name: last_name
          in: query
          schema:
            type: string
        - name: email
          in: query
          schema:
            type: string
        - name: advertiser_id
          in: query
          schema:
            type: integer
        - name: agency_id
          in: query
          schema:
            type: integer
        - name: active
          in: query
          schema:
            type: boolean
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of contacts
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Contact"
    put:
      operationId: createContact
      summary: Create a contact
      tags: [Contacts]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [first_name, last_name]
              properties:
                first_name:
                  type: string
                last_name:
                  type: string
                email:
                  type: string
                advertiser_id:
                  type: integer
                agency_id:
                  type: integer
                active:
                  type: boolean
      responses:
        "200":
          description: Created contact
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contact"

  # ── Contact Data Types ─────────────────────────────────────────
  /contactDataTypes/:
    get:
      operationId: listContactDataTypes
      summary: List all contact data types
      tags: [Contact Data]
      responses:
        "200":
          description: List of contact data types
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ContactDataType"

  # ── Contact Data ───────────────────────────────────────────────
  /contactData/{id}:
    get:
      operationId: getContactData
      summary: Get a single contact data item
      tags: [Contact Data]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Contact data item
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactData"
    post:
      operationId: updateContactData
      summary: Update a contact data item
      tags: [Contact Data]
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContactData"
      responses:
        "200":
          description: Updated contact data item
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactData"

  /contactData/:
    get:
      operationId: listContactData
      summary: Search contact data
      tags: [Contact Data]
      parameters:
        - name: contact_id
          in: query
          schema:
            type: integer
        - name: contact_data_type_id
          in: query
          schema:
            type: integer
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of contact data items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ContactData"
    put:
      operationId: createContactData
      summary: Create a contact data item
      tags: [Contact Data]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contact_id, contact_data_type_id, value]
              properties:
                contact_id:
                  type: integer
                contact_data_type_id:
                  type: integer
                value:
                  type: string
      responses:
        "200":
          description: Created contact data item
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactData"

  # ── Addresses ─────────────────────────────────────────────────
  /addresses/{id}:
    get:
      operationId: getAddress
      summary: Get a single address
      tags: [Addresses]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Address record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Address"
    post:
      operationId: updateAddress
      summary: Update an address
      tags: [Addresses]
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Address"
      responses:
        "200":
          description: Updated address
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Address"

  /addresses/:
    get:
      operationId: listAddresses
      summary: Search addresses
      tags: [Addresses]
      parameters:
        - name: advertiser_id
          in: query
          schema:
            type: integer
        - name: city
          in: query
          schema:
            type: string
        - name: state
          in: query
          schema:
            type: string
        - name: zip
          in: query
          schema:
            type: string
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of addresses
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Address"
    put:
      operationId: createAddress
      summary: Create an address
      tags: [Addresses]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Address"
      responses:
        "200":
          description: Created address
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Address"

  # ── Users ──────────────────────────────────────────────────────
  /users/{id}:
    get:
      operationId: getUser
      summary: Get a single user
      tags: [Users]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: User record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

  /users/:
    get:
      operationId: listUsers
      summary: Search users
      tags: [Users]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - name: email
          in: query
          schema:
            type: string
        - name: active
          in: query
          schema:
            type: boolean
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"

  # ── Products ───────────────────────────────────────────────────
  /products/{id}:
    get:
      operationId: getProduct
      summary: Get a single product
      tags: [Products & Inventory]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Product record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"

  /products/:
    get:
      operationId: listProducts
      summary: List all products
      tags: [Products & Inventory]
      responses:
        "200":
          description: List of products
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Product"

  # ── Ad Sizes ───────────────────────────────────────────────────
  /adSizes/{id}:
    get:
      operationId: getAdSize
      summary: Get a single ad size
      tags: [Products & Inventory]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Ad size record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdSize"

  /adSizes/:
    get:
      operationId: listAdSizes
      summary: Search ad sizes
      tags: [Products & Inventory]
      parameters:
        - name: product_id
          in: query
          schema:
            type: integer
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of ad sizes
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AdSize"

  # ── Ad Rates ───────────────────────────────────────────────────
  /adRates/{id}:
    get:
      operationId: getAdRate
      summary: Get a single ad rate
      tags: [Products & Inventory]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Ad rate record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdRate"

  /adRates/:
    get:
      operationId: listAdRates
      summary: Search ad rates
      tags: [Products & Inventory]
      parameters:
        - name: product_id
          in: query
          schema:
            type: integer
        - name: ad_size_id
          in: query
          schema:
            type: integer
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of ad rates
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AdRate"

  # ── Issues ─────────────────────────────────────────────────────
  /issues/{id}:
    get:
      operationId: getIssue
      summary: Get a single issue
      tags: [Products & Inventory]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Issue record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Issue"

  /issues/:
    get:
      operationId: listIssues
      summary: List issues for a product
      tags: [Products & Inventory]
      parameters:
        - name: product_id
          in: query
          schema:
            type: integer
        - name: issue_date_before
          in: query
          schema:
            type: string
            format: date
        - name: issue_date_after
          in: query
          schema:
            type: string
            format: date
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of issues
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Issue"

  # ── Locations ──────────────────────────────────────────────────
  /locations/{id}:
    get:
      operationId: getLocation
      summary: Get a single location
      tags: [Products & Inventory]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Location record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Location"

  /locations/:
    get:
      operationId: listLocations
      summary: List locations for a product
      tags: [Products & Inventory]
      parameters:
        - name: product_id
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: List of locations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Location"

  # ── Sections ───────────────────────────────────────────────────
  /sections/{id}:
    get:
      operationId: getSection
      summary: Get a single section
      tags: [Products & Inventory]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Section record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Section"

  /sections/:
    get:
      operationId: listSections
      summary: List sections for a product
      tags: [Products & Inventory]
      parameters:
        - name: product_id
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: List of sections
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Section"

  # ── Contracts ─────────────────────────────────────────────────
  /contracts/{id}:
    get:
      operationId: getContract
      summary: Get a single contract
      tags: [Orders & Revenue]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Contract record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contract"
        "404":
          description: Contract not found
    post:
      operationId: updateContract
      summary: Update a contract
      tags: [Orders & Revenue]
      description: >
        Updates an existing contract. Won contracts cannot be modified via this
        endpoint — use the action endpoints (setToLost, setToOpen) instead.
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [advertiser_id]
              properties:
                advertiser_id:
                  type: integer
                agency_id:
                  type: integer
                sold_by_user_id:
                  type: integer
                notes:
                  type: string
                insertion_order_number:
                  type: string
                down_payment_amount:
                  type: number
                  format: float
                term_id:
                  type: integer
                billing_email_contact_data_id:
                  type: integer
                production_email_contact_data_id:
                  type: integer
                sold_to_email_contact_data_id:
                  type: integer
                sold_to_address_id:
                  type: integer
                bill_to_address_id:
                  type: integer
                tax_rate_id:
                  type: integer
                percent_discount:
                  type: number
                  format: float
                business_unit_id:
                  type: integer
                hide_invoice_amount_details:
                  type: boolean
      responses:
        "200":
          description: Updated contract
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contract"
        "400":
          description: Validation error or Won contract modification attempt
        "404":
          description: Contract not found

  /contracts/{id}/setToWon:
    post:
      operationId: setContractToWon
      summary: Mark a contract as Won
      tags: [Orders & Revenue]
      description: >
        Marks the contract as Won (sold). Enforces all canSetContractToWon()
        checks including sales lock, pending forecast items, DocuSign, and
        down payment requirements.
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                sold_date:
                  type: string
                  format: date
                  description: Date the contract was sold (defaults to today if omitted)
      responses:
        "200":
          description: Contract updated to Won status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contract"
        "400":
          description: Business rule violation
        "404":
          description: Contract not found

  /contracts/{id}/setToLost:
    post:
      operationId: setContractToLost
      summary: Mark a contract as Lost
      tags: [Orders & Revenue]
      description: >
        Marks the contract as Lost. Cannot be applied to Won (sold) contracts
        or forecast contracts.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Contract updated to Lost status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contract"
        "400":
          description: Business rule violation
        "404":
          description: Contract not found

  /contracts/{id}/setToOpen:
    post:
      operationId: setContractToOpen
      summary: Move a Lost contract back to Open
      tags: [Orders & Revenue]
      description: >
        Moves a Lost contract back to Open (Proposal) status. Cannot be applied
        to Won (sold) contracts — only Lost→Open transitions are permitted.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Contract moved back to Open status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contract"
        "400":
          description: Business rule violation
        "404":
          description: Contract not found

  /contracts/{id}/approveDiscount:
    post:
      operationId: approveDiscountContract
      summary: Approve a discount that exceeds the allowed threshold
      tags: [Orders & Revenue]
      description: >
        Approves a discount on a contract when discountNeedsApproval() returns
        true. The calling user must be an admin or sales manager.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Discount approved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contract"
        "400":
          description: Discount does not need approval or insufficient permissions
        "404":
          description: Contract not found

  /contracts/:
    get:
      operationId: listContracts
      summary: Search contracts
      tags: [Orders & Revenue]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - name: advertiser_id
          in: query
          schema:
            type: integer
        - name: status
          in: query
          schema:
            type: string
          description: Filter by contract status (e.g. open, won, lost)
        - name: is_proposal
          in: query
          schema:
            type: boolean
          description: Filter proposals vs. signed contracts
        - name: date_created_before
          in: query
          schema:
            type: string
            format: date-time
        - name: date_created_after
          in: query
          schema:
            type: string
            format: date-time
        - name: date_sold_before
          in: query
          schema:
            type: string
            format: date-time
        - name: date_sold_after
          in: query
          schema:
            type: string
            format: date-time
        - name: amount_from
          in: query
          schema:
            type: number
            format: float
          description: Minimum contract total
        - name: amount_to
          in: query
          schema:
            type: number
            format: float
          description: Maximum contract total
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of contracts
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Contract"
    put:
      operationId: createContract
      summary: Create a contract
      tags: [Orders & Revenue]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [advertiser_id]
              properties:
                advertiser_id:
                  type: integer
                agency_id:
                  type: integer
                sold_by_user_id:
                  type: integer
                notes:
                  type: string
                insertion_order_number:
                  type: string
                down_payment_amount:
                  type: number
                  format: float
                term_id:
                  type: integer
                billing_email_contact_data_id:
                  type: integer
                production_email_contact_data_id:
                  type: integer
                sold_to_email_contact_data_id:
                  type: integer
                sold_to_address_id:
                  type: integer
                bill_to_address_id:
                  type: integer
                tax_rate_id:
                  type: integer
                percent_discount:
                  type: number
                  format: float
                business_unit_id:
                  type: integer
                hide_invoice_amount_details:
                  type: boolean
      responses:
        "200":
          description: Created contract — returns the new contract ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        "400":
          description: Validation error

  # ── Contract Line Items ────────────────────────────────────────
  /contractLineItems/{id}:
    get:
      operationId: getContractLineItem
      summary: Get a single line item
      tags: [Orders & Revenue]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Contract line item record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContractLineItem"
        "404":
          description: Line item not found
    post:
      operationId: updateContractLineItem
      summary: Update a contract line item
      tags: [Orders & Revenue]
      description: >
        Updates an existing contract line item. Cannot modify line items on a
        Won contract.
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contract_id, contract_line_item_type_id, amount]
              properties:
                contract_id:
                  type: integer
                contract_line_item_type_id:
                  type: integer
                amount:
                  type: number
                  format: float
                notes:
                  type: string
                taxable:
                  type: boolean
                apply_to_insertion_id:
                  type: integer
      responses:
        "200":
          description: Updated line item
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContractLineItem"
        "400":
          description: Validation error or Won contract protection
        "404":
          description: Line item not found
    delete:
      operationId: deleteContractLineItem
      summary: Delete a contract line item
      tags: [Orders & Revenue]
      description: >
        Deletes a contract line item. Cannot delete from a Won contract.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "204":
          description: Line item deleted
        "400":
          description: Cannot delete from a Won contract
        "404":
          description: Line item not found

  /contractLineItems/:
    get:
      operationId: listContractLineItems
      summary: Get line items for a contract
      tags: [Orders & Revenue]
      parameters:
        - name: contract_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: List of contract line items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ContractLineItem"
    put:
      operationId: createContractLineItem
      summary: Create a contract line item
      tags: [Orders & Revenue]
      description: >
        Creates a new line item on a contract. Cannot add to a Won contract.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contract_id, contract_line_item_type_id, amount]
              properties:
                contract_id:
                  type: integer
                contract_line_item_type_id:
                  type: integer
                amount:
                  type: number
                  format: float
                notes:
                  type: string
                taxable:
                  type: boolean
                apply_to_insertion_id:
                  type: integer
      responses:
        "200":
          description: Created line item — returns the new line item ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        "400":
          description: Validation error or Won contract protection

  /contractLineItemTypes/{id}:
    get:
      operationId: getContractLineItemType
      summary: Get a single line item type
      tags: [Orders & Revenue]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Line item type record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContractLineItemType"

  /contractLineItemTypes/:
    get:
      operationId: listContractLineItemTypes
      summary: List all line item types
      tags: [Orders & Revenue]
      responses:
        "200":
          description: List of line item types
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ContractLineItemType"

  # ── Insertions ────────────────────────────────────────────────
  /insertions/{id}:
    get:
      operationId: getInsertion
      summary: Get a single insertion
      tags: [Orders & Revenue]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Insertion record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Insertion"
        "404":
          description: Insertion not found
    post:
      operationId: updateInsertion
      summary: Update an insertion
      tags: [Orders & Revenue]
      description: Updates an existing insertion. The parent contract must not be Won.
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contract_id, product_id]
              properties:
                contract_id:
                  type: integer
                product_id:
                  type: integer
                ad_size_id:
                  type: integer
                ad_rate_id:
                  type: integer
                issue_id:
                  type: integer
                section_id:
                  type: integer
                location_id:
                  type: integer
                ad_id:
                  type: integer
                quantity:
                  type: integer
                discount:
                  type: number
                  format: float
                placement:
                  type: string
                description:
                  type: string
                deliver_date:
                  type: string
                  format: date
                taxable:
                  type: boolean
      responses:
        "200":
          description: Updated insertion
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Insertion"
        "400":
          description: Validation error or Won contract protection
        "404":
          description: Insertion not found
    delete:
      operationId: deleteInsertion
      summary: Delete an insertion
      tags: [Orders & Revenue]
      description: Deletes an insertion. Cannot delete if the insertion has already been invoiced.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "204":
          description: Insertion deleted
        "400":
          description: Cannot delete an invoiced insertion
        "404":
          description: Insertion not found

  /insertions/:
    get:
      operationId: listInsertions
      summary: Search insertions
      tags: [Orders & Revenue]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - name: contract_id
          in: query
          schema:
            type: integer
        - name: product_id
          in: query
          schema:
            type: integer
        - name: issue_id
          in: query
          schema:
            type: integer
        - name: advertiser_id
          in: query
          schema:
            type: integer
        - name: section_id
          in: query
          schema:
            type: integer
        - name: location_id
          in: query
          schema:
            type: integer
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of insertions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Insertion"
    put:
      operationId: createInsertion
      summary: Create an insertion
      tags: [Orders & Revenue]
      description: Creates a new insertion on a contract. The parent contract must not be Won.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contract_id, product_id]
              properties:
                contract_id:
                  type: integer
                product_id:
                  type: integer
                ad_size_id:
                  type: integer
                ad_rate_id:
                  type: integer
                issue_id:
                  type: integer
                section_id:
                  type: integer
                location_id:
                  type: integer
                ad_id:
                  type: integer
                quantity:
                  type: integer
                discount:
                  type: number
                  format: float
                placement:
                  type: string
                description:
                  type: string
                deliver_date:
                  type: string
                  format: date
                taxable:
                  type: boolean
      responses:
        "200":
          description: Created insertion — returns the new insertion ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        "400":
          description: Validation error or Won contract protection

  # ── Invoices ──────────────────────────────────────────────────
  /invoices/{id}:
    get:
      operationId: getInvoice
      summary: Get a single invoice
      tags: [Orders & Revenue]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Invoice record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Invoice"
        "404":
          description: Invoice not found
    delete:
      operationId: deleteInvoice
      summary: Delete an invoice
      tags: [Orders & Revenue]
      description: >
        Deletes an invoice. Cannot delete if the invoice has any applied
        payments — unapply all payments first.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "204":
          description: Invoice deleted
        "400":
          description: Invoice has applied payments and cannot be deleted
        "404":
          description: Invoice not found

  /invoices/{id}/writeOff:
    post:
      operationId: writeOffInvoice
      summary: Write off the remaining balance on an invoice
      tags: [Orders & Revenue]
      description: >
        Writes off the remaining balance on an invoice. The invoice must be
        editable, must not be a credit memo, and must have a positive remaining
        balance.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Invoice after write-off
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Invoice"
        "400":
          description: Business rule violation (non-editable, credit memo, zero balance, etc.)
        "404":
          description: Invoice not found

  /invoices/:
    get:
      operationId: listInvoices
      summary: Search invoices
      tags: [Orders & Revenue]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - name: contract_id
          in: query
          schema:
            type: integer
        - name: advertiser_id
          in: query
          schema:
            type: integer
        - name: date_created_before
          in: query
          schema:
            type: string
            format: date-time
        - name: date_created_after
          in: query
          schema:
            type: string
            format: date-time
        - name: date_due_before
          in: query
          schema:
            type: string
            format: date-time
        - name: date_due_after
          in: query
          schema:
            type: string
            format: date-time
        - name: amount_from
          in: query
          schema:
            type: number
            format: float
          description: Minimum invoice total
        - name: amount_to
          in: query
          schema:
            type: number
            format: float
          description: Maximum invoice total
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of invoices
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Invoice"
    put:
      operationId: createInvoice
      summary: Generate an invoice from a Won contract
      tags: [Orders & Revenue]
      description: >
        Generates a new invoice from a Won contract for the specified insertions.
        The contract must be Won (soldDate must be set). All insertion IDs must
        belong to the specified contract.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contract_id, insertion_ids]
              properties:
                contract_id:
                  type: integer
                  description: ID of the Won contract to invoice
                insertion_ids:
                  type: array
                  items:
                    type: integer
                  description: Array of insertion IDs to include on this invoice
                create_date:
                  type: string
                  format: date
                  description: Invoice creation date (defaults to today)
                due_date:
                  type: string
                  format: date
                  description: Invoice due date
      responses:
        "200":
          description: Generated invoice — returns the new invoice ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        "400":
          description: Contract is not Won, invalid insertion IDs, or insertions already invoiced

  # ── Invoice Line Items ────────────────────────────────────────
  /invoiceLineItems/{id}:
    get:
      operationId: getInvoiceLineItem
      summary: Get a single invoice line item
      tags: [Orders & Revenue]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Invoice line item record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvoiceLineItem"

  /invoiceLineItemTypes/:
    get:
      operationId: listInvoiceLineItemTypes
      summary: List all invoice line item types
      tags: [Orders & Revenue]
      responses:
        "200":
          description: List of invoice line item types
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/InvoiceLineItemType"

  /invoiceLineItemTypes/{id}:
    get:
      operationId: getInvoiceLineItemType
      summary: Get a single invoice line item type
      tags: [Orders & Revenue]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Invoice line item type record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvoiceLineItemType"

  # ── Payments ──────────────────────────────────────────────────
  /payments/{id}:
    get:
      operationId: getPayment
      summary: Get a single payment
      tags: [Orders & Revenue]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Payment record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payment"
        "404":
          description: Payment not found
    delete:
      operationId: deletePayment
      summary: Delete (void) a payment
      tags: [Orders & Revenue]
      description: >
        Deletes a payment and unapplies it from all invoices. The calling user
        must be an admin, sales manager, or accounting. The payment must not
        fall within an accounting lock date period.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "204":
          description: Payment deleted
        "400":
          description: Permission denied or payment is within an accounting lock period
        "404":
          description: Payment not found

  /payments/:
    get:
      operationId: listPayments
      summary: Search payments
      tags: [Orders & Revenue]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - name: advertiser_id
          in: query
          schema:
            type: integer
        - name: contract_id
          in: query
          schema:
            type: integer
        - name: entered_by_user_id
          in: query
          schema:
            type: integer
          description: Filter by user who entered the payment
        - name: date_created_before
          in: query
          schema:
            type: string
            format: date-time
        - name: date_created_after
          in: query
          schema:
            type: string
            format: date-time
        - name: amount_from
          in: query
          schema:
            type: number
            format: float
          description: Minimum payment amount
        - name: amount_to
          in: query
          schema:
            type: number
            format: float
          description: Maximum payment amount
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of payments
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Payment"

  /payments/single:
    post:
      operationId: createSinglePayment
      summary: Create a payment against an invoice
      tags: [Orders & Revenue]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invoice_id, amount, notes, payment_type, payment_date]
              properties:
                invoice_id:
                  type: integer
                amount:
                  type: number
                  format: float
                notes:
                  type: string
                payment_type:
                  type: string
                  description: Payment method (e.g. check, credit_card, wire)
                payment_date:
                  type: string
                  format: date
      responses:
        "200":
          description: Created payment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payment"

  # ── Ad Requests ───────────────────────────────────────────────
  /adRequests/{id}:
    get:
      operationId: getAdRequest
      summary: Get a single ad request
      tags: [Other]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Ad request record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdRequest"
        "404":
          description: Ad request not found
    post:
      operationId: updateAdRequest
      summary: Update an ad request
      tags: [Other]
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [advertiser_id]
              properties:
                advertiser_id:
                  type: integer
                requested_by_user_id:
                  type: integer
                assigned_to_user_id:
                  type: integer
                insertion_id:
                  type: integer
                ad_id:
                  type: integer
                details:
                  type: string
                due_date:
                  type: string
                  format: date
      responses:
        "200":
          description: Updated ad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdRequest"
        "400":
          description: Validation error
        "404":
          description: Ad request not found
    delete:
      operationId: deleteAdRequest
      summary: Delete an ad request
      tags: [Other]
      description: >
        Deletes an ad request. Cannot delete if the request has any proofs or
        if a designer has been assigned to the associated studio order.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "204":
          description: Ad request deleted
        "400":
          description: Cannot delete (has proofs or designer assigned)
        "404":
          description: Ad request not found

  /adRequests/{id}/cancel:
    post:
      operationId: cancelAdRequest
      summary: Cancel an ad request
      tags: [Other]
      description: Cancels an ad request. Cannot cancel an already-canceled request.
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Canceled ad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdRequest"
        "400":
          description: Ad request is already canceled
        "404":
          description: Ad request not found

  /adRequests/:
    get:
      operationId: listAdRequests
      summary: Search ad requests
      tags: [Other]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of ad requests
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AdRequest"
    put:
      operationId: createAdRequest
      summary: Create an ad request
      tags: [Other]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [advertiser_id]
              properties:
                advertiser_id:
                  type: integer
                requested_by_user_id:
                  type: integer
                assigned_to_user_id:
                  type: integer
                insertion_id:
                  type: integer
                ad_id:
                  type: integer
                details:
                  type: string
                due_date:
                  type: string
                  format: date
      responses:
        "200":
          description: Created ad request — returns the new ad request ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        "400":
          description: Validation error

  # ── Ads ───────────────────────────────────────────────────────
  /ads/{id}:
    get:
      operationId: getAd
      summary: Get a single ad
      tags: [Other]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Ad record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ad"
        "404":
          description: Ad not found
    post:
      operationId: updateAd
      summary: Update an ad
      tags: [Other]
      description: >
        Updates an existing ad's metadata. File uploads are not supported via
        the API — use the application UI for uploading creative files.
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [advertiser_id]
              properties:
                advertiser_id:
                  type: integer
                name:
                  type: string
                text:
                  type: string
      responses:
        "200":
          description: Updated ad
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ad"
        "400":
          description: Validation error
        "404":
          description: Ad not found

  /ads/:
    get:
      operationId: listAds
      summary: List / search ads
      tags: [Other]
      parameters:
        - name: id
          in: query
          schema:
            type: integer
        - name: advertiser_id
          in: query
          schema:
            type: integer
        - name: name
          in: query
          schema:
            type: string
          description: Filter by name (partial match)
        - name: active
          in: query
          schema:
            type: boolean
          description: Filter by active status (active = not deleted)
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/sortOrder"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of ads
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Ad"
    put:
      operationId: createAd
      summary: Create an ad
      tags: [Other]
      description: >
        Creates a new ad metadata record without a creative file. To create an
        ad with a creative file in a single call, use PUT /ads/upload.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [advertiser_id]
              properties:
                advertiser_id:
                  type: integer
                name:
                  type: string
                text:
                  type: string
      responses:
        "200":
          description: Created ad — returns the new ad ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        "400":
          description: Validation error

  /ads/upload:
    put:
      operationId: createAdWithCreative
      summary: Create an ad and upload a creative file
      tags: [Other]
      description: >
        Creates a new ad and uploads the creative file in a single call.
        Supply the file as a publicly reachable URL (`file_url`) that the
        server will fetch, or as base64-encoded bytes (`file_base64`). When
        the uploaded file is a raster image (jpg, jpeg, png, gif), width and
        height are populated automatically.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [advertiser_id]
              properties:
                advertiser_id:
                  type: integer
                name:
                  type: string
                text:
                  type: string
                  description: For text-based (classified) ads. When provided without a file, no creative file is stored.
                file_url:
                  type: string
                  format: uri
                  description: Publicly reachable URL of the creative file. Server fetches and stores it. One of `file_url` or `file_base64` is required.
                file_base64:
                  type: string
                  description: Base64-encoded creative file bytes. One of `file_url` or `file_base64` is required.
                file_name:
                  type: string
                  description: Filename with extension (e.g. `banner.jpg`). Required when using `file_base64`; inferred from URL otherwise.
                mime_type:
                  type: string
                  description: MIME type of the file (e.g. `image/jpeg`, `application/pdf`, `video/mp4`).
      responses:
        "201":
          description: Created ad with the uploaded creative
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ad"
        "400":
          description: Validation error
        "500":
          description: Upload failed

  /ads/{id}/upload:
    post:
      operationId: uploadAdCreative
      summary: Upload or replace the creative file on an existing ad
      tags: [Other]
      description: >
        Uploads a creative file and attaches it to an existing ad, replacing
        any current creative. Supply the file as `file_url` or `file_base64`.
        When the uploaded file is a raster image (jpg, jpeg, png, gif), width
        and height are populated automatically.
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                file_url:
                  type: string
                  format: uri
                  description: Publicly reachable URL of the creative file. One of `file_url` or `file_base64` is required.
                file_base64:
                  type: string
                  description: Base64-encoded creative file bytes. One of `file_url` or `file_base64` is required.
                file_name:
                  type: string
                  description: Filename with extension. Required when using `file_base64`.
                mime_type:
                  type: string
                  description: MIME type of the file.
      responses:
        "200":
          description: Updated ad
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ad"
        "400":
          description: Validation error (missing file or ad is archived)
        "404":
          description: Ad not found
        "500":
          description: Upload failed

  /ads/{id}/assign:
    post:
      operationId: assignAdToInsertion
      summary: Assign an ad to an ad insertion
      tags: [Other]
      description: >
        Assigns the specified ad to an ad insertion. If the product supports
        multiple ads or is configured for DFP, the ad is added to
        `adToInsertions`; otherwise the insertion's primary `adID` is set.
        Child ads (any ad whose `parentAdID` equals this ad's `adID`) are
        assigned as well.
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [insertion_id]
              properties:
                insertion_id:
                  type: integer
                  description: The ad insertion to assign this ad to.
      responses:
        "200":
          description: Updated insertion
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Insertion"
        "400":
          description: Ad is archived
        "404":
          description: Ad or insertion not found

  /ads/{id}/unassign:
    post:
      operationId: unassignAdFromInsertion
      summary: Unassign an ad from an ad insertion
      tags: [Other]
      description: >
        Removes the ad (and any child ads) from the specified insertion. If the
        ad was the insertion's primary `adID`, that reference is cleared.
      parameters:
        - $ref: "#/components/parameters/idPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [insertion_id]
              properties:
                insertion_id:
                  type: integer
                  description: The ad insertion to unassign this ad from.
      responses:
        "200":
          description: Updated insertion
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Insertion"
        "404":
          description: Ad or insertion not found

  # ── Memberships ───────────────────────────────────────────────
  /memberships/verify:
    get:
      operationId: verifyMembership
      summary: Verify a member by email
      tags: [Members]
      parameters:
        - name: email
          in: query
          required: true
          schema:
            type: string
            format: email
      responses:
        "200":
          description: Membership verification result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Membership"
        "404":
          description: Member not found

  /memberships/{id}:
    get:
      operationId: getMembership
      summary: Get a single membership
      tags: [Members]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Membership record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Membership"

  /memberships/authenticate:
    post:
      operationId: authenticateMember
      summary: Authenticate a member
      tags: [Members]
      description: >
        Authenticates a member by email combined with either their password or
        the zip code of one of their addresses (first 5 digits). Returns the
        contact record and their active (non-expired) memberships.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  description: The member's password. Required if zip is not supplied.
                zip:
                  type: string
                  description: Zip code of one of the member's addresses. Required if password is not supplied.
      responses:
        "200":
          description: Authentication succeeded
          content:
            application/json:
              schema:
                type: object
                properties:
                  authenticated:
                    type: boolean
                  authentication_method:
                    type: string
                    enum: [password, zip]
                  contact:
                    $ref: "#/components/schemas/Contact"
                  audience_ids:
                    type: array
                    description: IDs of audience filters the contact currently belongs to (from the memberAudienceFilterCache table).
                    items:
                      type: integer
                  memberships:
                    type: array
                    items:
                      $ref: "#/components/schemas/Membership"
                  count:
                    type: integer
        "400":
          description: Missing or invalid email / credentials
        "401":
          description: Authentication failed

  /memberships/passwordReset:
    post:
      operationId: sendMemberPasswordReset
      summary: Send a member a password reset email
      tags: [Members]
      description: >
        Sends the member an email containing a secure one-time login link for
        the given website (the same mechanism as the member portal's instant
        login). After clicking the link the member is logged in and can set a
        new password. A website_id is required so the email is branded and the
        member is redirected to the correct website.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, website_id]
              properties:
                email:
                  type: string
                  format: email
                website_id:
                  type: integer
                url:
                  type: string
                  description: Optional URL the member is redirected to after clicking the link. Defaults to the website's URL.
      responses:
        "200":
          description: Password reset email sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "400":
          description: Missing or invalid email / website_id
        "404":
          description: Website or member not found
        "500":
          description: Email could not be sent

  /memberships/setPassword:
    post:
      operationId: setMemberPassword
      summary: Set a member's password
      tags: [Members]
      description: >
        Sets the password for a member identified by contact_id or email.
        When identified by email, the password is applied to every
        non-deleted member contact matching that email.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password:
                  type: string
                  minLength: 6
                contact_id:
                  type: integer
                  description: The member's contact ID. Required if email is not supplied.
                email:
                  type: string
                  format: email
                  description: The member's email. Required if contact_id is not supplied.
      responses:
        "200":
          description: Password updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  contact_ids:
                    type: array
                    items:
                      type: integer
        "400":
          description: Missing or invalid parameters
        "404":
          description: Member not found

  # ── Membership Types ──────────────────────────────────────────
  /membershipTypes/:
    get:
      operationId: listMembershipTypes
      summary: List membership types
      tags: [Members]
      description: Read-only list of membership types.
      parameters:
        - name: membership_tier_type_id
          in: query
          schema:
            type: integer
          description: Filter by membership tier type ID.
      responses:
        "200":
          description: List of membership types
          content:
            application/json:
              schema:
                type: object
                properties:
                  membership_types:
                    type: array
                    items:
                      $ref: "#/components/schemas/MembershipType"
                  count:
                    type: integer

  /membershipTypes/{id}:
    get:
      operationId: getMembershipType
      summary: Get a single membership type
      tags: [Members]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Membership type record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MembershipType"
        "404":
          description: Membership type not found

  # ── Membership Levels ─────────────────────────────────────────
  /membershipLevels/:
    get:
      operationId: listMembershipLevels
      summary: List membership levels
      tags: [Members]
      description: >
        Read-only list of membership tier levels, grouped by tier type.
      parameters:
        - name: tier_type_id
          in: query
          schema:
            type: integer
          description: Filter by membership tier type ID.
      responses:
        "200":
          description: Membership levels grouped by tier type
          content:
            application/json:
              schema:
                type: object
                properties:
                  membership_levels:
                    type: array
                    items:
                      $ref: "#/components/schemas/MembershipLevelGroup"
                  count:
                    type: integer

  /membershipLevels/{id}:
    get:
      operationId: getMembershipLevel
      summary: Get a single membership level
      tags: [Members]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Membership level record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MembershipLevel"
        "404":
          description: Membership level not found

  # ── Audiences ─────────────────────────────────────────────────
  /audiences/{id}:
    get:
      operationId: getAudience
      summary: Get a single audience
      tags: [Members]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Audience record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Audience"
        "404":
          description: Audience not found

  /audiences/:
    get:
      operationId: listAudiences
      summary: List / search audiences
      tags: [Members]
      description: >
        Returns saved member audience filters (segments). By default, only
        active (non-deleted) audiences are returned.
      parameters:
        - name: id
          in: query
          schema:
            type: integer
          description: Filter by audience ID
        - name: saved_by_user_id
          in: query
          schema:
            type: integer
          description: Filter by the user who saved the audience
        - name: name
          in: query
          schema:
            type: string
          description: Case-insensitive contains-match on audience name
        - name: status
          in: query
          schema:
            type: string
          description: Filter by audience status
        - name: include_deleted
          in: query
          schema:
            type: boolean
          description: Include soft-deleted audiences. Defaults to false.
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/page"
      responses:
        "200":
          description: List of audiences
          content:
            application/json:
              schema:
                type: object
                properties:
                  audiences:
                    type: array
                    items:
                      $ref: "#/components/schemas/Audience"
                  count:
                    type: integer

  # ── Touch Points ──────────────────────────────────────────────
  /touchPoints/{id}:
    get:
      operationId: getTouchPoint
      summary: Get a single touch point
      tags: [Activities]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Touch point record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TouchPoint"
        "404":
          description: Touch point not found

  /touchPoints/:
    put:
      operationId: createTouchPoint
      summary: Create a touch point
      tags: [Activities]
      description: >
        Logs a touch point (activity) such as a phone call, email, SMS,
        appointment, or follow-up. At least one of advertiser_id, agency_id,
        or contact_id is required. When only contact_id is provided, the
        advertiser or agency is inherited from the contact.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type:
                  type: string
                  enum: [Appointment, Email, Follow-up, Phone, SMS]
                advertiser_id:
                  type: integer
                agency_id:
                  type: integer
                contact_id:
                  type: integer
                subject:
                  type: string
                body:
                  type: string
                date:
                  type: string
                  format: date-time
                  description: When the touch point occurred. Defaults to now.
                sent_by_user_id:
                  type: integer
                  description: Defaults to the API user.
                call_result:
                  type: string
                call_direction:
                  type: string
                call_duration:
                  type: integer
                  description: Call duration in seconds
                appointment_duration:
                  type: integer
                  description: Appointment duration in minutes
      responses:
        "201":
          description: Created touch point
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        "400":
          description: Validation error

  # ── Posts (Tasks & Notes) ─────────────────────────────────────
  /posts/{id}:
    get:
      operationId: getPost
      summary: Get a single post (task or note)
      tags: [Activities]
      parameters:
        - $ref: "#/components/parameters/idPath"
      responses:
        "200":
          description: Post record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Post"
        "404":
          description: Post not found

  /tasks/:
    put:
      operationId: createTask
      summary: Create a task
      tags: [Activities]
      description: >
        Creates a task attached to an object. Provide object_type and
        object_id, or use advertiser_id as a shortcut. The task is assigned
        to assigned_to_user_id (defaults to the API user), who is notified.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message:
                  type: string
                  description: The body of the task.
                subject:
                  type: string
                object_type:
                  type: string
                  description: The type of object the task is attached to (e.g. advertisers, agencies, contacts, contracts, invoices).
                object_id:
                  type: integer
                advertiser_id:
                  type: integer
                  description: Shortcut for object_type=advertisers and object_id.
                assigned_to_user_id:
                  type: integer
                  description: Defaults to the API user.
                due_date:
                  type: string
                  format: date
                  description: Defaults to today.
                start_date:
                  type: string
                  format: date
      responses:
        "201":
          description: Created task
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        "400":
          description: Validation error

  /notes/:
    put:
      operationId: createNote
      summary: Create a note
      tags: [Activities]
      description: >
        Creates a note attached to an object. Provide object_type and
        object_id, or use advertiser_id as a shortcut.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message:
                  type: string
                  description: The body of the note.
                subject:
                  type: string
                object_type:
                  type: string
                  description: The type of object the note is attached to (e.g. advertisers, agencies, contacts, contracts, invoices).
                object_id:
                  type: integer
                advertiser_id:
                  type: integer
                  description: Shortcut for object_type=advertisers and object_id.
      responses:
        "201":
          description: Created note
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        "400":
          description: Validation error

