openapi: 3.0.1
info:
  title: AdBridge Podcast API
  version: 1.0.0
  description: External API for managing podcasts, voices, and advertising campaigns.
tags:
- name: Account
  description: Information about the calling API key
- name: Networks
  description: Networks (organizations) that own podcasts and campaigns
- name: Podcasts
  description: Podcasts owned by a network
- name: Voices
  description: Host voices belonging to a podcast
- name: Transcriptions
  description: Audio transcriptions belonging to a podcast
- name: Advertisers
  description: Advertisers running campaigns within a network
- name: Campaigns
  description: Advertising campaigns within a network
- name: Offers
  description: Campaign offers (a campaign matched to a podcast)
- name: Creatives
  description: Host-voiced ads for a podcast
- name: Users
  description: Podcaster portal user accounts
- name: Members
  description: Network members (role/status management)
- name: Network invitations
  description: Network user invitations
- name: Service accounts
  description: Non-human accounts that belong to a network
- name: API keys
  description: Self-service management of the caller's own API keys
- name: Webhooks
  description: Outbound webhook subscriptions for lifecycle events
- name: Opt-in invitations
  description: Podcast opt-in registration invitations
- name: Public — Opt-in
  description: Recipient-facing podcast opt-in (magic-link token)
- name: Public — Opt-in voice
  description: Recipient-facing voice creation for opt-in (magic-link token)
- name: Public — Campaign offer
  description: Recipient-facing campaign-offer review (magic-link token)
- name: Public — Invitations
  description: Recipient-facing network invitation accept (magic-link token)
paths:
  /v1/advertisers/{id}:
    delete:
      tags:
      - Advertisers
      summary: Delete an advertiser (privileged)
      description: Permanently deletes the platform-wide advertiser document and responds
        204. Requires the super_user role; ordinary network-scoped keys cannot delete
        advertisers. This is a hard delete of the advertiser record only — it does
        not cascade to campaigns that reference it. The delete is optimistic-locked,
        so a concurrent modification returns 409, and an advertiser id that does not
        exist returns 404.
      operationId: deleteAdvertiser
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: Advertiser deleted
        '403':
          description: Deleting an advertiser requires the super_user role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Advertiser not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/apikeys:
    get:
      tags:
      - API keys
      summary: List the caller's API keys
      description: Lists every API key owned by the calling principal, including revoked
        and expired keys. The secret token is never returned here — only metadata
        such as the name, last four characters, scopes, and status. A super-user key
        may inspect another owner's keys by passing owner_type and owner_id; for everyone
        else those parameters are ignored and only the caller's own keys are returned.
      operationId: listApiKeys
      parameters:
      - name: owner_type
        in: query
        explode: false
        schema:
          type: string
          default: user
      - name: owner_id
        in: query
        required: true
        explode: false
        schema:
          type: string
      responses:
        '200':
          description: List the caller's API keys
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyListResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - API keys
      summary: Create an API key owned by the caller (token shown once)
      description: Mints a new key owned by the calling principal — the owner is always
        the caller and cannot be specified. The plaintext token is included in this
        response and only this response, so store it immediately; it cannot be retrieved
        later. Each requested scope must be a network/role the caller already holds
        (a key can only delegate a subset of its own access), and only a super-user
        key may set super_user=true. A key must request either super_user=true or
        at least one scope.
      operationId: createApiKey
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreateRequest'
        required: true
      responses:
        '200':
          description: Create an API key owned by the caller (token shown once)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreateResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/apikeys/{id}:
    get:
      tags:
      - API keys
      summary: Get one of the caller's API keys
      description: Returns metadata for a single key the caller owns; the secret token
        is never returned. A super-user may fetch any key. To avoid leaking whether
        an id exists, a key owned by someone else is reported as not found (404) rather
        than forbidden.
      operationId: getApiKey
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Get one of the caller's API keys
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - API keys
      summary: Revoke one of the caller's API keys
      description: 'Soft-revokes a key the caller owns (a super-user may revoke any
        key): the key is marked revoked and stops authenticating immediately, but
        its record is kept rather than deleted. Revoking an already-revoked or expired
        key is a no-op and still returns success.'
      operationId: revokeApiKey
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Revoke one of the caller's API keys
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/me:
    get:
      tags:
      - Account
      summary: Describe the calling API key and its access
      description: 'Returns the identity of the API key making the request: its display
        name, whether it is a super-user key, and the per-network roles it has been
        scoped to. Use this to discover which networks the key can reach and at what
        role before calling network-scoped endpoints. Internal identifiers (owner
        id/type, key id) are deliberately not exposed.'
      operationId: getCurrentKey
      responses:
        '200':
          description: Describe the calling API key and its access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks:
    get:
      tags:
      - Networks
      summary: List networks the key can access
      description: 'Returns the networks visible to the calling API key: a super-user
        key sees every network, while a scoped key sees only the networks listed in
        its scopes. Results are cursor-paginated and ordered by network id; pass the
        returned cursor to fetch the next page.'
      operationId: listNetworks
      parameters:
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '200':
          description: List networks the key can access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_NetworkDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Networks
      summary: Create a network (privileged)
      description: Creates a new network (organization). Requires the super_user role;
        ordinary network-scoped keys cannot create networks. The name, email, and
        a well-formed primary_contact are required. On success returns 201 with the
        created network and a Location header pointing at the new resource.
      operationId: createNetwork
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NetworkCreateRequest'
      responses:
        '201':
          description: Network created
          headers:
            Location:
              description: URL of the created network
              schema:
                type: string
        '400':
          description: Missing name/email/primary_contact or a malformed contact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Creating a network requires the super_user role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{id}:
    get:
      tags:
      - Networks
      summary: Get a network by id
      description: Returns a single network's profile, including its contacts and
        email settings. Requires at least read-only access to the network. Responds
        with 404 if the network does not exist.
      operationId: getNetwork
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Get a network by id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NetworkDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Networks
      summary: Delete a network (privileged)
      description: Permanently deletes the network document and responds 204. Requires
        the super_user role; ordinary network-scoped keys cannot delete networks.
        This is a hard delete of the network record only — it does not cascade to
        the network's podcasts, campaigns, members, or other nested resources. The
        delete is optimistic-locked, so a concurrent modification returns 409, and
        a network id that does not exist returns 404.
      operationId: deleteNetwork
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: Network deleted
        '403':
          description: Deleting a network requires the super_user role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Network not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}:
    patch:
      tags:
      - Networks
      summary: Update a network's mutable business fields
      description: Partially updates a network's business fields (name, email, description,
        website, default email language, and contacts); only the fields you supply
        are changed and the rest are left intact. Requires the administrator role
        on the network. The write is optimistically locked, so a concurrent modification
        returns 409.
      operationId: updateNetwork
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NetworkUpdateRequest'
      responses:
        '200':
          description: The updated network
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NetworkDto'
        '400':
          description: A malformed contact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Network not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/advertisers:
    get:
      tags:
      - Advertisers
      summary: List advertisers referenced by the network's campaigns
      description: Returns a cursor-paginated page of advertisers, scoped to only
        those referenced by a campaign's advertiser_id in this network (platform-wide
        advertisers not used here are not exposed). Ordered by id; use the returned
        cursor for the next page. Requires the read_only network role.
      operationId: listAdvertisers
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '200':
          description: A page of advertisers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_AdvertiserDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Advertisers
      summary: Create an advertiser
      description: Creates a platform-wide advertiser and returns it with a Location
        header. name and email are both required (otherwise 400). The advertiser becomes
        visible through this network's read endpoints only once a campaign in the
        network references it via advertiser_id. Requires the administrator network
        role.
      operationId: createAdvertiser
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdvertiserCreateRequest'
      responses:
        '201':
          description: Advertiser created
          headers:
            Location:
              description: URL of the created advertiser
              schema:
                type: string
        '400':
          description: Missing name or email
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/advertisers/{id}:
    get:
      tags:
      - Advertisers
      summary: Get an advertiser referenced by the network's campaigns
      description: Returns a single advertiser, but only if it is referenced by at
        least one campaign in this network; otherwise 404 (even if the advertiser
        exists for another network). This is how a campaign's advertiser_id is resolved.
        Requires the read_only network role.
      operationId: getAdvertiser
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The advertiser
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdvertiserDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
      - Advertisers
      summary: Update an advertiser's mutable business fields
      description: 'Partial update: only the fields you send are changed. As with
        the read endpoints, the advertiser is addressable only if referenced by a
        campaign in this network; otherwise 404. The write is optimistic-locked, so
        a concurrent modification returns 409. Requires the administrator network
        role.'
      operationId: updateAdvertiser
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdvertiserUpdateRequest'
      responses:
        '200':
          description: The updated advertiser
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdvertiserDto'
        '404':
          description: Advertiser not referenced by this network
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/campaigns:
    get:
      tags:
      - Campaigns
      summary: List campaigns in a network
      description: Returns a cursor-paginated page of the network's campaigns ordered
        by id; use the returned cursor to fetch the next page. Pass ?expand=advertiser
        to inline each campaign's advertiser. Requires the read_only network role.
      operationId: listCampaigns
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      - name: expand
        in: query
        explode: false
        schema:
          type: string
          nullable: true
      responses:
        '200':
          description: List campaigns in a network
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_CampaignDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Campaigns
      summary: Create a campaign in a network
      description: Creates a campaign owned by the network and returns it with a Location
        header. Only name is required; advertiser_id, when supplied, must reference
        an existing advertiser (otherwise 400). budget and cpm are in US dollars.
        platform (adbridge/adswizz/megaphone, defaults to adbridge), an adswizz link,
        and targeting/tracking may be set. The new campaign starts in the platform-owned
        pending status; status cannot be set here. Requires the administrator network
        role.
      operationId: createCampaign
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignCreateRequest'
      responses:
        '201':
          description: Campaign created
          headers:
            Location:
              description: URL of the created campaign
              schema:
                type: string
        '400':
          description: Missing name, malformed ad variant, unknown advertiser, or
            invalid platform
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/campaigns/{campaignId}/offers:
    get:
      tags:
      - Offers
      summary: List offers for a campaign
      description: Returns a cursor-paginated page of the campaign's offers (each
        an offer of this campaign to a podcast), ordered by id; use the returned cursor
        for the next page. Each offer carries a read-only status of pending, accepted,
        rejected, or negotiating. Requires the read_only network role.
      operationId: listOffers
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '200':
          description: A page of offers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_OfferDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Offers
      summary: 'Send an offer: match a podcast to the campaign and notify the podcast
        owner'
      description: Creates a pending offer linking the podcast to the campaign and
        emails the podcast owner a tokenized review link. The campaign must have all
        fields required to send an offer filled in and, if variant_id is given, that
        variant must exist; the podcast must belong to this network; and no offer
        may already exist for the podcast (and variant) (any of these returns 400).
        The email is best-effort and never fails the request (the response's email_sent
        reports whether it went out). Requires the administrator network role.
      operationId: createOffer
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OfferCreateRequest'
      responses:
        '201':
          description: Offer created and the owner notified
          headers:
            Location:
              description: URL of the created offer
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfferCreateResponse'
        '400':
          description: Missing podcast_id, campaign not ready to send, unknown variant,
            podcast not in network, or a duplicate offer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign or podcast not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/campaigns/{campaignId}/offers/direct-approve:
    post:
      tags:
      - Offers
      summary: Directly approve a podcast for the campaign (admin bypass)
      description: Creates (or reuses an open) offer for the podcast and approves
        it immediately, skipping the recipient invitation/verification flow, then
        renders the host-voiced creative exactly as accept does (resolving voice_id
        — or the podcast's primary voice — and the campaign copy, linking creative_id).
        Returns 400 for a missing podcast_id, an unknown variant, or a podcast outside
        this network; 409 if an offer already exists and is no longer open, or the
        podcast/campaign isn't renderable. Audio synthesis runs asynchronously (poll
        the creative or use the creative webhook). Requires the administrator network
        role.
      operationId: directApproveOffer
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OfferDirectApproveRequest'
      responses:
        '200':
          description: The approved offer, linked via creative_id to a pending creative
            (audio generates asynchronously)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfferDto'
        '400':
          description: Missing podcast_id, unknown variant, or podcast not in this
            network
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign or podcast not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: An offer already exists and is no longer open, or the podcast/campaign
            isn't ready to render a creative
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/campaigns/{campaignId}/offers/{id}:
    get:
      tags:
      - Offers
      summary: Get a single offer
      description: Returns one offer for the campaign, or 404 if it does not exist.
        The offer's status (pending, accepted, rejected, or negotiating) is read-only;
        transitions happen only via the accept/reject actions. Once accepted, creative_id
        links to the generated host-voiced creative. Requires the read_only network
        role.
      operationId: getOffer
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Get a single offer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfferDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/campaigns/{campaignId}/offers/{id}/accept:
    post:
      tags:
      - Offers
      summary: Accept an offer and start generating its host-voiced creative
      description: Transitions an open offer (pending or negotiating) to accepted;
        an offer that is already accepted or rejected returns 409. As a consequence
        of acceptance the server renders the host-voiced creative for you (resolving
        the podcast's configured voice and the campaign copy) and links it back via
        creative_id; if the podcast has no usable voice or the campaign isn't renderable
        the whole action fails 409 with no state change. The transition is synchronous
        and returns 200 immediately with a still-pending creative; the audio synthesis
        then runs asynchronously, so poll the creative for ready/failed or subscribe
        to the creative.ready / creative.failed webhook. Requires the administrator
        network role.
      operationId: acceptOffer
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: 'The accepted offer, linked via creative_id to a pending creative.
            Audio generation runs asynchronously: poll GET /v1/podcasts/{podcastId}/creatives/{creative_id}
            for ready/failed, or subscribe to the creative.ready / creative.failed
            webhook.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfferDto'
        '409':
          description: Offer cannot be accepted from its current status, or the podcast/campaign
            isn't ready to render a creative
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/campaigns/{campaignId}/offers/{id}/reject:
    post:
      tags:
      - Offers
      summary: Reject an offer
      description: Transitions an open offer (pending or negotiating) to rejected
        and returns it; an offer that is already accepted or rejected returns 409.
        Unlike accept, this triggers no creative generation. Requires the administrator
        network role.
      operationId: rejectOffer
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The rejected offer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfferDto'
        '409':
          description: Offer cannot be rejected from its current status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/campaigns/{campaignId}/offers/{id}/resend-verification-code:
    post:
      tags:
      - Offers
      summary: Generate a fresh offer verification code and email it to the podcast
        owner
      description: Issues a new verification code on the offer's latest invitation
        (valid for 30 minutes) and emails it to the podcast's primary contact, used
        when the owner reviews the offer via the magic link. Returns 400 if the podcast
        has no contact email and 404 if the offer has no invitation. The code is set
        even if the email send fails; the response reports email_sent and the code's
        expiry. Requires the administrator network role.
      operationId: resendOfferVerificationCode
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: A new code was generated; the email send is best-effort
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResendCodeResponse'
        '400':
          description: The offer's podcast has no contact email
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Offer, campaign, podcast, or the offer's invitation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/campaigns/{id}:
    get:
      tags:
      - Campaigns
      summary: Get a campaign by id
      description: Returns a single campaign in the network, or 404 if it does not
        exist. Pass ?expand=advertiser to inline the campaign's advertiser (resolved
        from advertiser_id). Requires the read_only network role.
      operationId: getCampaign
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: expand
        in: query
        explode: false
        schema:
          type: string
          nullable: true
      responses:
        '200':
          description: Get a campaign by id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Campaigns
      summary: Delete a campaign
      description: Permanently deletes the campaign document and responds 204. This
        is a hard delete of the campaign record only — it does not cascade to the
        campaign's offers or other nested resources. The delete is optimistic-locked,
        so a concurrent modification returns 409, and a campaign id that does not
        exist returns 404. Requires the administrator network role.
      operationId: deleteCampaign
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: Campaign deleted
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
      - Campaigns
      summary: Update a campaign's mutable business fields
      description: 'Partial update: only the fields you send are changed. advertiser_id,
        when supplied, must reference an existing advertiser; budget and cpm are in
        US dollars. platform, an adswizz link, and targeting/tracking may be updated.
        status is read-only and cannot be changed here (use archive). The write is
        optimistic-locked, so a concurrent modification returns 409. Requires the
        administrator network role.'
      operationId: updateCampaign
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignUpdateRequest'
      responses:
        '200':
          description: The updated campaign
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignDto'
        '400':
          description: Malformed ad variant, unknown advertiser, or invalid platform
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/campaigns/{id}/archive:
    post:
      tags:
      - Campaigns
      summary: Archive a campaign
      description: Transitions the campaign to the archived status and returns it.
        This is the only client-driven status transition; the rest of the lifecycle
        (pending, active, completed, cancelled) is platform-owned. Archiving an already-archived
        campaign returns 409, as does a concurrent modification. Requires the administrator
        network role.
      operationId: archiveCampaign
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The archived campaign
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignDto'
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Campaign is already archived, or a version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/invitations:
    get:
      tags:
      - Network invitations
      summary: List the network's user invitations
      description: Returns the network's user invitations, including pending, accepted,
        revoked, and expired ones, each with its target email and granted role. Requires
        at least read-only access. Results are cursor-paginated and ordered by invitation
        id.
      operationId: listNetworkInvitations
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '200':
          description: A page of invitations
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_UserInvitationDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Network invitations
      summary: Invite a user to the network and email them an accept link
      description: Creates an invitation for the given email with a membership role
        (admin or viewer) and emails the recipient a tokenized accept link. Requires
        the administrator role. Email delivery is best-effort and never fails the
        request, so the 201 response reports whether the email was sent and also returns
        the invite_url directly. The response includes the one-time accept URL, which
        is only surfaced at creation time.
      operationId: createNetworkInvitation
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserInvitationCreateRequest'
      responses:
        '201':
          description: Invitation created and the recipient notified
          headers:
            Location:
              description: URL of the created invitation
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInvitationCreateResponse'
        '400':
          description: Missing email/name or an invalid role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Network not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/invitations/{invitationId}:
    get:
      tags:
      - Network invitations
      summary: Get a single invitation
      description: Returns one invitation by id, including its current status. Requires
        at least read-only access. The recipient's accept token is never exposed here.
        Responds with 404 if the invitation does not belong to this network.
      operationId: getNetworkInvitation
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: invitationId
        in: path
        required: true
        schema:
          type: string
      responses:
        '404':
          description: Invitation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '200':
          description: Get a single invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInvitationDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/invitations/{invitationId}/revoke:
    post:
      tags:
      - Network invitations
      summary: Revoke a pending invitation so its link can no longer be accepted
      description: Marks a pending invitation as revoked, invalidating its accept
        link. Requires the administrator role. Only invitations that are still pending
        can be revoked; one that has already been accepted, revoked, or expired returns
        409. Returns the updated invitation.
      operationId: revokeNetworkInvitation
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: invitationId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The revoked invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInvitationDto'
        '404':
          description: Invitation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Invitation is not pending and cannot be revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/members:
    get:
      tags:
      - Members
      summary: List the members of a network
      description: Returns the network's members, each with its role and status. Requires
        at least read-only access. Results are cursor-paginated and ordered by member
        id. Pass ?expand=user to inline each member's user profile in the response.
      operationId: listMembers
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      - name: expand
        in: query
        explode: false
        schema:
          type: string
          nullable: true
      responses:
        '200':
          description: A page of members
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_MemberDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Members
      summary: Add an existing user to the network with a role
      description: Adds an already-existing user to the network with the given role
        (admin or viewer), creating an immediately active membership; this does not
        send an email (use the invitation endpoints to onboard someone by email).
        Requires the administrator role, and a caller may not grant a role higher
        than its own (403). Returns 400 if the user does not exist and 409 if they
        are already a member.
      operationId: addMember
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MemberAddRequest'
      responses:
        '201':
          description: Member added
          headers:
            Location:
              description: URL of the member
              schema:
                type: string
        '400':
          description: Missing user_id/role, invalid role, or unknown user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Cannot grant a role higher than your own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: User is already a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/members/{userId}:
    get:
      tags:
      - Members
      summary: Get a single network member
      description: Returns one member of the network by user id. Requires at least
        read-only access. Pass ?expand=user to inline the member's user profile. Responds
        with 404 if the user is not a member of this network.
      operationId: getMember
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: userId
        in: path
        required: true
        schema:
          type: string
      - name: expand
        in: query
        explode: false
        schema:
          type: string
          nullable: true
      responses:
        '404':
          description: Member not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '200':
          description: Get a single network member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Members
      summary: Remove a member from the network
      description: Removes a member's membership from the network. Requires the administrator
        role. Removing the last active admin is blocked and returns 409 (as is a version
        conflict). On success returns 204 with no body.
      operationId: removeMember
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: userId
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: Member removed
        '404':
          description: Member not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Would remove the last active admin, or a version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
      - Members
      summary: Change a member's role and/or status
      description: Updates a member's role (admin/viewer) and/or status (active/pending/suspended);
        at least one of the two must be provided. Requires the administrator role,
        and a caller cannot grant a role higher than its own (403). Demoting, suspending,
        or otherwise removing the last active admin is blocked and returns 409, as
        does a version conflict.
      operationId: updateMember
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: userId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MemberUpdateRequest'
      responses:
        '200':
          description: The updated member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MemberDto'
        '400':
          description: Nothing to update, or an invalid role/status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Cannot grant a role higher than your own
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Member not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Would demote/suspend the last active admin, or a version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/podcasts:
    get:
      tags:
      - Podcasts
      summary: List podcasts in a network
      description: Requires the read_only role on the network. Returns a cursor-paginated
        page; pass the returned cursor back as ?cursor to fetch the next page and
        ?limit to size it. Add ?expand=primary_voice to inline each podcast's platform-assigned
        primary voice when one is set.
      operationId: listPodcasts
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      - name: expand
        in: query
        explode: false
        schema:
          type: string
          nullable: true
      responses:
        '200':
          description: List podcasts in a network
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_PodcastDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Podcasts
      summary: Create a podcast in a network
      description: Requires the administrator role on the network. title, feed_url,
        and a valid primary_contact are mandatory. Platform-owned fields (opt_in_status,
        primary_voice_id) cannot be set here. Responds 201 with the created podcast
        and a Location header pointing at its by-id resource.
      operationId: createPodcast
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PodcastCreateRequest'
      responses:
        '201':
          description: Podcast created
          headers:
            Location:
              description: URL of the created podcast
              schema:
                type: string
        '400':
          description: Missing title/feed_url/primary_contact or a malformed contact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/podcasts/{podcastId}/invitations:
    post:
      tags:
      - Opt-in invitations
      summary: Invite a podcast owner to complete opt-in registration and email them
        the link
      description: Requires the administrator role on the network, and the podcast
        must belong to that network and have a primary-contact email. If an active
        invitation already exists it is reused rather than duplicated (see reused
        in the response). The owner is emailed the opt-in link best-effort, so a failed
        send still returns 201 with email_sent=false. Responds 201 with the invitation
        and a Location header.
      operationId: createOptInInvitation
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OptInInvitationCreateRequest'
      responses:
        '201':
          description: Invitation created (or active one reused) and the owner notified
          headers:
            Location:
              description: URL of the invitation
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptInInvitationCreateResponse'
        '400':
          description: Podcast not in this network or has no contact email
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Podcast or network not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/podcasts/{podcastId}/invitations/{invitationId}/resend-code:
    post:
      tags:
      - Opt-in invitations
      summary: Generate a fresh opt-in verification code and email it to the podcast
        owner
      description: Requires the administrator role on the network, and the podcast
        must belong to that network and have a primary-contact email. Generates a
        new 30-minute verification code on the named invitation (superseding any prior
        code) and emails it to the owner best-effort, so a failed send still returns
        200 with email_sent=false. The response reports the code's expiry.
      operationId: resendOptInVerificationCode
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: invitationId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: A new code was generated; the email send is best-effort
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResendCodeResponse'
        '400':
          description: Podcast not in this network or has no contact email
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Podcast, network, or invitation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/service-accounts:
    get:
      tags:
      - Service accounts
      summary: List a network's service accounts
      description: 'Returns the non-human accounts that belong to this network, each
        carrying its network membership role and status alongside the account record.
        Requires read-only access to the network. Results are cursor-paginated: pass
        the returned cursor to fetch the next page.'
      operationId: listServiceAccounts
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '200':
          description: List a network's service accounts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_ServiceAccountDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Service accounts
      summary: Create a service account
      description: Creates a non-human account in this network and, in the same operation,
        its network membership at the requested role (admin or viewer). Requires the
        administrator role, and you may only grant a membership role you yourself
        hold. The account starts active but owns no keys yet — mint one separately.
        If membership creation fails the new account is rolled back so no orphan identity
        is left behind.
      operationId: createServiceAccount
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ServiceAccountCreateRequest'
      responses:
        '200':
          description: Create a service account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServiceAccountDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/service-accounts/{id}:
    get:
      tags:
      - Service accounts
      summary: Get a service account
      description: Returns one service account in this network together with its membership
        role and status. Requires read-only access to the network.
      operationId: getServiceAccount
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Get a service account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServiceAccountDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Service accounts
      summary: Delete a service account
      description: Permanently removes the service account. All of its active API
        keys are revoked first, then its network membership is removed, then the account
        record itself — so a partial failure can never leave live keys behind a deleted
        identity. Requires the administrator role.
      operationId: deleteServiceAccount
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Delete a service account
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
      - Service accounts
      summary: Update a service account
      description: 'Updates name, description, status, or membership role; at least
        one field must be supplied. Requires the administrator role, and raising the
        role is bounded by the role you hold. Status changes propagate to membership:
        setting status to disabled suspends the membership and immediately revokes
        all of the account''s active keys, while re-activating restores the membership
        to active. Service accounts are exempt from the last-active-admin guard, which
        counts human members only.'
      operationId: updateServiceAccount
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ServiceAccountUpdateRequest'
      responses:
        '200':
          description: Update a service account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServiceAccountDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/service-accounts/{id}/keys:
    get:
      tags:
      - Service accounts
      summary: List a service account's API keys
      description: Lists the API keys owned by this service account, including revoked
        and expired ones; secret tokens are never returned. Requires read-only access
        to the network.
      operationId: listServiceAccountKeys
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: List a service account's API keys
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyListResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Service accounts
      summary: Mint an API key for a service account (token shown once)
      description: Mints an API key owned by the service account and scoped to this
        network only. The plaintext token is returned in this response and only this
        response, so capture it right away. Requires the administrator role. The key's
        role defaults to the account's membership role and may be narrowed but never
        raised above it, and it can never exceed the minting admin's own role. Keys
        cannot be minted for a disabled account.
      operationId: createServiceAccountKey
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ServiceAccountKeyCreateRequest'
      responses:
        '200':
          description: Mint an API key for a service account (token shown once)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreateResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/service-accounts/{id}/keys/{keyId}:
    delete:
      tags:
      - Service accounts
      summary: Revoke a service account's API key
      description: 'Soft-revokes a single key belonging to this service account: it
        stops authenticating immediately but its record is retained. Requires the
        administrator role. A key id that does not belong to this account returns
        not found (404), and revoking an already-revoked key is a no-op that still
        succeeds.'
      operationId: revokeServiceAccountKey
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: keyId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Revoke a service account's API key
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/stats:
    get:
      tags:
      - Networks
      summary: Aggregate counts for a network
      description: 'Returns live aggregate counts for the network: total and opted-in
        podcasts, total campaigns, total offers broken down by status (pending, accepted,
        rejected, negotiating), and total members. Requires at least read-only access.
        The counts are computed on demand by scanning the network''s collections rather
        than read from denormalized counters, so this call is heavier than the other
        reads.'
      operationId: getNetworkStats
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Aggregate counts for a network
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NetworkStatsResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/webhooks:
    get:
      tags:
      - Webhooks
      summary: List a network's webhook subscriptions
      description: Returns a cursor-paginated page of the network's webhook subscriptions,
        ordered by id; each shows its url, subscribed event types, and status (active
        or disabled). The signing secret is never included. Requires read access to
        the network.
      operationId: listWebhooks
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '200':
          description: A page of webhooks (the signing secret is never included)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_WebhookDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Webhooks
      summary: Register a webhook subscription; the signing secret is returned ONCE
      description: Registers an endpoint to receive push deliveries for the given
        lifecycle events. Valid event types are offer.accepted, offer.rejected, optin.completed,
        invitation.accepted, creative.ready, and creative.failed. The response carries
        a high-entropy signing secret returned only here (store it now); it keys the
        HMAC-SHA256 X-AdBridge-Signature on every delivery so you can verify authenticity.
        New subscriptions start active. Requires administrator access to the network.
      operationId: createWebhook
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreateRequest'
      responses:
        '201':
          description: Webhook created; response carries the secret (shown only here)
          headers:
            Location:
              description: URL of the created webhook
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookCreateResponse'
        '400':
          description: Missing/invalid url (must be http/https) or unknown/empty events
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/networks/{networkId}/webhooks/{id}:
    get:
      tags:
      - Webhooks
      summary: Get a single webhook subscription
      description: Fetches one webhook subscription, including its url, subscribed
        event types, and status. The signing secret is shown only once at creation
        and is never returned here. Requires read access to the network.
      operationId: getWebhook
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The webhook (the signing secret is never included)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDto'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Webhooks
      summary: Delete a webhook subscription
      description: Permanently removes the subscription so it stops receiving deliveries;
        to pause temporarily, update its status to disabled instead. The delete is
        optimistic-locked and returns 409 on a version conflict. Requires administrator
        access to the network.
      operationId: deleteWebhook
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: Webhook deleted
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: A version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
      - Webhooks
      summary: Update a webhook's url, events, and/or status
      description: Partially updates the subscription; supply any of url, events,
        or status (active or disabled) — at least one is required. Set status to disabled
        to pause deliveries without deleting the subscription. The signing secret
        cannot be changed or re-read. Writes are optimistic-locked and return 409
        on a version conflict. Requires administrator access to the network.
      operationId: updateWebhook
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookUpdateRequest'
      responses:
        '200':
          description: The updated webhook (the secret is never included)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDto'
        '400':
          description: Nothing to update, or an invalid url/events/status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: A version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{id}:
    get:
      tags:
      - Podcasts
      summary: Get a podcast by id
      description: The network is resolved from the podcast document and the caller
        must hold the read_only role on that network (403 otherwise, 404 if no such
        podcast). Add ?expand=primary_voice to inline the podcast's primary voice
        when one is set.
      operationId: getPodcast
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: expand
        in: query
        explode: false
        schema:
          type: string
          nullable: true
      responses:
        '200':
          description: Get a podcast by id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PodcastDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Podcasts
      summary: Delete a podcast
      description: The network is resolved from the podcast document and the caller
        must hold the administrator role on that network (403 otherwise, 404 if no
        such podcast). Permanently deletes the podcast document and responds 204.
        This is a hard delete of the podcast record only — it does not cascade to
        the podcast's voices, creatives, or other nested resources. The delete is
        optimistic-locked, so a concurrent modification returns 409.
      operationId: deletePodcast
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: Podcast deleted
        '403':
          description: Not authorized for the podcast's network
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Podcast not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
      - Podcasts
      summary: Update a podcast's mutable business fields
      description: 'The network is resolved from the podcast document and the caller
        must hold the administrator role on it. This is a partial update: only the
        fields present in the body are changed, and platform-owned fields are never
        accepted. The write is optimistic-locked, so a concurrent modification returns
        409.'
      operationId: updatePodcast
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PodcastUpdateRequest'
      responses:
        '200':
          description: The updated podcast
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PodcastDto'
        '400':
          description: A malformed contact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Podcast not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{podcastId}/creatives:
    get:
      tags:
      - Creatives
      summary: List creatives for a podcast
      description: Returns a cursor-paginated page of the podcast's host-voiced creatives,
        ordered by id. Each item carries its current generation status (pending, ready,
        or failed). Requires read access to the podcast's network.
      operationId: listCreatives
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '200':
          description: A page of creatives
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_CreativeDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Creatives
      summary: Create a creative and start generating its host-voiced audio
      description: Resolves the ad copy server-side (from the supplied ad_copy, otherwise
        the referenced campaign variant) and the voice (voice_id, defaulting to the
        podcast's primary voice), creates a pending creative, and kicks off synthesis
        asynchronously. Returns 202 with a Location header; poll that creative for
        ready/failed or subscribe to the creative.ready/creative.failed webhook. Requires
        administrator access to the podcast's network.
      operationId: createCreative
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreativeCreateRequest'
      responses:
        '202':
          description: Creative created (pending); audio generation started asynchronously.
            Poll the creative (Location) for status ready/failed, or subscribe to
            the creative webhook.
          headers:
            Location:
              description: URL of the created creative (the result resource to poll)
              schema:
                type: string
        '400':
          description: Missing campaign_id, no usable voice, or ad copy could not
            be resolved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{podcastId}/creatives/{id}:
    get:
      tags:
      - Creatives
      summary: Get a single creative
      description: Fetches one creative, the result resource for host-voiced generation.
        Poll this while status is pending; on success it becomes ready with an audio_url,
        on failure it becomes failed with a client-safe failure_reason. Requires read
        access to the podcast's network.
      operationId: getCreative
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The creative
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreativeDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{podcastId}/creatives/{id}/regenerate:
    post:
      tags:
      - Creatives
      summary: Regenerate a creative's host-voiced audio (e.g. after a failed generation)
      description: Re-runs generation for an existing creative, reusing its already-resolved
        ad copy and voice; useful to retry a failed creative or refresh a ready one.
        Like create, synthesis runs asynchronously and returns 202 with a Location
        header — poll the creative for ready/failed. Requires administrator access
        to the podcast's network.
      operationId: regenerateCreative
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreativeRegenerateRequest'
      responses:
        '202':
          description: Regeneration started asynchronously; poll the creative (Location)
            for ready/failed
          headers:
            Location:
              description: URL of the creative (the result resource to poll)
              schema:
                type: string
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{podcastId}/transcriptions:
    get:
      tags:
      - Transcriptions
      summary: List transcriptions for a podcast
      description: The network is resolved from the parent podcast and the caller
        must hold the read_only role on it. Returns a cursor-paginated page; pass
        the returned cursor back as ?cursor and size it with ?limit.
      operationId: listTranscriptions
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '200':
          description: A page of transcriptions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_TranscriptionDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{podcastId}/transcriptions/{id}:
    get:
      tags:
      - Transcriptions
      summary: Get a single transcription
      description: The network is resolved from the parent podcast and the caller
        must hold the read_only role on it (403 otherwise, 404 if the podcast or transcription
        does not exist).
      operationId: getTranscription
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The transcription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptionDto'
        '404':
          description: Podcast or transcription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Transcriptions
      summary: Delete a transcription
      description: Requires the administrator role on the parent podcast's network.
        Permanently removes the transcription and responds 204. The delete is optimistic-locked,
        so a concurrent modification returns 409.
      operationId: deleteTranscription
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: Transcription deleted
        '404':
          description: Podcast or transcription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{podcastId}/voices:
    get:
      tags:
      - Voices
      summary: List voices for a podcast
      description: The network is resolved from the parent podcast and the caller
        must hold the read_only role on it. Returns a cursor-paginated page; pass
        the returned cursor back as ?cursor and size it with ?limit.
      operationId: listVoices
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '200':
          description: A page of voices
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_VoiceDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
      - Voices
      summary: Create a voice
      description: Requires the administrator role on the parent podcast's network.
        Creates an empty voice record only; upload audio samples in a follow-up call
        to make it usable. Responds 201 with the new voice and a Location header.
        Any cloning needed for speech synthesis happens internally on first use, not
        here.
      operationId: createVoice
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VoiceCreateRequest'
      responses:
        '201':
          description: Voice created
          headers:
            Location:
              description: URL of the created voice
              schema:
                type: string
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{podcastId}/voices/{id}:
    get:
      tags:
      - Voices
      summary: Get a single voice
      description: The network is resolved from the parent podcast and the caller
        must hold the read_only role on it (403 otherwise, 404 if the podcast or voice
        does not exist).
      operationId: getVoice
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The voice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
      - Voices
      summary: Delete a voice
      description: Requires the administrator role on the parent podcast's network.
        Permanently removes the voice and responds 204. The delete is optimistic-locked,
        so a concurrent modification returns 409.
      operationId: deleteVoice
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: Voice deleted
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
      - Voices
      summary: Update a voice
      description: 'Requires the administrator role on the parent podcast''s network.
        Partial update: only the supplied fields change, and supplied info keys are
        merged onto the existing info map so keys set by other writers survive. Optimistic-locked,
        so a concurrent modification returns 409.'
      operationId: updateVoice
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VoiceUpdateRequest'
      responses:
        '200':
          description: The updated voice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{podcastId}/voices/{id}/preview:
    post:
      tags:
      - Voices
      summary: Synthesize a short preview of the cloned voice (returns MP3 audio)
      description: Requires the administrator role on the parent podcast's network.
        Synthesizes the supplied text (or a default sentence) using the voice and
        any provided ElevenLabs settings, cloning on demand if needed, and streams
        back MP3 audio synchronously. An upstream cloning or synthesis failure surfaces
        as 502.
      operationId: previewVoice
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewVoiceRequest'
      responses:
        '200':
          description: MP3 audio of the preview
          content:
            audio/mpeg:
              schema:
                type: string
                format: binary
        '404':
          description: Podcast or voice not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Voice cloning or speech synthesis failed upstream
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/podcasts/{podcastId}/voices/{id}/samples:
    post:
      tags:
      - Voices
      summary: Upload one or more audio samples for a voice (replaces the current
        set)
      description: Requires the administrator role on the parent podcast's network.
        Send the samples as multipart/form-data file parts; the uploaded set fully
        replaces any existing samples. Once samples are present the voice is ready
        to use, and cloning is performed internally on first use. Returns the voice
        with its updated sample set.
      operationId: uploadVoiceSamples
      parameters:
      - name: podcastId
        in: path
        required: true
        schema:
          type: string
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: array
              items:
                type: string
                format: binary
        required: true
      responses:
        '200':
          description: The voice with its updated sample set; now ready to use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceDto'
        '400':
          description: No files provided or an invalid filename
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/campaign-offer/{networkId}/{campaignId}/{token}:
    get:
      tags:
      - Public — Campaign offer
      summary: Resolve a campaign-offer token into the context needed to render the
        review screen
      description: 'Public endpoint authenticated solely by the opaque magic-link
        token in the path — no API key is required. Returns the curated details needed
        to render the offer review page: campaign, brand and product names, the podcast
        title, the invitation status and expiry, whether the recipient has passed
        email-code verification, and the podcast''s READY voices to choose from. Internal
        ids and the raw verification code are never exposed.'
      operationId: getCampaignOfferContext
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: token
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The curated offer context
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignOfferContextResponse'
        '404':
          description: Token is invalid or the invitation does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/campaign-offer/{networkId}/{campaignId}/{token}/accept:
    post:
      tags:
      - Public — Campaign offer
      summary: Accept the offer in a chosen voice; the host-voiced creative is generated
        server-side
      operationId: acceptCampaignOffer
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: token
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OfferAcceptRequest'
      responses:
        '200':
          description: Offer accepted; the host-voiced creative is created pending
            and audio synthesis runs asynchronously (creative_generation_started=true,
            creative_ready=false)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfferDecisionResponse'
        '400':
          description: Missing or unknown selected voice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Not verified, the offer isn't open, or the campaign isn't renderable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/campaign-offer/{networkId}/{campaignId}/{token}/preview-voice:
    post:
      tags:
      - Public — Campaign offer
      summary: Synthesize a short preview of one of the podcast's voices (returns
        MP3 audio)
      description: Public, token-authenticated action that synthesizes a short sample
        of a selected podcast voice so the recipient can hear it before accepting.
        Requires prior code verification, otherwise returns 409. Keyless callers are
        rate limited per token and per client IP (429 on excess); a valid API key
        for the campaign's network bypasses the limit. Synthesis runs against an upstream
        voice provider, which may return 502 on failure.
      operationId: previewCampaignOfferVoice
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: token
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewVoiceRequest'
      responses:
        '200':
          description: MP3 audio of the preview
          content:
            audio/mpeg:
              schema:
                type: string
                format: binary
        '400':
          description: Missing or unknown selected voice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Not verified, or the offer is no longer open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many preview requests
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Voice cloning or speech synthesis failed upstream
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/campaign-offer/{networkId}/{campaignId}/{token}/reject:
    post:
      tags:
      - Public — Campaign offer
      summary: Reject the offer
      description: 'Public, token-authenticated action that records the recipient''s
        decision to decline the offer: it marks both the offer and the invitation
        rejected and emits an offer.rejected webhook to the network. No creative is
        generated. Requires that the emailed code has already been verified and that
        the offer is still open, otherwise returns 409.'
      operationId: rejectCampaignOffer
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: token
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Offer rejected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfferDecisionResponse'
        '409':
          description: Not verified, or the offer isn't open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/campaign-offer/{networkId}/{campaignId}/{token}/send-code:
    post:
      tags:
      - Public — Campaign offer
      summary: Generate and email a fresh verification code to the podcast owner
      description: Public, token-authenticated action that generates a numeric verification
        code and emails it to the podcast's primary contact; the recipient must submit
        it to verify-code before accepting or rejecting the offer. The email send
        is best-effort — the code is stored even if delivery fails. Keyless callers
        are rate limited per token and per client IP; exceeding the limit returns
        429 with a Retry-After hint. A valid API key for the campaign's network bypasses
        the rate limit.
      operationId: sendCampaignOfferCode
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: token
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: A code was generated; the email send is best-effort
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResendCodeResponse'
        '409':
          description: The offer is no longer open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many code requests for this invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/campaign-offer/{networkId}/{campaignId}/{token}/verify-code:
    post:
      tags:
      - Public — Campaign offer
      summary: Submit a verification code
      description: Public, token-authenticated step that checks the numeric code previously
        emailed via send-code. Verification must succeed before the offer can be accepted
        or rejected. Attempts are rate limited per token and per client IP, and the
        code locks out after too many incorrect tries — both cases return 429, after
        which the recipient must request a fresh code. An expired or never-sent code
        returns 409.
      operationId: verifyCampaignOfferCode
      parameters:
      - name: networkId
        in: path
        required: true
        schema:
          type: string
      - name: campaignId
        in: path
        required: true
        schema:
          type: string
      - name: token
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyCodeRequest'
      responses:
        '200':
          description: The code was accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyCodeResponse'
        '400':
          description: Missing or incorrect code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: No code outstanding, or it has expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many attempts; request a new code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/invitations/{token}:
    get:
      tags:
      - Public — Invitations
      summary: Resolve an invitation token into the context needed to render the accept
        screen
      description: 'Public endpoint authenticated solely by the opaque magic-link
        token in the path — no API key is required. Returns the curated details needed
        to render the accept page: the network name, the assigned role, the invitation
        status and expiry, whether the recipient has passed email-code verification,
        and the invited email with its local part masked so the token alone does not
        reveal the full address.'
      operationId: getInvitationContext
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The curated invitation context (invitee email is masked)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationContextResponse'
        '404':
          description: Token is invalid or the invitation does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/invitations/{token}/accept:
    post:
      tags:
      - Public — Invitations
      summary: Accept the invitation (after code verification); the invitee is enrolled
        as a member
      description: Public, token-authenticated final step that accepts the invitation
        and enrolls the recipient as an active network member in the invited role.
        It resolves the invitee to a user record by the invited email (creating one
        if none exists) and keys the membership to it. Requires that the emailed code
        has already been verified, otherwise returns 409. The operation is idempotent
        for concurrent or repeated accepts and emits an invitation.accepted webhook
        to the network.
      operationId: acceptInvitation
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Accepted; the recipient is now a member
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvitationAcceptResponse'
        '404':
          description: Token is invalid or the invitation does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Not verified, the invitation is not pending, or the role is
            unsupported
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/invitations/{token}/send-code:
    post:
      tags:
      - Public — Invitations
      summary: Email a verification code to the invited address
      description: Public, token-authenticated action that generates a numeric verification
        code and emails it to the invited address, proving the recipient controls
        that mailbox; it must be submitted to verify-code before the invitation can
        be accepted. The email send is best-effort — the code is stored even if delivery
        fails. Keyless callers are rate limited per token and per client IP; exceeding
        the limit returns 429 with a Retry-After hint. A valid API key for the network
        bypasses the rate limit.
      operationId: sendInvitationCode
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: A code was generated; the email send is best-effort
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResendCodeResponse'
        '409':
          description: The invitation is not pending
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many code requests
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/invitations/{token}/verify-code:
    post:
      tags:
      - Public — Invitations
      summary: Submit the verification code
      description: Public, token-authenticated step that checks the numeric code previously
        emailed via send-code. Verification must succeed before the invitation can
        be accepted. Attempts are rate limited per token and per client IP, and the
        code locks out after too many incorrect tries — both cases return 429, after
        which the recipient must request a fresh code. An expired or never-sent code
        returns 409.
      operationId: verifyInvitationCode
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyCodeRequest'
      responses:
        '200':
          description: The code was accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyCodeResponse'
        '400':
          description: Missing or incorrect code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: No code outstanding, or it has expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many attempts; request a new code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/opt-in/{token}:
    get:
      tags:
      - Public — Opt-in
      summary: Resolve an opt-in invitation token into the context needed to render
        the screen
      description: 'Public endpoint authenticated solely by the opaque magic-link
        token in the path — no API key is required. Returns the curated details needed
        to render the opt-in page: podcast title, network name, invitation status
        and expiry, and whether the recipient has already passed email-code verification
        (plus the current code''s expiry, if one is outstanding). Internal ids, the
        raw verification code, and inviter identity are never exposed.'
      operationId: getOptInContext
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The curated invitation context
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptInContextResponse'
        '404':
          description: Token is invalid or the invitation does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/opt-in/{token}/complete:
    post:
      tags:
      - Public — Opt-in
      summary: 'Submit the opt-in registration: opt the podcast in and record advertiser
        preferences'
      description: 'Public, token-authenticated final step that records the recipient''s
        opt-in: it flips the podcast to opted-in, saves the contact details, blocked
        ad categories, and any supplied podcast metadata, and optionally links a previously
        created READY voice as the podcast''s primary voice. Requires that the emailed
        code has already been verified, otherwise returns 409. The call is idempotent
        on retry (the original completion timestamp is preserved) and emits an optin.completed
        webhook to the network.'
      operationId: completeOptIn
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OptInCompleteRequest'
      responses:
        '200':
          description: Opt-in completed; the podcast is now opted in
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptInCompleteResponse'
        '400':
          description: Missing first_name/email/role or an invalid email
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Not verified, or the invitation is no longer open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/opt-in/{token}/send-code:
    post:
      tags:
      - Public — Opt-in
      summary: Generate and email a fresh verification code to the podcast owner
      description: Public, token-authenticated action that generates a numeric verification
        code and emails it to the podcast's primary contact; the recipient must later
        submit it to verify-code before completing the opt-in. Code sending is an
        explicit POST (never a side-effecting GET). The email send is best-effort
        — the code is stored even if delivery fails. Keyless callers are rate limited
        per token and per client IP; exceeding the limit returns 429 with a Retry-After
        hint. A valid API key for the podcast's network bypasses the rate limit.
      operationId: sendOptInCode
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: A code was generated; the email send is best-effort
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResendCodeResponse'
        '409':
          description: The invitation is no longer open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many code requests for this invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/opt-in/{token}/transcribe:
    post:
      tags:
      - Public — Opt-in
      summary: Transcribe an audio clip with speaker diarization (auto-crop helper)
      description: Public, token-authenticated helper that transcribes an uploaded
        audio clip (up to 25 MB) into diarized segments, used to assist cropping host
        audio for voice samples. Requires prior code verification, otherwise returns
        409. Keyless callers are rate limited per token and per client IP (429 on
        excess); a valid API key for the podcast's network bypasses the limit. The
        transcription runs against an upstream provider, which may return 502 on failure.
      operationId: transcribeOptInAudio
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  nullable: true
                language:
                  type: string
                  nullable: true
            encoding:
              file:
                contentType: application/octet-stream
                explode: false
        required: true
      responses:
        '200':
          description: The diarized transcript
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptionResponse'
        '400':
          description: Missing/empty file or larger than 25 MB
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Not verified, or the invitation is no longer open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Transcription failed upstream
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many transcription requests
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/opt-in/{token}/verify-code:
    post:
      tags:
      - Public — Opt-in
      summary: Submit a verification code
      description: Public, token-authenticated step that checks the numeric code previously
        emailed via send-code. Verification must succeed before the opt-in can be
        completed. Attempts are rate limited per token and per client IP, and the
        code itself locks out after too many incorrect tries — both cases return 429,
        after which the recipient must request a fresh code. An expired or never-sent
        code returns 409.
      operationId: verifyOptInCode
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyCodeRequest'
      responses:
        '200':
          description: The code was accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyCodeResponse'
        '400':
          description: Missing or incorrect code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: No code outstanding, or it has expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many attempts; request a new code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/opt-in/{token}/voices:
    post:
      tags:
      - Public — Opt-in voice
      summary: Create a voice to collect the host's audio samples
      description: Public, token-authenticated action that creates a new voice on
        the opt-in's podcast to collect the host's audio samples. The voice starts
        pending — upload samples and finalize it before it can be used or linked at
        opt-in completion. Requires prior code verification, otherwise returns 409.
        The created voice's URL is returned in the Location header.
      operationId: createOptInVoice
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VoiceCreateRequest'
      responses:
        '201':
          description: Voice created (pending until samples are uploaded)
          headers:
            Location:
              description: URL of the voice
              schema:
                type: string
        '409':
          description: Not verified, or the invitation is no longer open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/opt-in/{token}/voices/{voiceId}/finalize:
    post:
      tags:
      - Public — Opt-in voice
      summary: Mark the voice ready once its samples are uploaded
      description: Public, token-authenticated action that marks the voice READY once
        it has at least one uploaded sample; only a READY voice may be linked as the
        podcast's primary voice at opt-in completion. Returns 400 if no samples have
        been uploaded yet, and requires prior code verification, otherwise returns
        409.
      operationId: finalizeOptInVoice
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      - name: voiceId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: The voice, now ready to use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceDto'
        '400':
          description: The voice has no uploaded samples
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Not verified, or the invitation is no longer open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/opt-in/{token}/voices/{voiceId}/preview:
    post:
      tags:
      - Public — Opt-in voice
      summary: Synthesize a short preview of the cloned voice (returns MP3 audio)
      description: Public, token-authenticated action that synthesizes a short sample
        of the cloned voice so the host can hear how it sounds. Requires prior code
        verification, otherwise returns 409. Keyless callers are rate limited per
        token and per client IP (429 on excess); a valid API key for the podcast's
        network bypasses the limit. Cloning and synthesis run against an upstream
        provider, which may return 502 on failure.
      operationId: previewOptInVoice
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      - name: voiceId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewVoiceRequest'
      responses:
        '200':
          description: MP3 audio of the preview
          content:
            audio/mpeg:
              schema:
                type: string
                format: binary
        '409':
          description: Not verified, or the invitation is no longer open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too many preview requests
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: Voice cloning or speech synthesis failed upstream
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/public/opt-in/{token}/voices/{voiceId}/samples:
    post:
      tags:
      - Public — Opt-in voice
      summary: Upload audio samples for the voice (replaces the current set)
      description: Public, token-authenticated action that uploads one or more audio
        files as the voice's samples, replacing any previously uploaded set. The voice
        stays pending until it is finalized. Requires prior code verification, otherwise
        returns 409.
      operationId: uploadOptInVoiceSamples
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      - name: voiceId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: array
              items:
                type: string
                format: binary
        required: true
      responses:
        '200':
          description: The voice with its updated sample set
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceDto'
        '400':
          description: No files provided or an invalid filename
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Not verified, or the invitation is no longer open
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key is not authorized for this resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The requested resource was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/users:
    get:
      tags:
      - Users
      summary: List all users (privileged)
      description: Returns every user account on the platform. Requires the super_user
        role. Results are cursor-paginated and ordered by user id; pass the returned
        cursor to fetch the next page.
      operationId: listUsers
      parameters:
      - name: limit
        in: query
        explode: false
        schema:
          type: integer
          format: int32
          nullable: true
        description: Maximum number of items per page (default 50, maximum 200).
      - name: cursor
        in: query
        explode: false
        schema:
          type: string
          nullable: true
        description: Opaque pagination token. Pass the next_cursor from a previous
          response to fetch the next page; do not construct or modify it.
      responses:
        '403':
          description: Listing users requires the super_user role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '200':
          description: List all users (privileged)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResponse_UserDto_'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          description: Invalid request (e.g. a malformed pagination cursor)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/users/{id}:
    get:
      tags:
      - Users
      summary: Get a user by id
      description: Returns a single user. A caller may read their own user record;
        a super_user may read any user. Any other read is forbidden. Responds with
        404 if the user does not exist.
      operationId: getUser
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '403':
          description: Reading another user requires the super_user role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '200':
          description: Get a user by id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserDto'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
      - Users
      summary: Update a user's mutable fields (privileged)
      description: Partially updates a user's display name, preferred language, system-admin
        flag, or status; only the fields you supply are changed and the rest are left
        intact. Requires the super_user role. Demoting the last remaining system admin
        is rejected with 409. The write is optimistically locked, so a concurrent
        modification returns 409.
      operationId: updateUser
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserUpdateRequest'
      responses:
        '200':
          description: The updated user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserDto'
        '400':
          description: An invalid status value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Updating a user requires the super_user role
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Version conflict, or demoting the last system admin
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    AdVariantDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier of the ad-copy variant.
          example: 2Bx6Rk9LtN5mZq8Wv4G
        name:
          type: string
          description: Human-readable name of the variant.
          example: Holiday promo
        adCopy:
          type: string
          description: The ad copy text for this variant.
          example: This episode is brought to you by Acme...
      description: Public v1 representation of an ad-copy variant within a campaign.
    AdVariantInput:
      type: object
      properties:
        id:
          type: string
          description: Identifier of the variant. Optional on create; the server generates
            one when omitted.
          nullable: true
          example: 2Bx6Rk9LtN5mZq8Wv4G
        name:
          type: string
          description: Human-readable name of the variant.
          nullable: true
          example: Holiday promo
        ad_copy:
          type: string
          description: The ad copy text for this variant.
          nullable: true
          example: This episode is brought to you by Acme...
      description: One ad-copy variant supplied on a campaign create/update. `id`
        is optional on create (the server generates one when omitted); `name` and
        `ad_copy` carry the variant content.
    AdswizzLinkDto:
      type: object
      properties:
        campaignId:
          type: string
          description: Identifier of the linked campaign in Adswizz
          example: az_8421907
      description: Link to the campaign in Adswizz; present only when platform is
        adswizz
    AdswizzLinkInput:
      type: object
      properties:
        campaign_id:
          type: string
          description: Identifier of the linked campaign in Adswizz
          nullable: true
          example: az_8421907
      description: Link to an Adswizz campaign (sets platform context; merged onto
        info.adswizz)
    AdvertiserCreateRequest:
      type: object
      properties:
        name:
          type: string
          description: Name of the advertiser (required)
          nullable: true
          example: Acme Corp
        email:
          type: string
          description: Primary contact email for the advertiser (required)
          nullable: true
          example: ads@example.com
        description:
          type: string
          description: Free-text description of the advertiser
          nullable: true
          example: Consumer electronics manufacturer
        website:
          type: string
          description: Advertiser's website URL
          nullable: true
          example: https://example.com
      description: Request body to create an advertiser. `name` and `email` are required.
        The advertiser starts `pending`; its lifecycle status is platform-owned and
        not accepted here.
    AdvertiserDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier of the advertiser
          example: 3Kd7Np5RvM9qWx2T8yZ6
        createdAt:
          type: string
          description: Timestamp when the advertiser was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: Timestamp when the advertiser was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
        name:
          type: string
          description: Name of the advertiser
          example: Acme Corp
        description:
          type: string
          description: Free-text description of the advertiser
          nullable: true
          example: Consumer electronics manufacturer
        website:
          type: string
          description: Advertiser's website URL
          nullable: true
          example: https://example.com
        email:
          type: string
          description: Primary contact email for the advertiser
          example: ads@example.com
        status:
          type: string
          description: Advertiser status
          enum:
          - pending
          - active
          - suspended
          - archived
      description: An advertiser that runs campaigns within a network. Referenced
        by `campaign.advertiser_id`.
    AdvertiserSummaryDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier of the advertiser
          example: 3Kd7Np5RvM9qWx2T8yZ6
        name:
          type: string
          description: Name of the advertiser
          example: Acme Corp
      description: The campaign's advertiser; present only when ?expand=advertiser
        was requested
    AdvertiserUpdateRequest:
      type: object
      properties:
        name:
          type: string
          description: Name of the advertiser
          nullable: true
          example: Acme Corp
        email:
          type: string
          description: Primary contact email for the advertiser
          nullable: true
          example: ads@example.com
        description:
          type: string
          description: Free-text description of the advertiser
          nullable: true
          example: Consumer electronics manufacturer
        website:
          type: string
          description: Advertiser's website URL
          nullable: true
          example: https://example.com
      description: Request body to update an advertiser's mutable business fields.
        Every field is optional; only provided fields change (optimistic-locked).
        The lifecycle status is platform-owned and not accepted.
    ApiKeyCreateRequest:
      type: object
      properties:
        name:
          type: string
          description: Human-readable label for the key
          nullable: true
          example: billing-sync
        scopes:
          type: array
          description: Per-network access to grant the key; each scope must be a subset
            of the caller's own access. Provide either this or super_user.
          nullable: true
          items:
            $ref: '#/components/schemas/ApiKeyScopeInput'
        super_user:
          type: boolean
          description: Request cross-network (super-user) access; allowed only when
            the caller is itself super-user. When true, scopes must be empty.
          nullable: true
          example: false
        expires_at:
          type: string
          description: Optional ISO-8601 instant in the future when the key should
            expire; omit for a key that never expires
          nullable: true
          example: 2026-01-15T09:30:00Z
      description: Request to create an API key. The key is always owned by the calling
        principal (owner is never client-supplied). `scopes` must each be a subset
        of the principal's own access; `super_user` may be `true` only when the caller
        is itself super-user. A key must request either `super_user=true` or at least
        one scope. `expires_at`, if given, is an ISO-8601 instant in the future. Self-service
        mints live keys only.
    ApiKeyCreateResponse:
      type: object
      properties:
        key:
          description: The created key
          allOf:
          - $ref: '#/components/schemas/ApiKeyDto'
        token:
          type: string
          description: 'The plaintext API token. Shown ONLY on create — store it now;
            it cannot be retrieved again. Send it as `Authorization: Bearer <token>`.'
          example: adb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
      description: 'The response to creating a key: the full ApiKeyDto plus the plaintext
        `token`, which is shown *only* here. Subsequent reads (get/list) never return
        the token, so the client must store it now; it cannot be recovered later.'
    ApiKeyDto:
      type: object
      properties:
        id:
          type: string
          description: The key id
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        name:
          type: string
          description: Human-readable label for the key
          example: billing-sync
        prefix:
          type: string
          description: Token prefix (e.g. adb_live)
          example: adb_live
        displayLast4:
          type: string
          description: Last 4 characters of the token, for identification in a UI
          example: a1b2
        superUser:
          type: boolean
          description: True if the key has cross-network (super-user) access; then
            scopes is empty
          example: false
        status:
          type: string
          description: Key status
          example: active
          enum:
          - active
          - revoked
          - expired
        scopes:
          type: array
          description: Per-network access the key holds
          items:
            $ref: '#/components/schemas/KeyScopeDto'
        createdAt:
          type: string
          description: When the key was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        lastUsedAt:
          type: string
          description: When the key was last used to authenticate a request, if ever
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        expiresAt:
          type: string
          description: When the key expires, if an expiry was set; null means it never
            expires
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
      description: The created key
    ApiKeyListResponse:
      type: object
      properties:
        items:
          type: array
          description: The keys owned by the principal (or by the requested owner,
            for super-users)
          items:
            $ref: '#/components/schemas/ApiKeyDto'
      description: 'A list of API keys. Not cursor-paginated: keys-per-owner is small,
        so all are returned (a deliberate exception to the pagination convention).'
    ApiKeyScopeInput:
      type: object
      properties:
        network_id:
          type: string
          description: The network this scope grants access to
          nullable: true
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        role:
          type: string
          description: The role to grant on this network; must be a subset of the
            caller's own role there
          nullable: true
          example: read_only
          enum:
          - read_only
          - administrator
      description: 'A requested per-network scope on a new key: a network id and the
        role to grant there. The role must be `read_only` or `administrator` (use
        the `super_user` flag, not a scope role, for cross-network access) and must
        be a subset of what the calling key already holds on that network.'
    CampaignCreateRequest:
      type: object
      properties:
        name:
          type: string
          description: Human-readable name of the campaign (required)
          nullable: true
          example: Spring Launch 2026
        advertiser_id:
          type: string
          description: Identifier of the advertiser this campaign runs for
          nullable: true
          example: 3Kd7Np5RvM9qWx2T8yZ6
        brand_name:
          type: string
          description: Brand being advertised in the campaign
          nullable: true
          example: Acme Corp
        product_name:
          type: string
          description: Product being advertised in the campaign
          nullable: true
          example: Acme Pro Headphones
        product_category:
          type: string
          description: Category the advertised product belongs to
          nullable: true
          example: Consumer Electronics
        categories:
          type: array
          description: Content categories the campaign is eligible to run against
          nullable: true
          items:
            type: string
        description:
          type: string
          description: Free-text description of the campaign
          nullable: true
          example: Promoting the Spring 2026 product line
        start_date:
          type: string
          description: Date the campaign is scheduled to start running
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        end_date:
          type: string
          description: Date the campaign is scheduled to stop running
          format: date-time
          nullable: true
          example: 2026-03-15T09:30:00Z
        budget:
          type: number
          description: Total campaign budget in US dollars
          format: double
          nullable: true
          example: 50000.0
        cpm:
          type: number
          description: Cost per thousand impressions (CPM) in US dollars
          format: double
          nullable: true
          example: 25.0
        ad_variants:
          type: array
          description: Ad variants (copy and configuration) to define for the campaign
          nullable: true
          items:
            $ref: '#/components/schemas/AdVariantInput'
        platform:
          type: string
          description: Ad-serving platform the campaign targets (defaults to adbridge)
          nullable: true
          example: adswizz
          enum:
          - adbridge
          - adswizz
          - megaphone
        adswizz:
          description: Link to an Adswizz campaign (sets platform context; merged
            onto info.adswizz)
          nullable: true
          allOf:
          - $ref: '#/components/schemas/AdswizzLinkInput'
        targeting:
          type: object
          additionalProperties:
            type: object
          description: Free-form targeting parameters (open, platform-defined)
          nullable: true
        tracking:
          type: object
          additionalProperties:
            type: object
          description: Free-form tracking configuration (open, platform-defined)
          nullable: true
      description: Request body to create a campaign. The network comes from the path.
        `name` is required; the remaining business fields may be filled in here or
        later (they must all be present before an offer can be sent --- see the offer
        flow). Lifecycle (`status`) and the `version`/raw `info` map are platform-owned
        and never accepted here.
    CampaignCsvFileDto:
      type: object
      properties:
        filename:
          type: string
          description: Original filename of the uploaded CSV
          example: spring-targets.csv
        url:
          type: string
          description: URL where the stored CSV can be downloaded
          example: https://cdn.example.com/campaigns/7Hb2Kp9QvL3mNx0R8tZ4/targets.csv
        size:
          type: integer
          description: Size of the CSV file in bytes
          format: int64
          example: 2048
        uploadedAt:
          type: string
          description: Timestamp when the CSV was uploaded (ISO-8601)
          example: 2026-01-15T09:30:00Z
      description: CSV file listing the campaign's targeting, if uploaded
    CampaignDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier of the campaign
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        networkId:
          type: string
          description: Identifier of the network that owns the campaign
          example: 9Fc4Lm2RtN6pQx1S7wZ3
        createdAt:
          type: string
          description: Timestamp when the campaign was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: Timestamp when the campaign was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
        platform:
          type: string
          description: Ad-serving platform the campaign targets
          example: adswizz
          enum:
          - adbridge
          - adswizz
          - megaphone
        adswizz:
          description: Link to the campaign in Adswizz; present only when platform
            is adswizz
          nullable: true
          allOf:
          - $ref: '#/components/schemas/AdswizzLinkDto'
        advertiserId:
          type: string
          description: Identifier of the advertiser this campaign runs for, if assigned
          nullable: true
          example: 3Kd7Np5RvM9qWx2T8yZ6
        advertiser:
          description: The campaign's advertiser; present only when `?expand=advertiser`
            was requested.
          nullable: true
          allOf:
          - $ref: '#/components/schemas/AdvertiserSummaryDto'
        name:
          type: string
          description: Human-readable name of the campaign
          example: Spring Launch 2026
        brandName:
          type: string
          description: Brand being advertised in the campaign
          example: Acme Corp
        productName:
          type: string
          description: Product being advertised in the campaign
          example: Acme Pro Headphones
        productCategory:
          type: string
          description: Category the advertised product belongs to
          example: Consumer Electronics
        startDate:
          type: string
          description: Date the campaign is scheduled to start running
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        endDate:
          type: string
          description: Date the campaign is scheduled to stop running
          format: date-time
          nullable: true
          example: 2026-03-15T09:30:00Z
        budget:
          type: number
          description: Total campaign budget in US dollars
          format: double
          nullable: true
          example: 50000.0
        cpm:
          type: number
          description: Cost per thousand impressions (CPM) in US dollars
          format: double
          nullable: true
          example: 25.0
        categories:
          type: array
          description: Content categories the campaign is eligible to run against
          items:
            type: string
        adVariants:
          type: array
          description: Ad variants (copy and configuration) defined for the campaign
          items:
            $ref: '#/components/schemas/AdVariantDto'
        excludedPodcastIds:
          type: array
          description: Identifiers of podcasts explicitly excluded from this campaign
          items:
            type: string
        targeting:
          type: object
          additionalProperties:
            type: object
          description: Free-form targeting parameters for the campaign (e.g. geo).
            Structure is open and platform-defined.
          nullable: true
        tracking:
          type: object
          additionalProperties:
            type: object
          description: Free-form tracking configuration for the campaign (e.g. a tracking
            pixel). Structure is open and platform-defined.
          nullable: true
        targetingCsv:
          description: CSV file listing the campaign's targeting, if uploaded
          nullable: true
          allOf:
          - $ref: '#/components/schemas/CampaignCsvFileDto'
        trackingCsv:
          description: CSV file listing the campaign's tracking, if uploaded
          nullable: true
          allOf:
          - $ref: '#/components/schemas/CampaignCsvFileDto'
        status:
          type: string
          description: Campaign lifecycle status
          enum:
          - pending
          - active
          - completed
          - cancelled
          - archived
      description: An advertising campaign belonging to a network.
    CampaignOfferContextResponse:
      type: object
      properties:
        campaignName:
          type: string
          description: Name of the campaign the offer belongs to
          example: Spring Sneaker Launch
        brandName:
          type: string
          description: Brand advertising in the campaign
          example: Acme Footwear
        productName:
          type: string
          description: Product being advertised in the campaign
          example: Acme Trail Runner 2
        podcastTitle:
          type: string
          description: Title of the podcast the offer is for
          example: The Daily Tech Roundup
        status:
          type: string
          description: Lifecycle status of the campaign-offer invitation behind the
            magic link
          example: pending
          enum:
          - pending
          - started
          - accepted
          - rejected
          - expired
          - cancelled
        expiresAt:
          type: string
          description: When the magic-link offer invitation expires and can no longer
            be acted on
          format: date-time
          example: 2026-01-15T09:30:00Z
        verified:
          type: boolean
          description: Whether the recipient has already passed verification-code
            verification for this magic link
          example: false
        codeExpiresAt:
          type: string
          description: When the current verification code expires, if one is outstanding;
            null when no code has been sent
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        voices:
          type: array
          description: The podcast's ready voices the recipient can choose from when
            accepting the offer
          items:
            $ref: '#/components/schemas/VoiceSummaryDto'
      description: 'The curated, recipient-facing view of a campaign offer --- enough
        for a client to render the review/accept screen. Deliberately minimal: no
        internal ids, no inviter identity, no raw verification code, no persistence
        internals, no creative/storage internals. Returned from the public token surface.
        The `voices` array lists the podcast''s ready voices the recipient can pick
        to accept the offer.'
    CampaignUpdateRequest:
      type: object
      properties:
        name:
          type: string
          description: Human-readable name of the campaign
          nullable: true
          example: Spring Launch 2026
        advertiser_id:
          type: string
          description: Identifier of the advertiser this campaign runs for
          nullable: true
          example: 3Kd7Np5RvM9qWx2T8yZ6
        brand_name:
          type: string
          description: Brand being advertised in the campaign
          nullable: true
          example: Acme Corp
        product_name:
          type: string
          description: Product being advertised in the campaign
          nullable: true
          example: Acme Pro Headphones
        product_category:
          type: string
          description: Category the advertised product belongs to
          nullable: true
          example: Consumer Electronics
        categories:
          type: array
          description: Content categories the campaign is eligible to run against
          nullable: true
          items:
            type: string
        description:
          type: string
          description: Free-text description of the campaign
          nullable: true
          example: Promoting the Spring 2026 product line
        start_date:
          type: string
          description: Date the campaign is scheduled to start running
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        end_date:
          type: string
          description: Date the campaign is scheduled to stop running
          format: date-time
          nullable: true
          example: 2026-03-15T09:30:00Z
        budget:
          type: number
          description: Total campaign budget in US dollars
          format: double
          nullable: true
          example: 50000.0
        cpm:
          type: number
          description: Cost per thousand impressions (CPM) in US dollars
          format: double
          nullable: true
          example: 25.0
        ad_variants:
          type: array
          description: Ad variants (copy and configuration) for the campaign
          nullable: true
          items:
            $ref: '#/components/schemas/AdVariantInput'
        platform:
          type: string
          description: Ad-serving platform the campaign targets
          nullable: true
          example: adswizz
          enum:
          - adbridge
          - adswizz
          - megaphone
        adswizz:
          description: Link to an Adswizz campaign (merged onto info.adswizz)
          nullable: true
          allOf:
          - $ref: '#/components/schemas/AdswizzLinkInput'
        targeting:
          type: object
          additionalProperties:
            type: object
          description: Free-form targeting parameters (open, platform-defined); replaces
            the stored value
          nullable: true
        tracking:
          type: object
          additionalProperties:
            type: object
          description: Free-form tracking configuration (open, platform-defined);
            replaces the stored value
          nullable: true
      description: Request body to update a campaign's mutable business fields. Every
        field is optional; only the provided fields change (optimistic-locked). `status`
        is read-only here --- use the `archive` action for the only client-driven
        transition. The `version`/raw `info` map are never accepted.
    ContactDto:
      type: object
      properties:
        email:
          type: string
          description: Email address of the contact
          example: ops@example.com
        firstName:
          type: string
          description: First name of the contact
          example: Jane
        lastName:
          type: string
          description: Last name of the contact
          example: Doe
        role:
          type: string
          description: Optional free-form role or title of the contact
          nullable: true
          example: Head of Partnerships
      description: Primary business contact for the network
    ContactInput:
      type: object
      properties:
        email:
          type: string
          description: Email address of the contact (required)
          nullable: true
          example: ops@example.com
        first_name:
          type: string
          description: First name of the contact (required)
          nullable: true
          example: Jane
        last_name:
          type: string
          description: Last name of the contact (required)
          nullable: true
          example: Doe
        role:
          type: string
          description: Optional free-form role or title of the contact
          nullable: true
          example: Head of Partnerships
      description: Primary business contact for the network (required)
    CreativeCreateRequest:
      type: object
      properties:
        campaign_id:
          type: string
          description: Identifier of the campaign this creative is generated for.
          nullable: true
          example: 9Wq4Zx7NpR2kLb6Hm0D
        voice_id:
          type: string
          description: Identifier of the voice clone used to synthesize the audio.
            Defaults to the podcast's primary voice when omitted.
          nullable: true
          example: 8Mp3Cq7VtL2nXw5Rk9H
        offer_id:
          type: string
          description: Identifier of the offer this creative fulfills, when creating
            from an offer.
          nullable: true
          example: 5Tn1Yv8QcM4dKw3Pr7E
        variant_id:
          type: string
          description: Identifier of the campaign ad-copy variant to read. Its copy
            is used when ad_copy is omitted.
          nullable: true
          example: 2Bx6Rk9LtN5mZq8Wv4G
        ad_copy:
          type: string
          description: Optional ad copy override. When omitted the copy is taken from
            the campaign variant or the campaign's default copy.
          nullable: true
          example: This episode is brought to you by Acme...
        language_code:
          type: string
          description: Optional BCP-47 language code to synthesize the audio in.
          nullable: true
          example: en-US
      description: 'Request to create a creative and generate its host-voiced audio.
        The platform allocates the voice clone, synthesizes, and stores the audio
        behind the scenes --- the client just states what ad to make and for which
        campaign. `voice_id` defaults to the podcast''s primary voice when omitted.
        `ad_copy` is an optional override: when omitted the copy is taken from the
        campaign variant (`variant_id`) or the campaign''s default copy, so the client
        need not re-send text it read.'
    CreativeDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier of the creative.
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        createdAt:
          type: string
          description: Timestamp when the creative was created.
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: Timestamp when the creative was last updated.
          format: date-time
          example: 2026-01-15T09:30:00Z
        podcastId:
          type: string
          description: Identifier of the podcast this creative belongs to.
          example: 3Fk8Lm2QrT5wYx9Bn1C
        campaignId:
          type: string
          description: Identifier of the campaign this creative was generated for.
          example: 9Wq4Zx7NpR2kLb6Hm0D
        offerId:
          type: string
          description: Identifier of the offer this creative fulfills, when the creative
            was made from an offer.
          nullable: true
          example: 5Tn1Yv8QcM4dKw3Pr7E
        variantId:
          type: string
          description: Identifier of the campaign ad-copy variant whose text the host
            read, when applicable.
          nullable: true
          example: 2Bx6Rk9LtN5mZq8Wv4G
        voiceId:
          type: string
          description: Identifier of the voice clone used to synthesize the ad audio.
          example: 8Mp3Cq7VtL2nXw5Rk9H
        status:
          type: string
          description: 'Creative lifecycle status. Audio generation is asynchronous:
            a creative starts `pending`, then becomes `ready` (audio_url populated)
            or `failed` (failure_reason populated). Poll this resource, or subscribe
            to the creative.ready / creative.failed webhook.'
          enum:
          - pending
          - ready
          - failed
          - approved
          - rejected
          - published
          - archived
        adCopy:
          type: string
          description: The ad copy the host read for this creative.
          example: This episode is brought to you by Acme...
        audioUrl:
          type: string
          description: Playable CDN URL of the generated ad audio, populated once
            the creative is ready.
          nullable: true
          example: https://example.com/audio/creative.mp3
        failureReason:
          type: string
          description: Client-safe reason audio generation failed, present only when
            status is failed. Recover by calling the creative regenerate action.
          nullable: true
          example: Voice synthesis timed out; please try again.
      description: 'A creative: an ad rendered in a podcast''s host voice.'
    CreativeRegenerateRequest:
      type: object
      properties:
        language_code:
          type: string
          description: Optional BCP-47 language code to synthesize the audio in. When
            omitted the creative's existing language is reused.
          nullable: true
          example: en-US
      description: Optional body for regenerating a creative's audio. Everything else
        (voice, ad copy, settings) is reused from the existing creative; only the
        synthesis language may be overridden.
    InvitationAcceptResponse:
      type: object
      properties:
        status:
          type: string
          description: Always "accepted" on success
          example: accepted
        networkId:
          type: string
          description: The network the recipient joined
          example: 9Kp3Lm2QvX7nRt0B8wZ5
        role:
          type: string
          description: The membership role granted
          example: viewer
          enum:
          - admin
          - viewer
        userId:
          type: string
          description: The user id the membership is keyed to (existing user, or a
            newly created one)
          example: 7Hb2Kp9QvL3mNx0R8tZ4
      description: 'Result of accepting a network user invitation: the recipient is
        now a member of the network.'
    InvitationContextResponse:
      type: object
      properties:
        networkName:
          type: string
          description: Display name of the network the recipient is invited to
          example: Acme Podcast Network
        email:
          type: string
          description: Email address the invitation was sent to
          example: member@example.com
        role:
          type: string
          description: Role to be granted on acceptance
          example: viewer
          enum:
          - admin
          - viewer
        status:
          type: string
          description: Invitation status
          example: pending
          enum:
          - pending
          - accepted
          - expired
          - revoked
        verified:
          type: boolean
          description: Whether the recipient has already passed code verification
          example: false
        expiresAt:
          type: string
          description: When the invitation expires and can no longer be accepted
          format: date-time
          example: 2026-01-15T09:30:00Z
      description: The curated, recipient-facing view of a network user invitation
        --- enough for a client to render the "you've been invited to X as Y" screen.
        `email` is the invitee's own address (echoed back), not third-party PII. No
        token, inviter id, or persistence internals.
    KeyScopeDto:
      type: object
      properties:
        networkId:
          type: string
          description: The network this scope grants access to
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        role:
          type: string
          description: Role held on this network
          example: read_only
          enum:
          - read_only
          - administrator
        networkUrl:
          type: string
          description: Link to this network, so the key can navigate without constructing
            ids
          example: https://api.adbridge.ai/external/v1/networks/7Hb2Kp9QvL3mNx0R8tZ4
      description: A network the calling API key can access, and the role it holds
        there.
    MeResponse:
      type: object
      properties:
        keyName:
          type: string
          description: Human-readable name of the calling API key
          example: Acme integration key
        superUser:
          type: boolean
          description: True if the key has cross-network (super-user) access; then
            scopes is empty
          example: false
        scopes:
          type: array
          description: Per-network scopes granted to the key (empty for a super-user
            key)
          items:
            $ref: '#/components/schemas/KeyScopeDto'
        links:
          type: object
          additionalProperties:
            type: string
          description: Top-level collection links the key can navigate from
      description: 'Describes the calling API key: its name, whether it has cross-network
        access, and its scopes.'
    MemberAddRequest:
      type: object
      properties:
        user_id:
          type: string
          description: Id of an existing user to add to the network
          nullable: true
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        role:
          type: string
          description: Network membership role to grant
          nullable: true
          example: viewer
          enum:
          - admin
          - viewer
      description: Request to add an existing user to a network. `user_id` must be
        an existing user; `role` is the membership role to grant (`admin` or `viewer`).
        The added member is immediately `active` --- this is a mechanical role assignment,
        not an email invitation.
    MemberDto:
      type: object
      properties:
        id:
          type: string
          description: The member's user id
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        role:
          type: string
          description: Network membership role
          example: admin
          enum:
          - admin
          - viewer
        status:
          type: string
          description: Membership status
          example: active
          enum:
          - active
          - pending
          - suspended
        invitedAt:
          type: string
          description: When the member was invited to the network, if applicable
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        acceptedAt:
          type: string
          description: When the member accepted their invitation, if applicable
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        createdAt:
          type: string
          description: When the membership was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: When the membership was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
        user:
          description: The member's user profile; present only when `?expand=user`
            was requested.
          nullable: true
          allOf:
          - $ref: '#/components/schemas/UserSummaryDto'
      description: A network membership, as seen by an API client. `id` is the member's
        user id. The inviter identity, free-form info, and persistence version are
        intentionally not exposed.
    MemberUpdateRequest:
      type: object
      properties:
        role:
          type: string
          description: New membership role; omit to leave unchanged
          nullable: true
          example: admin
          enum:
          - admin
          - viewer
        status:
          type: string
          description: New membership status; omit to leave unchanged
          nullable: true
          example: active
          enum:
          - active
          - pending
          - suspended
      description: Request to change a member's role and/or status. Both fields are
        optional; omit a field to leave it unchanged. `role` ∈ `admin|viewer`; `status`
        ∈ `active|pending|suspended`.
    NetworkCreateRequest:
      type: object
      properties:
        name:
          type: string
          description: Display name of the network (required)
          nullable: true
          example: Acme Podcast Network
        email:
          type: string
          description: Primary email address for the network (required)
          nullable: true
          example: ops@example.com
        description:
          type: string
          description: Optional free-form description of the network
          nullable: true
          example: A network of independent technology podcasts
        website:
          type: string
          description: Optional public website for the network
          nullable: true
          example: https://example.com
        default_email_language:
          type: string
          description: Default language code used for emails sent to the network
          nullable: true
          example: en
        primary_contact:
          description: Primary business contact for the network (required)
          nullable: true
          allOf:
          - $ref: '#/components/schemas/ContactInput'
        contacts:
          type: array
          description: Additional business contacts for the network
          nullable: true
          items:
            $ref: '#/components/schemas/ContactInput'
      description: Request body to create a network. Creating a network is a privileged
        operation (super-user only). `name`, `email` and `primary_contact` are required.
        Internal fields are never accepted.
    NetworkDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier of the network
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        createdAt:
          type: string
          description: When the network was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: When the network was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
        name:
          type: string
          description: Display name of the network
          example: Acme Podcast Network
        description:
          type: string
          description: Optional free-form description of the network
          nullable: true
          example: A network of independent technology podcasts
        website:
          type: string
          description: Optional public website for the network
          nullable: true
          example: https://example.com
        email:
          type: string
          description: Primary email address for the network
          example: ops@example.com
        defaultEmailLanguage:
          type: string
          description: Default language code used for emails sent to the network
          nullable: true
          example: en
        primaryContact:
          description: Primary business contact for the network
          allOf:
          - $ref: '#/components/schemas/ContactDto'
        contacts:
          type: array
          description: Additional business contacts for the network
          items:
            $ref: '#/components/schemas/ContactDto'
      description: A network (organization) that owns podcasts and campaigns.
    NetworkStatsResponse:
      type: object
      properties:
        totalPodcasts:
          type: integer
          description: Total podcasts in the network
          format: int64
          example: 12
        optedInPodcasts:
          type: integer
          description: Podcasts whose opt_in_status is opted_in
          format: int64
          example: 8
        totalCampaigns:
          type: integer
          description: Total campaigns in the network
          format: int64
          example: 5
        totalOffers:
          type: integer
          description: Total campaign offers across the network's campaigns
          format: int64
          example: 20
        offersPending:
          type: integer
          description: Offers in the pending state
          format: int64
          example: 4
        offersAccepted:
          type: integer
          description: Offers that have been accepted
          format: int64
          example: 10
        offersRejected:
          type: integer
          description: Offers that have been rejected
          format: int64
          example: 3
        offersNegotiating:
          type: integer
          description: Offers currently in negotiation
          format: int64
          example: 3
        totalMembers:
          type: integer
          description: Total network members
          format: int64
          example: 6
      description: Aggregate counts for a network, for a dashboard overview. Computed
        by counting the network's collections at request time; no internal ids or
        denormalized persistence counters are exposed.
    NetworkUpdateRequest:
      type: object
      properties:
        name:
          type: string
          description: New display name of the network
          nullable: true
          example: Acme Podcast Network
        email:
          type: string
          description: New primary email address for the network
          nullable: true
          example: ops@example.com
        description:
          type: string
          description: New free-form description of the network
          nullable: true
          example: A network of independent technology podcasts
        website:
          type: string
          description: New public website for the network
          nullable: true
          example: https://example.com
        default_email_language:
          type: string
          description: New default language code used for emails sent to the network
          nullable: true
          example: en
        primary_contact:
          description: New primary business contact for the network
          nullable: true
          allOf:
          - $ref: '#/components/schemas/ContactInput'
        contacts:
          type: array
          description: New list of additional business contacts for the network
          nullable: true
          items:
            $ref: '#/components/schemas/ContactInput'
      description: Request body to update a network's mutable business fields. Every
        field is optional; only provided fields change (optimistic-locked). Internal
        fields are never accepted.
    OfferAcceptRequest:
      type: object
      properties:
        selected_voice_id:
          type: string
          description: Voice the host-voiced creative will be rendered in (required;
            must belong to the offer's podcast)
          nullable: true
          example: 8Pt5Rs7XwQ1qZy6V0cF3
        notes:
          type: string
          description: Optional free-text notes accompanying the acceptance
          nullable: true
          example: Please emphasize the spring discount
      description: Request to accept a campaign offer from the recipient flow. `selected_voice_id`
        is the voice the host-voiced creative will be rendered in (required; must
        belong to the offer's podcast). `notes` is optional free text.
    OfferCreateRequest:
      type: object
      properties:
        podcast_id:
          type: string
          description: Identifier of the podcast to send the offer to (required)
          nullable: true
          example: 2Jp6Mn4StL8qVx1R7yA9
        variant_id:
          type: string
          description: Identifier of the campaign ad variant to offer, if specifying
            one
          nullable: true
          example: 4Lq9Pr2UvM6nWx3T8zC5
      description: Request to create a campaign offer (match a campaign to a podcast).
        The network and campaign come from the path; the offer starts in `pending`
        and is advanced via the accept/reject actions.
    OfferCreateResponse:
      type: object
      properties:
        offer:
          description: The offer that was created
          allOf:
          - $ref: '#/components/schemas/OfferDto'
        offerUrl:
          type: string
          description: Public review link sent to the podcast owner
          example: https://app.adbridge.ai/campaign-offer/cmZfM2Y5YTJiN2MxZDhlNGY2MA
        emailSent:
          type: boolean
          description: Whether the notification email was sent (best-effort; offer
            is created regardless)
          example: true
      description: 'Response for creating (sending) a campaign offer. Wraps the created
        OfferDto with the outcome of the side effects: the public review link the
        podcast owner was sent, and whether the notification email actually went out
        (sending is best-effort --- a false here does not mean the offer failed to
        create).'
    OfferDecisionResponse:
      type: object
      properties:
        status:
          type: string
          description: The resulting offer status
          example: accepted
          enum:
          - accepted
          - rejected
        creativeGenerationStarted:
          type: boolean
          description: Whether host-voiced creative generation was started (accept
            only). Generation runs asynchronously; subscribe to the creative.ready
            / creative.failed webhook for the outcome.
          example: true
        creativeReady:
          type: boolean
          description: Always false on accept now that generation is asynchronous
            (the creative is created pending); subscribe to the creative.ready webhook
            for the ready outcome.
          example: false
      description: Result of a recipient accept/reject of a campaign offer.
    OfferDirectApproveRequest:
      type: object
      properties:
        podcast_id:
          type: string
          description: Identifier of the podcast to approve for the campaign (required)
          nullable: true
          example: 2Jp6Mn4StL8qVx1R7yA9
        variant_id:
          type: string
          description: Identifier of the campaign ad variant to approve, if specifying
            one
          nullable: true
          example: 4Lq9Pr2UvM6nWx3T8zC5
        voice_id:
          type: string
          description: Voice to render the creative with; defaults to the podcast's
            primary voice
          nullable: true
          example: 7Hb2Kp9QvL3mNx0R8tZ4
      description: Request to directly approve a podcast for a campaign (admin bypass).
        Creates or reuses an offer for the podcast and approves it immediately, skipping
        the recipient invitation/verification flow. The network and campaign come
        from the path.
    OfferDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier of the offer
          example: 5Gm8Qr3TvP7nKx4W9zB2
        createdAt:
          type: string
          description: Timestamp when the offer was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: Timestamp when the offer was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
        networkId:
          type: string
          description: Identifier of the network that owns the offer
          example: 9Fc4Lm2RtN6pQx1S7wZ3
        campaignId:
          type: string
          description: Identifier of the campaign the offer is for
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        podcastId:
          type: string
          description: Identifier of the podcast the offer was matched to
          example: 2Jp6Mn4StL8qVx1R7yA9
        variantId:
          type: string
          description: Identifier of the campaign ad variant offered, if specified
          nullable: true
          example: 4Lq9Pr2UvM6nWx3T8zC5
        status:
          type: string
          description: Offer status; change it via the accept/reject actions
          enum:
          - pending
          - accepted
          - rejected
          - negotiating
        creativeId:
          type: string
          description: Identifier of the creative produced for this offer once accepted,
            if any
          nullable: true
          example: 6Nr3Qs5VwP9oXy4U8aD7
      description: 'A campaign offer: a campaign matched to a podcast, with an accept/reject
        lifecycle.'
    OptInCompleteRequest:
      type: object
      properties:
        first_name:
          type: string
          description: Recipient's first name. Required to complete opt-in.
          nullable: true
          example: Jordan
        last_name:
          type: string
          description: Recipient's last name.
          nullable: true
          example: Rivera
        email:
          type: string
          description: Recipient's contact email. Required to complete opt-in; must
            be a valid email address.
          nullable: true
          example: recipient@example.com
        phone_number:
          type: string
          description: Recipient's phone number.
          nullable: true
          example: +1-415-555-0142
        role:
          type: string
          description: Recipient's role with respect to the podcast (free-form, e.g.
            host or producer). Required to complete opt-in.
          nullable: true
          example: host
        title:
          type: string
          description: Updated podcast title; when present it overwrites the stored
            title.
          nullable: true
          example: The Daily Tech Roundup
        description:
          type: string
          description: Updated podcast description; when present it overwrites the
            stored description.
          nullable: true
          example: A short daily look at the latest in technology.
        link:
          type: string
          description: Updated podcast website or landing-page URL.
          nullable: true
          example: https://example.com/podcast
        image_url:
          type: string
          description: Updated podcast cover-image URL.
          nullable: true
          example: https://example.com/cover.jpg
        categories:
          type: array
          description: Podcast content categories; when non-empty it replaces the
            stored categories.
          nullable: true
          items:
            type: string
        language:
          type: string
          description: Podcast primary language as a 2-letter ISO-639-1 code.
          nullable: true
          example: en
        downloads_per_month:
          type: integer
          description: Estimated monthly downloads; ignored if negative.
          format: int64
          nullable: true
          example: 125000
        blocked_categories:
          type: array
          description: Advertiser categories the recipient wants to exclude; becomes
            the podcast's excluded advertiser categories.
          nullable: true
          items:
            type: string
        additional_notes:
          type: string
          description: Free-form notes the recipient wants to attach to the opt-in.
          nullable: true
          example: Prefer ads read in the host's own voice.
        external_ids:
          type: object
          additionalProperties:
            type: string
          description: Optional partner-defined external identifiers to store on the
            podcast, keyed by system name.
          nullable: true
        voice_id:
          type: string
          description: Id of a READY voice created via the opt-in voice pipeline to
            link as the podcast's primary voice. Must belong to this podcast and be
            finalized.
          nullable: true
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        voice_name:
          type: string
          description: Display name to store for the linked voice.
          nullable: true
          example: Host Voice
      description: The opt-in registration submission. `first_name`, `email`, and
        `role` are required; everything else is optional and, when present, updates
        the podcast. `blocked_categories` becomes the podcast's excluded advertiser
        categories. `voice_id` (a voice created via the opt-in voice pipeline) is
        linked as the podcast's primary voice when supplied.
    OptInCompleteResponse:
      type: object
      properties:
        status:
          type: string
          description: Outcome of the opt-in registration; always "completed" on success
          example: completed
          enum:
          - completed
        podcastId:
          type: string
          description: Id of the podcast that was opted in
          example: 7Hb2Kp9QvL3mNx0R8tZ4
      description: Result of completing opt-in registration.
    OptInContextResponse:
      type: object
      properties:
        podcastTitle:
          type: string
          description: Title of the podcast being invited to opt in
          example: The Daily Tech Roundup
        networkName:
          type: string
          description: Name of the network that sent the invitation
          example: Acme Podcast Network
        status:
          type: string
          description: Lifecycle status of the opt-in invitation behind the magic
            link
          example: pending
          enum:
          - pending
          - started
          - completed
          - expired
          - cancelled
        expiresAt:
          type: string
          description: When the magic-link invitation expires and can no longer be
            used
          format: date-time
          example: 2026-01-15T09:30:00Z
        verified:
          type: boolean
          description: Whether the recipient has already passed verification-code
            verification for this magic link
          example: false
        codeExpiresAt:
          type: string
          description: When the current verification code expires, if one is outstanding;
            null when no code has been sent
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
      description: 'The curated, recipient-facing view of an opt-in invitation ---
        just enough for a client to render the registration screen. Deliberately minimal:
        no internal ids, no inviter identity, no raw verification code, no persistence
        internals. Returned from the public token surface.'
    OptInInvitationCreateRequest:
      type: object
      properties:
        expires_in_days:
          type: integer
          description: Number of days until the invitation expires; defaults to 7
            if omitted
          format: int32
          nullable: true
          example: 7
      description: Request to invite a podcast owner to complete opt-in registration.
        The network and podcast come from the path. `expires_in_days` is optional
        (defaults to 7). If an active invitation already exists for the podcast it
        is reused rather than duplicated.
    OptInInvitationCreateResponse:
      type: object
      properties:
        invitation:
          description: The created or reused opt-in invitation
          allOf:
          - $ref: '#/components/schemas/OptInInvitationDto'
        inviteUrl:
          type: string
          description: Public link the podcast owner uses to complete registration
          example: https://app.adbridge.ai/opt-in/inv_3f9a2b7c1d8e4f60a5b2
        reused:
          type: boolean
          description: Whether an active invitation already existed and was reused
            instead of created
          example: false
        emailSent:
          type: boolean
          description: Whether the invitation email was sent (best-effort; invitation
            exists regardless)
          example: true
      description: Response for inviting a podcast owner to complete opt-in registration.
        Wraps the created (or reused) OptInInvitationDto with the public registration
        link the owner was sent, and whether the invitation email actually went out
        (best-effort).
    OptInInvitationDto:
      type: object
      properties:
        id:
          type: string
          description: Unique id of the opt-in invitation
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        podcastId:
          type: string
          description: Id of the podcast invited to opt into advertising
          example: 9Kp3Lm2QvX7nRt0B8wZ5
        status:
          type: string
          description: Invitation lifecycle status
          example: pending
          enum:
          - pending
          - started
          - completed
          - expired
          - cancelled
        expiresAt:
          type: string
          description: When the invitation expires and can no longer be completed
          format: date-time
          example: 2026-01-15T09:30:00Z
        accessedAt:
          type: string
          description: When the podcast owner first opened the invitation link, if
            applicable
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        completedAt:
          type: string
          description: When the podcast owner completed opt-in registration, if applicable
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        createdAt:
          type: string
          description: When the invitation was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: When the invitation was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
      description: The created or reused opt-in invitation
    PageResponse_AdvertiserDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AdvertiserDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_CampaignDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CampaignDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_CreativeDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CreativeDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_MemberDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/MemberDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_NetworkDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/NetworkDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_OfferDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/OfferDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_PodcastDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PodcastDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_ServiceAccountDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ServiceAccountDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_TranscriptionDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/TranscriptionDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_UserDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/UserDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_UserInvitationDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/UserInvitationDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_VoiceDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/VoiceDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PageResponse_WebhookDto_:
      required:
      - items
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDto'
        nextCursor:
          type: string
          nullable: true
      description: A single page of results. `nextCursor` is an opaque token to pass
        back as `?cursor=` to fetch the following page; `null` means there are no
        more results.
    PodcastCreateRequest:
      type: object
      properties:
        title:
          type: string
          description: Display title of the podcast. Required.
          nullable: true
          example: The Daily Tech
        feed_url:
          type: string
          description: URL of the podcast's RSS feed. Required.
          nullable: true
          example: https://feeds.example.com/the-daily-tech.xml
        description:
          type: string
          description: Short description of the podcast.
          nullable: true
          example: A daily look at the latest in technology.
        link:
          type: string
          description: URL of the podcast's public website or landing page.
          nullable: true
          example: https://example.com/podcasts/the-daily-tech
        image_url:
          type: string
          description: URL of the podcast's cover artwork image.
          nullable: true
          example: https://cdn.example.com/the-daily-tech/cover.jpg
        language:
          type: string
          description: Primary language of the podcast as an ISO 639-1 code.
          nullable: true
          example: en
        categories:
          type: array
          description: Content categories the podcast belongs to.
          nullable: true
          items:
            type: string
        primary_contact:
          description: The primary contact for this podcast. Required.
          nullable: true
          allOf:
          - $ref: '#/components/schemas/ContactInput'
        contacts:
          type: array
          description: Additional contacts associated with this podcast.
          nullable: true
          items:
            $ref: '#/components/schemas/ContactInput'
        external_ids:
          type: object
          additionalProperties:
            type: string
          description: External system identifiers keyed by system name.
          nullable: true
        downloads_per_month:
          type: integer
          description: Estimated number of episode downloads per month.
          format: int64
          nullable: true
          example: 125000
      description: Request body to create a podcast. The network comes from the path.
        `title`, `feed_url` and `primary_contact` are required. Platform-owned fields
        (`opt_in_status`, `primary_voice_id`, lifecycle status, `version`, `info`)
        are never accepted --- the opt-in flow owns them. RSS auto-import from `feed_url`
        is a deferred follow-up.
    PodcastDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier for the podcast.
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        createdAt:
          type: string
          description: Timestamp when the podcast was created.
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: Timestamp when the podcast was last updated.
          format: date-time
          example: 2026-01-15T09:30:00Z
        title:
          type: string
          description: Display title of the podcast.
          example: The Daily Tech
        description:
          type: string
          description: Short description of the podcast.
          nullable: true
          example: A daily look at the latest in technology.
        link:
          type: string
          description: URL of the podcast's public website or landing page.
          nullable: true
          example: https://example.com/podcasts/the-daily-tech
        imageUrl:
          type: string
          description: URL of the podcast's cover artwork image.
          nullable: true
          example: https://cdn.example.com/the-daily-tech/cover.jpg
        feedUrl:
          type: string
          description: URL of the podcast's RSS feed.
          example: https://feeds.example.com/the-daily-tech.xml
        categories:
          type: array
          description: Content categories the podcast belongs to.
          items:
            type: string
        language:
          type: string
          description: Primary language of the podcast as an ISO 639-1 code.
          nullable: true
          example: en
        networkId:
          type: string
          description: Identifier of the network that owns this podcast.
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        primaryVoiceId:
          type: string
          description: Identifier of the podcast's primary voice, if one is assigned.
          nullable: true
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        primaryVoice:
          description: The podcast's primary voice; present only when `?expand=primary_voice`
            was requested.
          nullable: true
          allOf:
          - $ref: '#/components/schemas/VoiceSummaryDto'
        externalIds:
          type: object
          additionalProperties:
            type: string
          description: External system identifiers keyed by system name.
        status:
          type: string
          description: Podcast status
          example: active
          enum:
          - active
          - disabled
        optInStatus:
          type: string
          description: Opt-in status for advertising
          example: opted_in
          enum:
          - not_started
          - pending
          - opted_in
          - declined
        excludedAdCategories:
          type: array
          description: Ad categories excluded from advertising on this podcast.
          items:
            type: string
        downloadsPerMonth:
          type: integer
          description: Estimated number of episode downloads per month.
          format: int64
          nullable: true
          example: 125000
        primaryContact:
          description: The primary contact for this podcast.
          allOf:
          - $ref: '#/components/schemas/ContactDto'
        contacts:
          type: array
          description: Additional contacts associated with this podcast.
          items:
            $ref: '#/components/schemas/ContactDto'
      description: A podcast owned by a network.
    PodcastUpdateRequest:
      type: object
      properties:
        title:
          type: string
          description: Display title of the podcast.
          nullable: true
          example: The Daily Tech
        feed_url:
          type: string
          description: URL of the podcast's RSS feed.
          nullable: true
          example: https://feeds.example.com/the-daily-tech.xml
        description:
          type: string
          description: Short description of the podcast.
          nullable: true
          example: A daily look at the latest in technology.
        link:
          type: string
          description: URL of the podcast's public website or landing page.
          nullable: true
          example: https://example.com/podcasts/the-daily-tech
        image_url:
          type: string
          description: URL of the podcast's cover artwork image.
          nullable: true
          example: https://cdn.example.com/the-daily-tech/cover.jpg
        language:
          type: string
          description: Primary language of the podcast as an ISO 639-1 code.
          nullable: true
          example: en
        categories:
          type: array
          description: Content categories the podcast belongs to.
          nullable: true
          items:
            type: string
        primary_contact:
          description: The primary contact for this podcast.
          nullable: true
          allOf:
          - $ref: '#/components/schemas/ContactInput'
        contacts:
          type: array
          description: Additional contacts associated with this podcast.
          nullable: true
          items:
            $ref: '#/components/schemas/ContactInput'
        external_ids:
          type: object
          additionalProperties:
            type: string
          description: External system identifiers keyed by system name.
          nullable: true
        downloads_per_month:
          type: integer
          description: Estimated number of episode downloads per month.
          format: int64
          nullable: true
          example: 125000
      description: Request body to update a podcast's mutable business fields. Every
        field is optional; only provided fields change (optimistic-locked). `opt_in_status`
        and `primary_voice_id` are NOT accepted here --- those are platform-owned
        and set by the opt-in flow.
    PreviewVoiceRequest:
      type: object
      properties:
        text:
          type: string
          description: Text to synthesize for the preview. Defaults to a canned line
            when omitted.
          nullable: true
          example: Hello, this is a preview of my podcast voice.
        stability:
          type: number
          description: Voice stability, from 0.0 (more variable) to 1.0 (more consistent).
          format: double
          nullable: true
          example: 0.5
        similarity_boost:
          type: number
          description: Similarity boost, from 0.0 to 1.0, controlling how closely
            the output matches the original voice.
          format: double
          nullable: true
          example: 0.75
        style:
          type: number
          description: Style exaggeration, from 0.0 to 1.0.
          format: double
          nullable: true
          example: 0.0
        use_speaker_boost:
          type: boolean
          description: Whether to boost the resemblance to the speaker.
          nullable: true
          example: true
        speed:
          type: number
          description: Playback speed multiplier for the synthesized audio.
          format: double
          nullable: true
          example: 1.0
        selected_voice_id:
          type: string
          description: Identifier of the voice to preview, on endpoints where the
            voice is not a path variable. Ignored when the voice id is in the path.
          nullable: true
          example: 7Hb2Kp9QvL3mNx0R8tZ4
      description: Request to synthesize a short preview of a cloned voice. `text`
        defaults to a canned line when omitted; the voice settings are optional and
        default to balanced values. `selected_voice_id` identifies which voice to
        preview on endpoints where the voice is not a path variable (e.g. the campaign-offer
        preview); endpoints that carry the voice id in the path ignore it.
    ResendCodeResponse:
      type: object
      properties:
        emailSent:
          type: boolean
          description: Whether the verification-code email was sent (best-effort);
            the code itself is never returned
          example: true
        codeExpiresAt:
          type: string
          description: When the newly generated verification code expires
          format: date-time
          example: 2026-01-15T09:30:00Z
      description: Response for a verification-code resend action. The code itself
        is never returned --- it is delivered only by email. Reports whether the email
        went out (best-effort) and when the new code expires.
    ServiceAccountCreateRequest:
      type: object
      properties:
        name:
          type: string
          description: Human-readable label for the account
          nullable: true
          example: billing-sync
        role:
          type: string
          description: Network role to grant the account; capped at the creating admin's
            own effective role
          nullable: true
          example: viewer
          enum:
          - admin
          - viewer
        description:
          type: string
          description: Optional free-text description of what the account is used
            for
          nullable: true
          example: Automated nightly billing reconciliation
      description: Request to create a platform service account. `role` is the network
        role to grant the account (`admin` or `viewer`); it is capped at the creating
        admin's own effective role. The account is created under the network in the
        path.
    ServiceAccountDto:
      type: object
      properties:
        id:
          type: string
          description: The service account id
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        name:
          type: string
          description: Human-readable label
          example: billing-sync
        description:
          type: string
          description: Optional free-text description of what the account is used
            for
          nullable: true
          example: Automated nightly billing reconciliation
        status:
          type: string
          description: Account status
          example: active
          enum:
          - active
          - disabled
        role:
          type: string
          description: Network role held by the account
          example: viewer
          enum:
          - admin
          - viewer
        membershipStatus:
          type: string
          description: Membership status
          example: active
          enum:
          - active
          - pending
          - suspended
        networkUrl:
          type: string
          description: Link to the owning network
          example: https://api.adbridge.ai/external/v1/networks/7Hb2Kp9QvL3mNx0R8tZ4
        createdAt:
          type: string
          description: When the account was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: When the account was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
      description: 'A platform service account as seen by a client: a non-human account
        that belongs to the network. Its `role`/`membership_status` come from the
        companion network-member record. Internal fields (version, info, created_by)
        are not exposed.'
    ServiceAccountKeyCreateRequest:
      type: object
      properties:
        name:
          type: string
          description: Human-readable label for the key
          nullable: true
          example: billing-sync
        role:
          type: string
          description: The key's scope role on the account's network; defaults to
            the account's membership role and is capped at both that role and the
            minting admin's own role
          nullable: true
          example: read_only
          enum:
          - read_only
          - administrator
        expires_at:
          type: string
          description: Optional ISO-8601 instant in the future when the key should
            expire; omit for a key that never expires
          nullable: true
          example: 2026-01-15T09:30:00Z
      description: Request to mint an API key for a service account. `role` is the
        key's scope role on the account's network (`read_only`/`administrator`); it
        defaults to the account's membership role and is capped at both the account's
        membership role and the minting admin's own role. `expires_at`, if given,
        is an ISO-8601 instant in the future.
    ServiceAccountUpdateRequest:
      type: object
      properties:
        name:
          type: string
          description: New human-readable label for the account
          nullable: true
          example: billing-sync
        description:
          type: string
          description: New free-text description of what the account is used for
          nullable: true
          example: Automated nightly billing reconciliation
        status:
          type: string
          description: New account status; disabling suspends the membership and revokes
            the account's keys
          nullable: true
          example: active
          enum:
          - active
          - disabled
        role:
          type: string
          description: New network role for the account; capped at the caller's own
            role
          nullable: true
          example: viewer
          enum:
          - admin
          - viewer
      description: Request to update a platform service account. All fields optional;
        only present ones change. `status` `active`/`disabled` (disabling suspends
        membership and revokes the account's keys); `role` `admin`/`viewer` (capped
        at the caller's role).
    TranscriptSegmentDto:
      type: object
      properties:
        speaker:
          type: string
          description: Diarized speaker label for this segment.
          example: speaker_0
        text:
          type: string
          description: Transcribed text spoken in this segment.
          example: This episode is brought to you by Acme...
        start:
          type: number
          description: Start time of the segment, in seconds from the beginning of
            the audio.
          format: double
          nullable: true
          example: 0.0
        end:
          type: number
          description: End time of the segment, in seconds from the beginning of the
            audio.
          format: double
          nullable: true
          example: 4.2
      description: One diarized speaker segment of a transcript, with its time range
        in seconds.
    TranscriptionDto:
      type: object
      properties:
        id:
          type: string
          description: The transcription id
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        podcastId:
          type: string
          description: The parent podcast id
          example: 9aF3kQ2pVx0LmNb7Rt84
        status:
          type: string
          description: Status of the transcription job
          example: completed
          enum:
          - pending
          - processing
          - completed
          - failed
        filename:
          type: string
          description: Original audio filename
          example: episode-42.mp3
        text:
          type: string
          description: Full plain-text transcript, when available.
          nullable: true
          example: Welcome back to the show. Today we are talking about...
        segments:
          type: array
          description: Diarized speaker segments of the transcript, in chronological
            order.
          items:
            $ref: '#/components/schemas/TranscriptSegmentDto'
        language:
          type: string
          description: Detected language, as a language code, when available.
          nullable: true
          example: en
        duration:
          type: number
          description: Total duration of the audio, in seconds, when available.
          format: double
          nullable: true
          example: 182.5
        error:
          type: string
          description: Error message if the transcription failed.
          nullable: true
          example: audio file could not be decoded
        createdBy:
          type: string
          description: The user who initiated the transcription.
          nullable: true
          example: user@example.com
        createdAt:
          type: string
          description: When the transcription was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: When the transcription was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
      description: A transcription job as seen by an API client. The raw internal
        `info` map and the persistence `version` are intentionally not exposed.
    TranscriptionResponse:
      type: object
      properties:
        text:
          type: string
          description: Full transcribed text of the audio clip.
          example: Welcome back to the show. Today we are talking about...
        segments:
          type: array
          description: Diarized speaker segments of the transcript, in chronological
            order.
          items:
            $ref: '#/components/schemas/TranscriptSegmentDto'
        language:
          type: string
          description: Detected language of the audio, as a language code, when available.
          nullable: true
          example: en
        duration:
          type: number
          description: Total duration of the transcribed audio, in seconds, when available.
          format: double
          nullable: true
          example: 182.5
        filename:
          type: string
          description: Original filename of the uploaded audio clip.
          example: host-read.mp3
        fileSize:
          type: integer
          description: Size of the uploaded audio file, in bytes.
          format: int64
          example: 2457600
      description: Result of transcribing an audio clip with speaker diarization ---
        used by the opt-in auto-crop step to pick the host's segments. Mirrors the
        TS transcribe response.
    UserDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier of the user (Firebase Auth UID)
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        email:
          type: string
          description: Email address of the user
          example: jane@example.com
        displayName:
          type: string
          description: Optional display name of the user
          nullable: true
          example: Jane Doe
        preferredLanguage:
          type: string
          description: Preferred language code for the user
          example: en
          enum:
          - en
          - es
          - ar
        isSystemAdmin:
          type: boolean
          description: Whether the user is a platform system administrator (super-user)
          example: false
        status:
          type: string
          description: Account status
          example: active
          enum:
          - active
          - inactive
          - suspended
        lastLogin:
          type: string
          description: When the user last logged in
          format: date-time
          nullable: true
          example: 2026-01-15T09:30:00Z
        createdAt:
          type: string
          description: When the user was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: When the user was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
      description: A podcaster portal user. The id is the user's Firebase Auth UID.
        The free-form `info` map and the internal `version` are never exposed.
    UserInvitationCreateRequest:
      type: object
      properties:
        email:
          type: string
          description: Email address to send the invitation to
          nullable: true
          example: member@example.com
        first_name:
          type: string
          description: Recipient's first name
          nullable: true
          example: Jordan
        last_name:
          type: string
          description: Recipient's last name
          nullable: true
          example: Lee
        role:
          type: string
          description: Network membership role to grant on acceptance
          nullable: true
          example: viewer
          enum:
          - admin
          - viewer
        expires_in_days:
          type: integer
          description: Number of days until the invitation expires; defaults to 7
            if omitted
          format: int32
          nullable: true
          example: 7
      description: Request to invite a user to a network. The network comes from the
        path. `role` is the network membership role to grant on acceptance (`admin`
        or `viewer`). `expires_in_days` is optional (defaults to 7).
    UserInvitationCreateResponse:
      type: object
      properties:
        invitation:
          description: The created invitation
          allOf:
          - $ref: '#/components/schemas/UserInvitationDto'
        inviteUrl:
          type: string
          description: Public link the recipient uses to accept the invitation
          example: https://app.adbridge.ai/invite/inv_3f9a2b7c1d8e4f60a5b2
        emailSent:
          type: boolean
          description: Whether the invitation email was sent (best-effort; invitation
            is created regardless)
          example: true
      description: Response for inviting a user to a network. Wraps the created UserInvitationDto
        with the public accept link the recipient was sent, and whether the invitation
        email actually went out (sending is best-effort --- a false here does not
        mean the invitation failed to create).
    UserInvitationDto:
      type: object
      properties:
        id:
          type: string
          description: Unique id of the invitation
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        networkId:
          type: string
          description: Id of the network the recipient is invited to join
          example: 9Kp3Lm2QvX7nRt0B8wZ5
        email:
          type: string
          description: Email address the invitation was sent to
          example: member@example.com
        firstName:
          type: string
          description: Recipient's first name
          example: Jordan
        lastName:
          type: string
          description: Recipient's last name
          example: Lee
        role:
          type: string
          description: Network membership role to grant on acceptance
          example: viewer
          enum:
          - admin
          - viewer
        status:
          type: string
          description: Invitation lifecycle status
          example: pending
          enum:
          - pending
          - accepted
          - expired
          - revoked
        expiresAt:
          type: string
          description: When the invitation expires and can no longer be accepted
          format: date-time
          example: 2026-01-15T09:30:00Z
        createdAt:
          type: string
          description: When the invitation was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: When the invitation was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
      description: A network user invitation, as seen by an API client. The opaque
        link token, inviter identity, and persistence internals (version, free-form
        info) are intentionally not exposed --- the link is returned separately as
        `invite_url` on create.
    UserSummaryDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier of the user
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        email:
          type: string
          description: Email address of the user
          example: ops@example.com
        displayName:
          type: string
          description: Optional display name of the user
          nullable: true
          example: Jane Doe
        status:
          type: string
          description: Account status
          enum:
          - active
          - inactive
          - suspended
      description: The member's user profile; present only when ?expand=user was requested
    UserUpdateRequest:
      type: object
      properties:
        display_name:
          type: string
          description: Optional display name of the user
          nullable: true
          example: Jane Doe
        preferred_language:
          type: string
          description: Preferred language code for the user
          nullable: true
          example: en
          enum:
          - en
          - es
          - ar
        is_system_admin:
          type: boolean
          description: Whether the user is a platform system administrator (super-user)
          nullable: true
          example: false
        status:
          type: string
          description: Account status
          nullable: true
          example: active
          enum:
          - active
          - inactive
          - suspended
      description: Request body to update a user's mutable fields. Every field is
        optional; only the provided fields change (optimistic-locked). The user's
        `email`, `last_login`, raw `info` map, and `version` are never accepted.
    VerifyCodeRequest:
      type: object
      properties:
        code:
          type: string
          description: The 6-digit verification code delivered by email after a send-code
            request. Required to verify the magic link.
          nullable: true
          example: '123456'
      description: Request body for submitting a 6-digit verification code on the
        public token surface.
    VerifyCodeResponse:
      type: object
      properties:
        verified:
          type: boolean
          description: True once the verification code is accepted (or the magic link
            was already verified)
          example: true
      description: Result of a successful verification-code submission (failures are
        returned as 4xx errors).
    VoiceCreateRequest:
      type: object
      properties:
        title:
          type: string
          description: Display name of the voice.
          nullable: true
          example: Morning Host
        description:
          type: string
          description: Short description of the voice.
          nullable: true
          example: Warm, conversational host voice for morning episodes.
        language:
          type: string
          description: Primary language of the voice as an ISO 639-1 code.
          nullable: true
          example: en
      description: Request body for creating a voice. Audio samples are added afterwards
        via the upload endpoint, not supplied here; lifecycle state (status / verification
        / cloning) is owned by the platform.
    VoiceDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier for the voice.
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        createdAt:
          type: string
          description: Timestamp when the voice was created.
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: Timestamp when the voice was last updated.
          format: date-time
          example: 2026-01-15T09:30:00Z
        podcastId:
          type: string
          description: Identifier of the podcast this voice belongs to.
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        title:
          type: string
          description: Display name of the voice.
          nullable: true
          example: Morning Host
        description:
          type: string
          description: Short description of the voice.
          nullable: true
          example: Warm, conversational host voice for morning episodes.
        status:
          type: string
          description: Lifecycle status; a voice is 'ready' once it has audio samples
          example: ready
          enum:
          - pending
          - ready
          - archived
          - failed
        verificationStatus:
          type: string
          description: Whether the voice has been verified for use
          example: verified
          enum:
          - pending
          - verified
          - rejected
        info:
          description: Additional voice metadata, including language and uploaded
            samples.
          allOf:
          - $ref: '#/components/schemas/VoiceInfoDto'
      description: Public representation of a voice. A stable client contract, decoupled
        from internal storage.
    VoiceInfoDto:
      type: object
      properties:
        language:
          type: string
          description: Primary language of the voice as an ISO 639-1 code.
          nullable: true
          example: en
        files:
          type: array
          description: The uploaded audio samples backing this voice.
          items:
            $ref: '#/components/schemas/VoiceSampleDto'
      description: Additional voice metadata, including language and uploaded samples.
    VoiceSampleDto:
      type: object
      properties:
        filename:
          type: string
          description: Stored filename of the audio sample.
          nullable: true
          example: sample-001.mp3
        originalName:
          type: string
          description: Original filename of the sample as uploaded by the client.
          nullable: true
          example: morning-host-take-1.mp3
        size:
          type: integer
          description: Size of the audio sample in bytes.
          format: int64
          nullable: true
          example: 204800
      description: A voice training-sample the client has uploaded.
    VoiceSummaryDto:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier for the voice.
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        title:
          type: string
          description: Display name of the voice.
          nullable: true
          example: Morning Host
        status:
          type: string
          description: Lifecycle status; a voice is 'ready' once it has audio samples
          example: ready
          enum:
          - pending
          - ready
          - archived
          - failed
      description: A minimal voice reference (id, title, status) surfaced inline where
        a client needs to recognize a voice without a second lookup --- e.g. the selectable
        voices on a campaign-offer review screen, or a podcast's expanded `primary_voice`.
        Decoupled from internal voice storage.
    VoiceUpdateRequest:
      type: object
      properties:
        title:
          type: string
          description: Display name of the voice.
          nullable: true
          example: Morning Host
        description:
          type: string
          description: Short description of the voice.
          nullable: true
          example: Warm, conversational host voice for morning episodes.
        language:
          type: string
          description: Primary language of the voice as an ISO 639-1 code.
          nullable: true
          example: en
      description: Request body for partially updating a voice. Only non-null fields
        are applied. Samples are managed via the upload endpoint; lifecycle state
        is owned by the platform.
    WebhookCreateRequest:
      type: object
      properties:
        url:
          type: string
          description: Destination URL deliveries are POSTed to; must be http or https
          nullable: true
          example: https://example.com/webhooks/adbridge
        events:
          type: array
          description: Non-empty list of event types to subscribe to
          nullable: true
          items:
            type: string
            example: offer.accepted
            enum:
            - offer.accepted
            - offer.rejected
            - optin.completed
            - invitation.accepted
            - creative.ready
            - creative.failed
      description: Request to create a webhook subscription. `url` must be http/https;
        `events` must be a non-empty list of known event types. The signing secret
        is generated server-side and returned once (clients do not supply it).
    WebhookCreateResponse:
      type: object
      properties:
        webhook:
          description: The created webhook
          allOf:
          - $ref: '#/components/schemas/WebhookDto'
        secret:
          type: string
          description: The signing secret (HMAC-SHA256 key). Shown ONLY on create
            — store it now; it cannot be retrieved again. Used to verify the X-AdBridge-Signature
            header on deliveries.
          example: whsec_xxxxxxxxxxxxxxxxxxxx
      description: The response to creating a webhook. It carries the full WebhookDto
        plus the signing `secret` --- which is shown *only* here, on create. Subsequent
        reads (get/list) never return the secret, so the client must store it now;
        it cannot be recovered later.
    WebhookDto:
      type: object
      properties:
        id:
          type: string
          description: The webhook id
          example: 7Hb2Kp9QvL3mNx0R8tZ4
        url:
          type: string
          description: The destination URL deliveries are POSTed to
          example: https://example.com/webhooks/adbridge
        events:
          type: array
          description: Subscribed event types
          items:
            type: string
            example: offer.accepted
            enum:
            - offer.accepted
            - offer.rejected
            - optin.completed
            - invitation.accepted
            - creative.ready
            - creative.failed
        status:
          type: string
          description: Subscription status
          example: active
          enum:
          - active
          - disabled
        createdAt:
          type: string
          description: When the subscription was created
          format: date-time
          example: 2026-01-15T09:30:00Z
        updatedAt:
          type: string
          description: When the subscription was last updated
          format: date-time
          example: 2026-01-15T09:30:00Z
      description: A webhook subscription as seen by an API client. The signing `secret`
        is intentionally NOT exposed here --- it is returned only once, on create
        (see WebhookCreateResponse). The persistence `version` is not exposed.
    WebhookUpdateRequest:
      type: object
      properties:
        url:
          type: string
          description: New destination URL; if given, must be http or https
          nullable: true
          example: https://example.com/webhooks/adbridge
        events:
          type: array
          description: New set of subscribed event types; if given, must be a non-empty
            list
          nullable: true
          items:
            type: string
            example: offer.accepted
            enum:
            - offer.accepted
            - offer.rejected
            - optin.completed
            - invitation.accepted
            - creative.ready
            - creative.failed
        status:
          type: string
          description: New subscription status
          nullable: true
          example: active
          enum:
          - active
          - disabled
      description: Request to update a webhook subscription. All fields are optional;
        only the present ones change. `url` (if given) must be http/https; `events`
        (if given) must be a non-empty list of known event types; `status` (if given)
        must be `active` or `disabled`. The signing secret cannot be changed via update.
    ErrorResponse:
      type: object
      description: The standard error envelope returned for all non-2xx responses
      properties:
        type:
          type: string
          description: A URI identifying the error type
        title:
          type: string
          description: A short, human-readable summary of the error
        status:
          description: The HTTP status code
          type: integer
          format: int32
        detail:
          type: string
          description: A human-readable explanation of this occurrence
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      description: 'API key issued to your account, sent as: Authorization: Bearer
        <key>'
servers:
- url: https://api.adbridge.ai
security:
- ApiKey: [
    ]
x-tagGroups:
- name: Getting started
  tags:
  - Account
- name: Core resources
  tags:
  - Networks
  - Podcasts
  - Voices
  - Transcriptions
  - Advertisers
  - Campaigns
  - Offers
  - Creatives
- name: Administration
  tags:
  - Users
  - Members
  - Network invitations
  - Service accounts
  - API keys
  - Webhooks
- name: Podcast onboarding
  tags:
  - Opt-in invitations
- name: Recipient flows (no API key)
  tags:
  - Public — Opt-in
  - Public — Opt-in voice
  - Public — Campaign offer
  - Public — Invitations
