openapi: 3.1.0
info:
  title: Mumara ONE REST API V2
  description: |
    The **Mumara ONE REST API V2** lets your own applications manage your Mumara ONE account: lists, contacts, broadcasts, segments, suppressions, custom fields, sending domains, statistics and more. It is the recommended version for new integrations.

    To **send transactional email**, use a Bridge and the [Sending API](../sending/) instead. This API manages your account; it doesn't send one-to-one email.

    ---

    ## Getting Started

    ### 1. Create an API key
    Go to **Setup → API Keys** in Mumara ONE and create a key. Choose the permissions it needs, and optionally the IP addresses it may be used from and its rate limit. See [API Keys](https://docs.mumara.com/one/setup/api-keys).

    ### 2. Authenticate your requests
    Send the key in the `Authorization` header of every request:

    ```
    Authorization: Bearer YOUR_API_KEY
    ```

    > **Important:** V2 endpoints accept the key **only** in the `Authorization: Bearer` header. Query parameter (`?api_token=`) and request body authentication are **not supported** on V2. This keeps keys out of URLs, server logs, browser history and `Referer` headers.

    ### 3. Set the Content-Type
    Send request bodies as JSON:

    ```
    Content-Type: application/json
    ```

    ---

    ## Base URL

    All V2 endpoints are served under:

    ```
    https://one.mumara.com/api/v2/
    ```

    ---

    ## Response Format

    Every V2 response follows a predictable JSON structure, so you always know where to find the data, errors and pagination details.

    **Successful response:**
    ```json
    {
      "success": true,
      "data": { },
      "message": "Operation completed successfully"
    }
    ```

    **Paginated response:**
    ```json
    {
      "success": true,
      "data": [ ],
      "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 142,
        "last_page": 6,
        "from": 1,
        "to": 25
      }
    }
    ```

    **Error response:**
    ```json
    {
      "success": false,
      "error": {
        "code": "VALIDATION_ERROR",
        "message": "The given data was invalid.",
        "details": {
          "email": ["The email field is required."]
        }
      }
    }
    ```

    ### Standard Error Codes

    | HTTP Status | Error Code | When It Occurs |
    |------------|------------|----------------|
    | `400` | `ERROR` | Malformed request or business logic error |
    | `401` | `UNAUTHORIZED` | The request came from an IP address the key doesn't allow |
    | `403` | `FORBIDDEN` | The key doesn't have permission for this action (`details.reason` is `api_permission`) |
    | `404` | `NOT_FOUND` | The item doesn't exist, or doesn't belong to your account |
    | `409` | `CONFLICT` | The item already exists (e.g., duplicate email in list) |
    | `422` | `VALIDATION_ERROR` | The request body failed validation |
    | `429` | `RATE_LIMITED` | Too many requests—slow down |
    | `500` | `INTERNAL_ERROR` | Unexpected server error |

    ### Key errors use the V1 format

    A missing, wrong or switched-off key is refused before the V2 layer runs, so these errors use the V1 body, with `status` and `result` instead of `success` and `error`:

    | HTTP Status | Body |
    |------------|------|
    | `401` | `{"status": "error", "result": "Error: Invalid or missing API token.", "suggestion": "..."}` |
    | `403` | `{"status": "error", "result": "Error: API token has expired."}` (the key is switched off) |

    Check the HTTP status first, then read whichever body shape you received.

    ### Sending Domain Error Codes

    The Sending Domains endpoints return additional, self-explanatory codes (see the `SendingDomainErrorCode` schema for the full list). Every refusal explains the rule that caused it in `error.details`.

    | HTTP Status | Error Code | When It Occurs |
    |------------|------------|----------------|
    | `403` | `OWNERSHIP_REVOKED` | Ownership verification was reset by Mumara ONE; contact support |
    | `403` | `SANDBOX_DOMAIN` | The sandbox domain Mumara ONE created for your account can't be changed |
    | `403` | `DKIM_REGENERATE_DISABLED` | Regenerating DKIM keys isn't available to your account |
    | `409` | `DOMAIN_ALREADY_EXISTS` | The domain is already registered |
    | `409` | `DOMAIN_IN_USE` | Lists, triggers or web forms still use the domain |
    | `422` | `OWNERSHIP_VERIFICATION_REQUIRED` | Prove ownership before configuring the domain |
    | `422` | `OWNERSHIP_VERIFICATION_FAILED` | The ownership record/file could not be confirmed |
    | `422` | `FEATURE_FORCED_BY_PACKAGE` | Your plan requires this feature; it cannot be disabled |
    | `422` | `FEATURE_UNAVAILABLE` | The feature isn't available in Mumara ONE |
    | `422` | `DOMAIN_LIMIT_REACHED` | Your plan's sending-domain limit is reached |
    | `422` | `HOOK_ABORT` | Mumara ONE refused the action; `details.code` names the rule (see below) |
    | `429` | `VERIFICATION_COOLDOWN` | Re-verification requested before the cooldown expired |

    `HOOK_ABORT` codes in Mumara ONE:

    | `details.code` | When It Occurs |
    |----------------|----------------|
    | `RESERVED_HOSTNAME` | The domain is reserved by the platform and can't be added |
    | `ACCOUNT_NOT_ACTIVE` | Your account isn't active, so domains can't be added |
    | `SERVER_REBUILD_IN_PROGRESS` | Your sending servers are being updated; try regenerating DKIM keys again in a few minutes |
    | `REASSIGN_TARGET_SUSPENDED` | The domain you asked to move lists and forms to is suspended |

    ---

    ## Pagination

    All list endpoints support **page-based pagination** with two query parameters:

    | Parameter | Default | Range | Description |
    |-----------|---------|-------|-------------|
    | `page` | `1` | 1+ | The page number to retrieve |
    | `per_page` | `25` | 1–100 | Number of records per page |

    **Example:**
    ```
    GET /api/v2/contacts?list_id=5&page=2&per_page=50
    ```

    The response `meta` object tells you everything you need to page through results:

    ```json
    "meta": {
      "current_page": 2,
      "per_page": 50,
      "total": 1423,
      "last_page": 29,
      "from": 51,
      "to": 100
    }
    ```

    ---

    ## Batch Operations

    Several resources support batch operations for efficient bulk processing:

    - **Batch Create:** `POST /v2/{resource}/batch` — Create up to **1,000 records** in a single request.
    - **Batch Delete:** `DELETE /v2/{resource}/batch` — Delete up to **100 records** by ID in a single request (contacts support up to 1,000).

    Batch endpoints return a summary of the operation:

    ```json
    {
      "success": true,
      "data": {
        "processed": 500,
        "created": 480,
        "updated": 15,
        "failed": 5,
        "errors": [
          { "email": "invalid@", "error": "Invalid email format" }
        ]
      }
    }
    ```

    ---

    ## Rate Limiting

    Each key may make as many requests per minute as its **Rate limit**, which you set on the key under **Setup → API Keys**. The default is **60 requests per minute**.

    | Header | Description |
    |--------|-------------|
    | `X-RateLimit-Limit` | Requests this key may make per minute (every response) |
    | `X-RateLimit-Remaining` | Requests left in the current minute (every response) |
    | `Retry-After` | Seconds to wait before retrying (only on a `429`) |
    | `X-RateLimit-Reset` | Unix timestamp when you can retry (only on a `429`) |

    When rate limited, you receive `429 Too Many Requests`. Wait for `Retry-After` before the next request. The limit counts per key, so give each integration its own key.

    ---

    ## What V2 doesn't cover

    - **Scheduling a broadcast, importing contacts from a file, and creating triggers and drip campaigns** are in the [V1 API](../v1/) for now.
    - **Sending infrastructure** (sending servers, bounce mailboxes and rules, feedback loops) is run by Mumara ONE, so it has no endpoints here.
    - **Events.** To receive delivery, bounce, open, click and complaint events, set up webhooks under **Actions → Webhooks** instead of polling the statistics endpoints. The payloads are in the [Sending API reference](../sending/).

    ---

    ## Your data only

    A key only reaches the account it belongs to. Everything you list, read, change or delete is limited to your own lists, contacts, broadcasts and other resources.

  version: 2.0.0
  contact:
    name: Mumara Support
    url: https://support.mumara.com
    email: support@mumara.com
  license:
    name: Proprietary
    url: https://mumara.com/terms

servers:
  - url: https://one.mumara.com/api/v2
    description: Mumara ONE

tags:
  - name: Lists
    description: |
      Contact lists hold your contacts. Every contact belongs to a list, and a list sets the sender identity (owner name and email) and reply address used when sending to it. Mumara ONE handles bounces for you, so lists need no bounce settings.
  - name: Contacts
    description: |
      Manage individual subscribers/contacts within your lists. Contacts can be created one at a time or imported in bulk using batch operations (up to 1,000 per request). Each contact has a status (subscribed, unsubscribed, bounced, or complaint) and can store custom field data.
  - name: Broadcasts
    description: |
      Broadcasts are email campaigns. Create a broadcast with a subject, content, and configuration here; to schedule it for delivery, use `POST /broadcastSchedule` in the [V1 API](../v1/). Broadcasts can be in draft, scheduled, sending, or sent states.
  - name: Sending Domains
    description: |
      Sending domains are the domains you send from. The API mirrors the **Setup → Sending Domains** flow and respects your plan's rules:

      1. **Add** the domain (`POST /sending-domains`). If ownership verification is required, the response is `pending_ownership_verification` and carries the ownership TXT record (or verification file) to publish.
      2. **Prove ownership** (`POST /sending-domains/{id}/verify-ownership`). Mumara ONE checks the record in DNS. On success the response already contains **all remaining DNS records** to add (DKIM TXT, tracking CNAME or tracking file, bounce SPF + MX, DMARC) according to which features are enabled.
      3. **Publish and confirm** the records (`POST /sending-domains/{id}/verify`). Each record is reported with its expected and found value; the domain becomes `active` once every enabled record passes.
      4. **Manage** features (`PATCH /sending-domains/{id}/features/{feature}`), selectors, DKIM keys and the domain itself. Features your plan requires can't be turned off, and every refusal explains which rule caused it.

      Every response includes a composite `status` and a machine-readable `next_step`, so a client always knows what to do next. `GET /sending-domains/policy` describes up front what the account may do.

      **Access control.** The key needs the *V2 Sending Domains* permissions, which you choose when you create or edit it under **Setup → API Keys**.

      > **Breaking change (V2 revision 2026-08):** the sending-domain object was redesigned. The former `is_verified` and `domain_status` booleans, `type`, `domain_hash`, `dkim.verified` and `tracking.verified` were replaced by `status`, `ownership`, `masking_type`, `domain_status_code`, `is_enabled` and per-feature `status` strings (`pending` / `verified` / `failed`).
  - name: Suppressions
    description: |
      Suppression lists prevent emails from being sent to specific addresses, domains, or IPs. Use suppressions to honor unsubscribe requests, block known-bad addresses, or comply with legal requirements. Three types are supported: email, domain, and IP.
  - name: Custom Fields
    description: |
      Custom fields extend the data you can store for each contact beyond the default email address. Define fields of various types (text, select, date, etc.) and associate them with lists. Use merge tags in your broadcasts to personalize content.
  - name: Spin Tags
    description: |
      Spin tags enable dynamic content rotation in your broadcasts. Define a placeholder name and a list of word/phrase variations; Mumara ONE randomly selects one for each recipient, helping improve deliverability and engagement.
  - name: Segments
    description: |
      Segments are dynamic filters that identify subsets of contacts across one or more lists based on conditions (field values, activity, dates, etc.). Use segments when scheduling broadcasts to target specific audiences without creating separate lists.
  - name: Drips
    description: |
      Drip campaigns (autoresponders) are automated email sequences triggered by events like subscription or date fields. The V2 API provides read and delete access; create drips in the app or with `POST /addDrip` in the [V1 API](../v1/).
  - name: Triggers
    description: |
      Triggers are automation rules that execute actions when specific events occur (e.g., send an email when a contact subscribes). The V2 API lets you view, enable/disable, and delete triggers; create them in the app or with `POST /addTrigger` in the [V1 API](../v1/).
  - name: Web Forms
    description: |
      Web forms are embeddable HTML subscription forms that can be placed on your website to capture new contacts. Retrieve form details, update settings, get embed codes, or delete forms via the API.
  - name: Groups
    description: |
      Groups organise your lists, broadcasts and custom fields. Each module type has its own group tree, helping you keep your workspace organized.
  - name: Statistics
    description: |
      Access detailed analytics for your broadcast campaigns, including delivery rates, opens, clicks, bounces, unsubscribes, and complaints. Statistics are tied to specific broadcast schedule IDs.
  - name: Account
    description: |
      Read the profile of the account the API key belongs to.

security:
  - bearerAuth: []

paths:
  # ============================================
  # LISTS
  # ============================================
  /lists:
    get:
      summary: List All Lists
      description: |
        Retrieve all contact lists accessible to the authenticated user. Results are paginated and can be filtered by group.

        Returns your own lists.
      operationId: v2GetLists
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: group_id
          in: query
          description: Filter lists belonging to a specific group
          schema:
            type: integer
      responses:
        '200':
          description: Lists retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      summary: Create a List
      description: |
        Create a new contact list. The `owner_email` domain must match a verified sending domain in your account.

        **Required setup before creating a list:** add and verify a sending domain, in the app or with the Sending Domains endpoints.

        If no `group_id` is specified, the list is placed in the default group.
      operationId: v2CreateList
      tags:
        - Lists
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListCreate'
            example:
              name: Newsletter Subscribers
              owner_name: John Doe
              owner_email: john@yourdomain.com
              reply_email: reply@yourdomain.com
              group_id: 1
      responses:
        '201':
          description: List created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListResponse'
        '409':
          description: A list with this name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          $ref: '#/components/responses/ValidationError'

  /lists/{id}:
    get:
      summary: Get a List
      description: |
        Retrieve a specific list by ID, including its group and associated custom fields.
      operationId: v2GetList
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: List retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a List
      description: |
        Update one or more fields of an existing list. Only include the fields you want to change—omitted fields remain unchanged.

        If updating `owner_email`, the new domain must be a verified sending domain in your account.
      operationId: v2UpdateList
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListUpdate'
      responses:
        '200':
          description: List updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: A list with this name already exists
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      summary: Delete a List
      description: |
        Soft-delete a list and queue its contacts for background deletion. Lists that are currently used by triggers or segments cannot be deleted until those references are removed.
      operationId: v2DeleteList
      tags:
        - Lists
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: List deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: List is in use by triggers or segments

  # ============================================
  # CONTACTS
  # ============================================
  /contacts:
    get:
      summary: List All Contacts
      description: |
        Retrieve contacts with pagination. Filter by list, status, or email search.

        **Status values:** `subscribed`, `unsubscribed`, `bounced`, `complaint`

        If no `list_id` is specified, returns contacts from all lists accessible to the authenticated user.
      operationId: v2GetContacts
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: list_id
          in: query
          description: Filter contacts belonging to a specific list
          schema:
            type: integer
        - name: status
          in: query
          description: Filter by subscription status
          schema:
            type: string
            enum: [subscribed, unsubscribed, bounced, complaint]
        - name: email
          in: query
          description: Search contacts by email (partial match supported)
          schema:
            type: string
      responses:
        '200':
          description: Contacts retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      summary: Create a Contact
      description: |
        Add a new contact to a list. The email must be unique within the target list.

        **Custom fields** can be set using the top-level convenience fields (`first_name`, `last_name`, `phone`) or the `custom_fields` object for any field by its tag name.
      operationId: v2CreateContact
      tags:
        - Contacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactCreate'
            example:
              email: john.doe@example.com
              list_id: 1
              first_name: John
              last_name: Doe
              phone: "+1-555-123-4567"
              status: subscribed
              format: html
              custom_fields:
                company: Acme Corp
                city: New York
      responses:
        '201':
          description: Contact created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactResponse'
        '404':
          description: List not found
        '409':
          description: Contact already exists in this list
        '422':
          $ref: '#/components/responses/ValidationError'

  /contacts/{id}:
    get:
      summary: Get a Contact
      description: Retrieve a specific contact by ID, including their list info and all custom field values.
      operationId: v2GetContact
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Contact retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a Contact
      description: |
        Update a contact's email, status, or custom fields. Only include the fields you want to change.

        **Status transitions:** You can change a contact to any status: `subscribed`, `unsubscribed`, `bounced`, or `complaint`.
      operationId: v2UpdateContact
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactUpdate'
      responses:
        '200':
          description: Contact updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: A contact with this email already exists in the list
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      summary: Delete a Contact
      description: Permanently delete a contact and all their associated data (custom field values, activity history).
      operationId: v2DeleteContact
      tags:
        - Contacts
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Contact deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /contacts/batch:
    post:
      summary: Batch Create/Update Contacts
      description: |
        Import up to **1,000 contacts** into a list in a single request. This is significantly more efficient than creating contacts individually.

        - Set `update_existing: true` to update custom fields for contacts that already exist in the list.
        - Set `update_existing: false` (default) to skip existing contacts and report them as errors.

        The response includes a detailed summary of the operation, including individual error details for failed records.
      operationId: v2BatchContacts
      tags:
        - Contacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactBatch'
            example:
              list_id: 1
              update_existing: true
              contacts:
                - email: jane@example.com
                  first_name: Jane
                  last_name: Smith
                  custom_fields:
                    company: Acme Corp
                - email: bob@example.com
                  first_name: Bob
      responses:
        '200':
          description: Batch operation completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResponse'
        '404':
          description: List not found
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      summary: Batch Delete Contacts
      description: |
        Delete multiple contacts by their IDs in a single request (up to 1,000). The list subscriber counts are automatically updated. Contacts that don't exist or aren't accessible are silently skipped.
      operationId: v2BatchDeleteContacts
      tags:
        - Contacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - ids
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                  maxItems: 1000
                  description: Array of contact IDs to delete (max 1,000)
                  example: [101, 102, 103]
      responses:
        '200':
          description: Contacts deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchDeleteResponse'

  # ============================================
  # BROADCASTS
  # ============================================
  /broadcasts:
    get:
      summary: List All Broadcasts
      description: |
        Retrieve all broadcast campaigns accessible to the authenticated user with pagination.
      operationId: v2GetBroadcasts
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Broadcasts retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastsResponse'
    post:
      summary: Create a Broadcast
      description: |
        Create a new broadcast campaign in draft state. After creation, configure the broadcast through the web interface or use the update endpoint to set content and scheduling details.
      operationId: v2CreateBroadcast
      tags:
        - Broadcasts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BroadcastCreate'
            example:
              name: January Newsletter
              subject: "Your January Newsletter is Here!"
              content_html: "<html><body><h1>Newsletter</h1><p>Your monthly update.</p></body></html>"
              content_text: "Newsletter - Your monthly update."
              group_id: 1
      responses:
        '201':
          description: Broadcast created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastResponse'
        '422':
          $ref: '#/components/responses/ValidationError'

  /broadcasts/{id}:
    get:
      summary: Get a Broadcast
      description: Retrieve a specific broadcast campaign by ID, including its group and content.
      operationId: v2GetBroadcast
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Broadcast retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a Broadcast
      description: |
        Update a broadcast campaign. A broadcast whose send is in flight, or holding mid-send and
        resumable, cannot be modified: the request is refused with `422 BROADCAST_LOCKED` and
        `error.details.schedule_status` names the state that blocked it. A broadcast scheduled
        for later stays editable, and so does one whose send has finished — nothing can change
        underneath it, and the dashboard allows the same edit.
      operationId: v2UpdateBroadcast
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BroadcastUpdate'
      responses:
        '200':
          description: Broadcast updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Broadcast is currently sending or already sent
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      summary: Delete a Broadcast
      description: Delete a broadcast campaign. Broadcasts that are currently sending cannot be deleted.
      operationId: v2DeleteBroadcast
      tags:
        - Broadcasts
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Broadcast deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # SENDING DOMAINS
  # ============================================
  /sending-domains/policy:
    get:
      summary: Get Sending Domain Policy
      description: |
        Describe what the authenticated account may do with sending domains, resolved from your plan: whether ownership verification is required, the domain quota, which features your plan **forces** (and therefore cannot be disabled), whether DKIM keys may be regenerated, which selectors are editable, which masking types are offered, SSL availability, bounce/DMARC defaults and the DNS lookup provider used for verification.

        Call this before creating domains to avoid predictable `422` refusals. Mumara ONE adds details of your plan and your tracking and bounce setup under `addons`.
      operationId: v2GetSendingDomainPolicy
      tags:
        - Sending Domains
      responses:
        '200':
          description: Policy resolved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendingDomainPolicyResponse'
              example:
                success: true
                data:
                  user_id: 7
                  license:
                    type: Commercial ESP
                    commercial_esp: true
                  ownership_verification_required: true
                  ownership_methods: [dns_txt, file_upload]
                  primary_domain_ready: true
                  domain_quota:
                    limit: 10
                    unlimited: false
                    used: 4
                    remaining: 6
                    can_add: true
                  duplicates_allowed: false
                  eligibility_checks:
                    enabled: true
                    domain_age:
                      enabled: true
                      min_days: 30
                    blacklist:
                      enabled: true
                  features:
                    dkim:
                      available: true
                      forced: true
                      forced_by: package
                      default_enabled: true
                      selector_editable: false
                      default_selector: key
                      key_size_default: 2048
                      key_sizes: [512, 1024, 2048, 4096]
                      regenerate_allowed: false
                    tracking:
                      available: true
                      forced: false
                      forced_by: null
                      default_enabled: true
                      selector_editable: true
                      default_selector: click
                      masking_types: [cname, htaccess]
                      default_masking_type: cname
                      cname_targets: [track.mumara-app.com]
                    bounce:
                      available: true
                      forced: false
                      forced_by: null
                      default_enabled: false
                      selector_editable: true
                      default_selector: bounce
                      spf_include: spf.mumara.net
                      mx_target: return.mumara.net
                    dmarc:
                      available: true
                      default_enabled: true
                      suggested_record: "v=DMARC1; p=none;"
                      policy: none
                    ssl:
                      available: true
                      forced: false
                      default_enabled: false
                  verification:
                    cooldown_minutes: 10
                    dns_lookup_provider: google_doh
                    current_value_lookup_provider: dig
                    unauthenticated_sending_allowed: true
                  addons: {}
        '403':
          $ref: '#/components/responses/Forbidden'

  /sending-domains:
    get:
      summary: List All Sending Domains
      description: Retrieve all sending domains with their composite status, ownership state and per-feature verification state. Sandbox domains provisioned by the platform are included; pass `include_sandbox=false` to hide them.
      operationId: v2GetSendingDomains
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: status
          in: query
          description: Filter by composite status code
          schema:
            $ref: '#/components/schemas/DomainStatusCode'
        - name: is_verified
          in: query
          description: Filter by ownership verification (`true` = ownership verified)
          schema:
            type: boolean
        - name: search
          in: query
          description: Case-insensitive substring match on the domain name
          schema:
            type: string
        - name: include_sandbox
          in: query
          description: Sandbox domains are included by default; pass `false` to hide them
          schema:
            type: boolean
            default: true
      responses:
        '200':
          description: Sending domains retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendingDomainsResponse'
    post:
      summary: Create a Sending Domain
      description: |
        Add a new sending domain. DKIM keys are generated immediately (unless `enable_dkim` is `false`). The response tells you exactly what to do next:

        **Phase 1 — ownership (only when ownership verification is required):**
        the domain is created as `pending_ownership_verification`; `data.ownership.methods` carries the TXT record (recommended) and the file-upload alternative. No feature records are issued until ownership is proven via `POST /sending-domains/{id}/verify-ownership`.

        **Phase 2 — authentication records:** once ownership is verified (or when verification is not required), `data.dns_records` lists **every** record to publish for the enabled features — DKIM TXT, tracking CNAME (or a tracking file for `htaccess`/`cloak` masking), bounce SPF + MX, DMARC — and `next_step` points at `POST /sending-domains/{id}/verify`.

        **Phase 3 — confirm:** call `verify`; when every enabled record passes the domain becomes `active`.

        Feature defaults come from your plan: DKIM and tracking default to enabled, bounce is enabled when your plan forces it, DMARC follows Mumara ONE's default, SSL is on when Mumara ONE forces secure tracking URLs. Explicitly disabling a feature your plan forces is refused with `FEATURE_FORCED_BY_PACKAGE` (and `enable_ssl: false` with `SSL_FORCED` while secure URLs are enforced). Custom selectors are accepted only when Mumara ONE allows editing them. Admin tokens may create a domain for another account with `user_id`.
      operationId: v2CreateSendingDomain
      tags:
        - Sending Domains
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendingDomainCreate'
            example:
              domain: mail.example.com
              masking_type: cname
              enable_dkim: true
              enable_tracking: true
              enable_bounce: true
              enable_dmarc: true
              enable_ssl: false
              key_size: 2048
      responses:
        '201':
          description: Sending domain created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendingDomainResponse'
              examples:
                ownershipVerificationRequired:
                  summary: Ownership verification required
                  value:
                    success: true
                    message: Sending domain added. Prove ownership of mail.example.com before authentication records are issued (see ownership.methods and next_step).
                    data:
                      id: 118
                      user_id: 7
                      domain: mail.example.com
                      masking_type: cname
                      is_sandbox: false
                      status:
                        code: pending_ownership_verification
                        label: Pending ownership verification
                        can_send: false
                        description: Publish the ownership TXT record (or upload the verification file), then call verify-ownership.
                      ownership:
                        required: true
                        verified: false
                        state: pending
                        methods:
                          - method: dns_txt
                            recommended: true
                            record:
                              type: TXT
                              host: 3f2b9c1a7e.mail.example.com
                              name: 3f2b9c1a7e.mail
                              value: 1724500000d41d8cd98f00b204e9800998ecf8427e
                              ttl: 3600
                              instructions: Create a TXT record for host 3f2b9c1a7e.mail.example.com with exactly this value.
                          - method: file_upload
                            recommended: false
                            file:
                              file_name: e4d909c290d0fb1ca068ffaddf22cbd0.html
                              content: e4d909c290d0fb1ca068ffaddf22cbd0
                              url: https://mail.example.com/e4d909c290d0fb1ca068ffaddf22cbd0.html
                              instructions: Upload the file so it is publicly readable at this URL; its body must be exactly the content.
                      domain_status_code: 0
                      is_enabled: false
                      configuration_confirmed: false
                      dkim:
                        enabled: true
                        forced: true
                        selector: key
                        host: key._domainkey.mail.example.com
                        status: pending
                        has_keys: true
                      tracking:
                        enabled: true
                        forced: false
                        selector: click
                        host: click.mail.example.com
                        masking_type: cname
                        target: track.mumara-app.com
                        status: pending
                        redirect_status: pending
                        ssl:
                          enabled: false
                          effective: false
                          forced: false
                          available: true
                      bounce:
                        available: true
                        enabled: false
                        forced: false
                        selector: bounce
                        host: bounce.mail.example.com
                        spf:
                          status: pending
                          include_domain: spf.mumara.net
                        mx:
                          status: pending
                          target: return.mumara.net
                          priority: 10
                      dmarc:
                        enabled: true
                        host: _dmarc.mail.example.com
                        organizational_host: _dmarc.example.com
                        status: pending
                        suggested_value: "v=DMARC1; p=none;"
                      last_verified_at: null
                      dns_records: []
                      files: []
                      next_step:
                        action: verify_ownership
                        method: POST
                        endpoint: https://one.mumara.com/api/v2/sending-domains/118/verify-ownership
                        body:
                          method: dns_txt
                        description: After the TXT record propagates (or the file is uploaded), call this endpoint. On success all remaining DNS records are returned.
                      warnings: []
                      created_at: '2026-08-24T10:00:00+00:00'
                      updated_at: '2026-08-24T10:00:00+00:00'
                ownershipVerificationNotRequired:
                  summary: Ownership verification disabled — records issued immediately
                  value:
                    success: true
                    message: Sending domain added. Publish the 5 DNS record(s) in dns_records, then call POST https://one.mumara.com/api/v2/sending-domains/119/verify.
                    data:
                      id: 119
                      user_id: 7
                      domain: news.example.com
                      masking_type: cname
                      is_sandbox: false
                      status:
                        code: pending_authentication
                        label: Pending authentication
                        can_send: true
                        description: Publish the DNS records below and call verify to authenticate the domain.
                      ownership:
                        required: false
                        verified: true
                        state: not_required
                      domain_status_code: 4
                      is_enabled: true
                      configuration_confirmed: false
                      dkim:
                        enabled: true
                        forced: false
                        selector: key
                        host: key._domainkey.news.example.com
                        status: pending
                        has_keys: true
                      tracking:
                        enabled: true
                        forced: false
                        selector: click
                        host: click.news.example.com
                        masking_type: cname
                        target: track.mumara-app.com
                        status: pending
                        redirect_status: pending
                        ssl:
                          enabled: false
                          effective: false
                          forced: false
                          available: true
                      bounce:
                        available: true
                        enabled: true
                        forced: false
                        selector: bounce
                        host: bounce.news.example.com
                        spf:
                          status: pending
                          include_domain: spf.mumara.net
                        mx:
                          status: pending
                          target: return.mumara.net
                          priority: 10
                      dmarc:
                        enabled: true
                        host: _dmarc.news.example.com
                        organizational_host: _dmarc.example.com
                        status: pending
                        suggested_value: "v=DMARC1; p=none;"
                      last_verified_at: null
                      dns_records:
                        - feature: dkim
                          type: TXT
                          host: key._domainkey.news.example.com
                          name: key._domainkey.news
                          value: "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
                          ttl: 3600
                          status: pending
                          instructions: Publish this TXT record exactly. Long values may need to be split into 255-character strings by your DNS provider.
                        - feature: tracking
                          type: CNAME
                          host: click.news.example.com
                          name: click.news
                          value: track.mumara-app.com
                          ttl: 3600
                          status: pending
                          instructions: Point click.news.example.com to track.mumara-app.com via CNAME.
                        - feature: bounce_spf
                          type: TXT
                          host: bounce.news.example.com
                          name: bounce.news
                          value: "v=spf1 include:spf.mumara.net ~all"
                          ttl: 3600
                          status: pending
                          instructions: Publish this SPF record on the bounce host.
                        - feature: bounce_mx
                          type: MX
                          host: bounce.news.example.com
                          name: bounce.news
                          value: return.mumara.net
                          ttl: 3600
                          priority: 10
                          status: pending
                          instructions: Create an MX record with priority 10 pointing at return.mumara.net.
                        - feature: dmarc
                          type: TXT
                          host: _dmarc.news.example.com
                          name: _dmarc.news
                          value: "v=DMARC1; p=none;"
                          organizational_host: _dmarc.example.com
                          ttl: 3600
                          status: pending
                          instructions: Any valid record starting with v=DMARC1 passes; the suggested value reflects Application Settings defaults. A policy already published at _dmarc.example.com also covers this subdomain and satisfies verification on its own.
                      files: []
                      next_step:
                        action: verify_dns_records
                        method: POST
                        endpoint: https://one.mumara.com/api/v2/sending-domains/119/verify
                        description: Publish the 5 records in dns_records, then call verify. The domain becomes active when all enabled records pass.
                      warnings: []
                      created_at: '2026-08-24T10:00:00+00:00'
                      updated_at: '2026-08-24T10:00:00+00:00'
        '403':
          description: The key doesn't have permission, or the account may not add domains
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: The domain is already registered (`DOMAIN_ALREADY_EXISTS`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  code: DOMAIN_ALREADY_EXISTS
                  message: mail.example.com already exists as a sending domain.
                  details:
                    field: domain
        '422':
          description: |
            Validation or policy refusal. Codes: `VALIDATION_ERROR`, `INVALID_DOMAIN`, `PRIMARY_DOMAIN_NOT_VERIFIED`, `DOMAIN_LIMIT_REACHED`, `DOMAIN_NOT_ALLOWED`, `DOMAIN_NOT_ELIGIBLE`, `FEATURE_FORCED_BY_PACKAGE`, `FEATURE_UNAVAILABLE`, `SSL_FORCED`, `MASKING_TYPE_UNAVAILABLE`, `SELECTOR_EDIT_DISABLED`, `INVALID_SELECTOR`, `HOOK_ABORT`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                limitReached:
                  summary: Your plan's limit is reached
                  value:
                    success: false
                    error:
                      code: DOMAIN_LIMIT_REACHED
                      message: The maximum number of sending domains allowed by the package (10) has been reached.
                      details:
                        limit: 10
                        used: 10
                featureForced:
                  summary: Package forces DKIM
                  value:
                    success: false
                    error:
                      code: FEATURE_FORCED_BY_PACKAGE
                      message: "DKIM signing cannot be disabled: your package requires it on every sending domain."
                      details:
                        feature: dkim
                        forced_by: package
                        package_id: 3
                        policy_endpoint: https://one.mumara.com/api/v2/sending-domains/policy
                sslForced:
                  summary: enable_ssl:false while secure tracking URLs are enforced
                  value:
                    success: false
                    error:
                      code: SSL_FORCED
                      message: Secure tracking URLs are enforced for every sending domain and cannot be disabled per domain.
                      details:
                        feature: ssl
                        field: enable_ssl
        '503':
          description: The domain eligibility service (age / blacklist pre-check) could not be reached; the domain was not added (`ELIGIBILITY_CHECK_FAILED`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  code: ELIGIBILITY_CHECK_FAILED
                  message: The domain eligibility service could not be reached; the domain was not added. Try again later.
                  details:
                    check: blacklist
                    error: "Connection timed out"

  /sending-domains/{id}:
    get:
      summary: Get a Sending Domain
      description: Retrieve a sending domain with its composite status, ownership state, per-feature configuration and verification state, the DNS records still relevant to it and the recommended next step.
      operationId: v2GetSendingDomain
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Sending domain retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendingDomainResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a Sending Domain
      description: |
        Change the masking type, toggle features, or rename selectors in one call. Every change is validated against your plan before anything is written. A single refused change is reported with its own code and `details.field`; when several changes are refused you receive one `422 UPDATE_REJECTED` whose `details.fields` is keyed by request field (each entry carrying `code` and `message`). Nothing is written in either case.

        - Disabling a feature your plan forces → `FEATURE_FORCED_BY_PACKAGE`
        - Selector changes are only accepted when Mumara ONE allows editing that selector (`SELECTOR_EDIT_DISABLED`); a changed selector resets that feature's verification and the domain returns to `pending_authentication`
        - A masking type Mumara ONE doesn't offer → `MASKING_TYPE_UNAVAILABLE`
        - Turning SSL off while *Force secure tracking URLs* is on → `SSL_FORCED`

        Prefer `PATCH /sending-domains/{id}/features/{feature}` for a single toggle.
      operationId: v2UpdateSendingDomain
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendingDomainUpdate'
            example:
              masking_type: htaccess
              enable_dmarc: true
              tracking_selector: track
      responses:
        '200':
          description: Sending domain updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendingDomainResponse'
        '403':
          $ref: '#/components/responses/SendingDomainForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: |
            Codes: `VALIDATION_ERROR`, `FEATURE_FORCED_BY_PACKAGE`, `FEATURE_UNAVAILABLE`, `MASKING_TYPE_UNAVAILABLE`, `INVALID_SELECTOR`, `SELECTOR_EDIT_DISABLED`, `SSL_FORCED`, `DOMAIN_SUSPENDED`, `OWNERSHIP_VERIFICATION_REQUIRED`, `UPDATE_REJECTED` (several changes refused — `details.fields` keyed by request field), `HOOK_ABORT`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                singleChangeRefused:
                  summary: One change refused
                  value:
                    success: false
                    error:
                      code: SELECTOR_EDIT_DISABLED
                      message: Changing the tracking selector has been disabled by the administrator (Application Settings > Domains).
                      details:
                        field: tracking_selector
                        code: SELECTOR_EDIT_DISABLED
                        message: Changing the tracking selector has been disabled by the administrator (Application Settings > Domains).
                severalChangesRefused:
                  summary: Several changes refused at once
                  value:
                    success: false
                    error:
                      code: UPDATE_REJECTED
                      message: Some of the requested changes are not allowed; nothing was changed.
                      details:
                        fields:
                          tracking_selector:
                            code: INVALID_SELECTOR
                            message: "A selector must be a single DNS label: lower-case letters, digits and hyphens, 1-63 characters, not starting or ending with a hyphen."
                          bounce_selector:
                            code: SELECTOR_EDIT_DISABLED
                            message: Changing the bounce selector has been disabled by the administrator (Application Settings > Domains).
    delete:
      summary: Delete a Sending Domain
      description: |
        Delete a sending domain. If contact lists, triggers, web forms or other items still reference the domain, the request is refused with `409 DOMAIN_IN_USE` and `error.details.connected_assets` lists them. Pass `reassign_to_domain_id` (another sending domain of the same account) to move those assets to it and complete the deletion in one call.
      operationId: v2DeleteSendingDomain
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendingDomainDeleteRequest'
            example:
              reassign_to_domain_id: 42
      responses:
        '200':
          description: Sending domain deleted successfully. `data` is `null` unless assets were moved with `reassign_to_domain_id`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendingDomainDeleteResponse'
              examples:
                plain:
                  summary: No connected assets
                  value:
                    success: true
                    message: Sending domain deleted successfully
                reassigned:
                  summary: Assets moved to another domain
                  value:
                    success: true
                    message: Sending domain deleted; its assets were moved to news.example.com.
                    data:
                      reassigned_to:
                        id: 42
                        domain: news.example.com
                      moved_assets:
                        lists:
                          - id: 12
                            name: Newsletter
                          - id: 15
                            name: Customers
                        sending_nodes:
                          - id: 3
                            name: Primary SMTP
        '403':
          $ref: '#/components/responses/SendingDomainForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/DomainInUse'
        '422':
          description: Codes `REASSIGN_TARGET_INVALID`, `HOOK_ABORT`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  code: REASSIGN_TARGET_INVALID
                  message: reassign_to_domain_id must be another sending domain of the same account.
                  details:
                    field: reassign_to_domain_id

  /sending-domains/{id}/dns-records:
    get:
      summary: Get DNS Records
      description: |
        List every DNS record to publish for the features enabled on this domain — DKIM TXT, tracking CNAME (or, for `htaccess`/`cloak` masking, the tracking file in `files`), bounce SPF + MX (only when bounce DNS verification is offered), DMARC — each with its verification `status`.

        **DMARC on a subdomain:** the suggested `host` is always the sending domain's own `_dmarc` host, but receivers fall back to the organizational domain when a subdomain publishes no policy (RFC 7489 §6.6.3), so a record at `organizational_host` satisfies verification on its own. Publishing there protects every address on the domain rather than this subdomain alone — but only one DMARC record is allowed per host, so extend an existing record instead of replacing it.

        Set `include_current_values=true` to also look up what is currently published at each host (uses the "current value" DNS lookup provider Mumara ONE uses; adds latency).

        Before ownership is verified this endpoint returns `records: []` together with the `ownership.methods` block and a `next_step` of `verify_ownership`.
      operationId: v2GetSendingDomainDnsRecords
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - name: include_current_values
          in: query
          description: Also return the value currently published in DNS for each record
          schema:
            type: boolean
            default: false
        - name: features
          in: query
          description: Comma-separated subset of features to return. Accepts `dkim`, `tracking`, `bounce` (both bounce records), `spf` / `bounce_spf`, `mx` / `bounce_mx`, `dmarc`
          schema:
            type: string
            example: dkim,bounce
      responses:
        '200':
          description: DNS records retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DnsRecordsResponse'
              example:
                success: true
                data:
                  domain: mail.example.com
                  status:
                    code: authentication_failed
                    label: Authentication failed
                    can_send: true
                    description: One or more records failed. Sending is still permitted because unauthenticated sending is allowed by the administrator.
                  ownership:
                    required: true
                    verified: true
                    state: verified
                  records:
                    - feature: dkim
                      type: TXT
                      host: key._domainkey.mail.example.com
                      name: key._domainkey.mail
                      value: "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
                      ttl: 3600
                      status: verified
                      current_value: "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
                      instructions: Publish this TXT record exactly. Long values may need to be split into 255-character strings by your DNS provider.
                    - feature: tracking
                      type: CNAME
                      host: click.mail.example.com
                      name: click.mail
                      value: track.mumara-app.com
                      ttl: 3600
                      status: failed
                      status_detail: redirect_mismatch
                      current_value: old-cdn.example.net.
                      instructions: Point click.mail.example.com to track.mumara-app.com via CNAME. Mumara confirms by fetching http://click.mail.example.com/version.
                    - feature: bounce_spf
                      type: TXT
                      host: bounce.mail.example.com
                      name: bounce.mail
                      value: "v=spf1 include:spf.mumara.net ~all"
                      ttl: 3600
                      status: pending
                      current_value: null
                      instructions: Publish this SPF record on the bounce host.
                    - feature: bounce_mx
                      type: MX
                      host: bounce.mail.example.com
                      name: bounce.mail
                      value: return.mumara.net
                      ttl: 3600
                      priority: 10
                      status: pending
                      current_value: null
                      instructions: Create an MX record with priority 10 pointing at return.mumara.net.
                    - feature: dmarc
                      type: TXT
                      host: _dmarc.mail.example.com
                      name: _dmarc.mail
                      value: "v=DMARC1; p=none;"
                      organizational_host: _dmarc.example.com
                      ttl: 3600
                      status: pending
                      current_value: "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"
                      current_value_source: _dmarc.example.com
                      instructions: Any valid record starting with v=DMARC1 passes; the suggested value reflects Application Settings defaults. This domain is a subdomain, so the policy at _dmarc.example.com already covers it and satisfies verification on its own — publishing there protects the whole domain, but only one DMARC record is allowed per host, so extend the existing record rather than replacing it.
                  files: []
                  summary:
                    total: 5
                    verified: 1
                    pending: 3
                    failed: 1
                  dns_lookup_provider: google_doh
                  current_value_lookup_provider: dig
                  next_step:
                    action: fix_dns_records
                    method: GET
                    endpoint: https://one.mumara.com/api/v2/sending-domains/118/dns-records?include_current_values=1
                    description: "Fix the failed record(s): tracking. Then call POST https://one.mumara.com/api/v2/sending-domains/118/verify again."
        '403':
          $ref: '#/components/responses/SendingDomainForbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /sending-domains/{id}/verify-ownership:
    post:
      summary: Verify Domain Ownership
      description: |
        Prove that you control the domain. Mumara ONE checks the ownership TXT record (`method: dns_txt`, default) or fetches the verification file (`method: file_upload`) using Mumara ONE's DNS lookup — exactly as **Setup → Sending Domains** does.

        - **Success (200):** ownership is recorded, the domain moves to `pending_authentication`, and the response already contains **all** DNS records to publish next (`dns_records` / `files`).
        - **Failure (422 `OWNERSHIP_VERIFICATION_FAILED`):** `error.details` explains what was expected, what was found, and how to proceed.
        - Already verified, or verification not required → `200` with the current state (idempotent).
        - Ownership reset by Mumara ONE (`403 OWNERSHIP_REVOKED`) cannot be re-verified by the account itself; the sandbox domain cannot be changed by clients (`403 SANDBOX_DOMAIN`).

        On success `data` is the full sending domain (as returned by `GET /sending-domains/{id}`) with `ownership.method` / `ownership.checked` added and `dns_records` / `files` already populated.
      operationId: v2VerifySendingDomainOwnership
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyOwnershipRequest'
            example:
              method: dns_txt
      responses:
        '200':
          description: Ownership verified (or already verified / not required)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyOwnershipResponse'
              example:
                success: true
                message: Domain ownership verified. Publish the 3 DNS record(s) in dns_records, then call POST https://one.mumara.com/api/v2/sending-domains/118/verify.
                data:
                  id: 118
                  domain: mail.example.com
                  ownership:
                    required: true
                    verified: true
                    state: verified
                    method: dns_txt
                    checked:
                      host: 3f2b9c1a7e.mail.example.com
                      url: null
                      expected: 1724500000d41d8cd98f00b204e9800998ecf8427e
                      found: 1724500000d41d8cd98f00b204e9800998ecf8427e
                  status:
                    code: pending_authentication
                    label: Pending authentication
                    can_send: true
                    description: The DNS records of the enabled features have not been verified yet. Sending is still permitted because the administrator allows unauthenticated sending domains.
                  dns_records:
                    - feature: dkim
                      type: TXT
                      host: key._domainkey.mail.example.com
                      name: key._domainkey.mail
                      value: "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
                      ttl: 3600
                      status: pending
                      instructions: Publish this TXT record exactly.
                    - feature: tracking
                      type: CNAME
                      host: click.mail.example.com
                      name: click.mail
                      value: track.mumara-app.com
                      ttl: 3600
                      status: pending
                      instructions: Point click.mail.example.com to track.mumara-app.com via CNAME.
                    - feature: dmarc
                      type: TXT
                      host: _dmarc.mail.example.com
                      name: _dmarc.mail
                      value: "v=DMARC1; p=none;"
                      organizational_host: _dmarc.example.com
                      ttl: 3600
                      status: pending
                      instructions: Any valid record starting with v=DMARC1 passes. A policy already published at _dmarc.example.com also covers this subdomain and satisfies verification on its own.
                  files: []
                  next_step:
                    action: verify_dns_records
                    method: POST
                    endpoint: https://one.mumara.com/api/v2/sending-domains/118/verify
                    description: Publish the records listed in dns_records (GET https://one.mumara.com/api/v2/sending-domains/118/dns-records), then call verify. The domain becomes active once every enabled record passes.
        '403':
          description: |
            `OWNERSHIP_REVOKED` — Mumara ONE reset the verification; contact support to have it reviewed. `SANDBOX_DOMAIN` — the sandbox domain Mumara ONE created for your account can't be changed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  code: OWNERSHIP_REVOKED
                  message: An administrator has reset the ownership verification of this domain; only an administrator can verify it again.
                  details:
                    next_step:
                      action: contact_administrator
                      method: null
                      endpoint: null
                      description: This state can only be changed by an administrator.
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/OwnershipVerificationFailed'

  /sending-domains/{id}/verify:
    post:
      summary: Verify DNS Records
      description: |
        Check every DNS record required by the enabled features (or only those listed in `records`) against live DNS using Mumara ONE's DNS lookup, and update the domain status. This is a **report**: it always returns `200` with the outcome of every record — expected value, value found, `status`, `status_detail` and a human-readable `message` — on top of the full sending domain object, plus `hooks_fired` (the add-on hooks queued by this run) and the `cooldown` that now applies.

        - Ownership must be verified first (`422 OWNERSHIP_VERIFICATION_REQUIRED`).
        - Re-checks are throttled by the re-check cooldown (`429 VERIFICATION_COOLDOWN` with a `Retry-After` header). The first confirmation is never throttled.
        - Tracking verification also performs an HTTP request against the tracking host, so this call can take several seconds.
        - **DMARC follows the RFC 7489 §6.6.3 discovery chain**, exactly as receivers do: `_dmarc.<domain>` first and, only when that publishes no valid policy, `_dmarc.<organizational domain>`. A subdomain that inherits its parent's policy therefore verifies, with `status_detail` = `inherited` and `host` naming the record that satisfied the check. When neither host publishes a policy the `message` names both.
        - A DNS lookup no resolver can answer is reported as `pending`, never `failed`, and leaves the stored verdict untouched — an outage never reads as a removed record.
      operationId: v2VerifySendingDomain
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyRecordsRequest'
            example:
              records: [dkim, tracking]
      responses:
        '200':
          description: Verification report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyDomainResponse'
              example:
                success: true
                message: "Verification completed: 2 verified, 3 failed."
                data:
                  domain: mail.example.com
                  checked_at: '2026-08-24T10:15:00+00:00'
                  records:
                    - feature: dkim
                      type: TXT
                      host: key._domainkey.mail.example.com
                      expected: "v=DKIM1; k=rsa; p=MIIB..."
                      found: "v=DKIM1; k=rsa; p=MIIB..."
                      status: verified
                      status_detail: null
                      message: DKIM record matches.
                    - feature: tracking
                      type: CNAME
                      host: click.mail.example.com
                      expected: track.mumara-app.com
                      found: track.mumara-app.com.
                      status: failed
                      status_detail: ssl_broken
                      message: The host points at Mumara but https://click.mail.example.com does not present a valid certificate. Install a certificate or disable SSL for this domain.
                    - feature: bounce_spf
                      type: TXT
                      host: bounce.mail.example.com
                      expected: "v=spf1 include:spf.mumara.net ~all"
                      found: "v=spf1 include:_spf.google.com ~all"
                      status: failed
                      status_detail: missing_include
                      message: An SPF record exists but does not include spf.mumara.net.
                    - feature: bounce_mx
                      type: MX
                      host: bounce.mail.example.com
                      expected: return.mumara.net
                      found: return.mumara.net
                      status: verified
                      status_detail: null
                      message: MX record matches.
                    - feature: dmarc
                      type: TXT
                      host: _dmarc.mail.example.com
                      expected: "v=DMARC1; p=none;"
                      found: null
                      status: failed
                      status_detail: not_found
                      message: No DMARC record was found at _dmarc.mail.example.com or at _dmarc.example.com, which would otherwise cover this subdomain.
                  summary:
                    total: 5
                    verified: 2
                    failed: 3
                    pending: 0
                    skipped: []
                  status:
                    code: authentication_failed
                    label: Authentication failed
                    can_send: true
                    description: One or more DNS records failed verification. Sending is still permitted because the administrator allows unauthenticated sending domains.
                  dns_lookup_provider: google_doh
                  hooks_fired: [SendingDomainDKIMPassed, SendingDomainMXPassed, SendingDomainDnsFailed]
                  cooldown:
                    cooldown_minutes: 10
                    next_verification_allowed_at: '2026-08-24T10:25:00+00:00'
                    applies: true
                  next_step:
                    action: fix_dns_records
                    method: GET
                    endpoint: https://one.mumara.com/api/v2/sending-domains/118/dns-records?include_current_values=1
                    description: "Fix the failed record(s): tracking, bounce_spf, dmarc. Then call POST https://one.mumara.com/api/v2/sending-domains/118/verify again."
        '403':
          $ref: '#/components/responses/SendingDomainForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/OwnershipVerificationRequired'
        '429':
          $ref: '#/components/responses/VerificationCooldown'

  /sending-domains/{id}/features/{feature}:
    patch:
      summary: Enable or Disable a Feature
      description: |
        Toggle one feature of the domain: `dkim`, `tracking`, `bounce`, `dmarc`, `ssl`, or `status` (the domain's own on/off switch). The change is validated against your plan **before** it is written, so a refusal never leaves the domain half-changed:

        | Feature | Refused when |
        |---------|--------------|
        | `dkim` / `tracking` / `bounce` | disabling a feature your plan forces → `FEATURE_FORCED_BY_PACKAGE` |
        | `bounce` | bounce DNS verification isn't available in Mumara ONE → `FEATURE_UNAVAILABLE` |
        | `ssl` | disabling while *Force secure tracking URLs* is on → `SSL_FORCED`; enabling when the per-domain SSL switch is not offered → `FEATURE_UNAVAILABLE` |
        | `status` | the domain was suspended by Mumara ONE → `DOMAIN_SUSPENDED` |
        | any | ownership not yet verified → `OWNERSHIP_VERIFICATION_REQUIRED` |

        Enabling a feature that needs a DNS record returns that record in `data.dns_records`; enabling DKIM on a domain without keys generates them first. The response carries `data.changed` describing the transition; the call is idempotent — a feature already in the requested state returns `200` with `data.changed.changed: false` and no hooks fire. An unknown `{feature}` is a `404` (route constraint).
      operationId: v2SetSendingDomainFeature
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - name: feature
          in: path
          required: true
          description: Feature to toggle
          schema:
            type: string
            enum: [dkim, tracking, bounce, dmarc, ssl, status]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FeatureToggleRequest'
            example:
              enabled: true
      responses:
        '200':
          description: Feature updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeatureToggleResponse'
        '403':
          $ref: '#/components/responses/SendingDomainForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/FeatureForcedByPackage'

  /sending-domains/{id}/dkim/regenerate:
    post:
      summary: Regenerate DKIM Keys
      description: |
        Generate a new DKIM key pair for the domain (optionally with a different `key_size`). The previous public key stops being valid immediately, so publish the new TXT record (returned in `data.dns_records`) and call `verify` again; until then the domain is `pending_authentication`.

        Refused with `403 DKIM_REGENERATE_DISABLED` when key regeneration isn't available to your account, and with `422 HOOK_ABORT` (`details.code` `SERVER_REBUILD_IN_PROGRESS`) while your sending servers are being updated; try again in a few minutes.
      operationId: v2RegenerateSendingDomainDkim
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DkimRegenerateRequest'
            example:
              key_size: 2048
      responses:
        '200':
          description: New keys generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendingDomainResponse'
        '403':
          $ref: '#/components/responses/DkimRegenerateDisabled'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/SendingDomainHookAbort'
        '500':
          description: Key generation failed on the server (`DKIM_KEY_GENERATION_FAILED`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  code: DKIM_KEY_GENERATION_FAILED
                  message: DKIM key generation failed. Contact your administrator.

  /sending-domains/{id}/dkim/keys:
    get:
      summary: Get DKIM Public Key
      description: |
        Return the domain's DKIM selector, the TXT record value to publish and the public key in PEM form. **The private key is never exposed through the API**; it stays on the server and is used for signing.
      operationId: v2GetSendingDomainDkimKeys
      tags:
        - Sending Domains
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: DKIM public material
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DkimKeysResponse'
              example:
                success: true
                data:
                  domain: mail.example.com
                  selector: key
                  host: key._domainkey.mail.example.com
                  dns_value: "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
                  public_key_pem: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----\n"
                  key_size_bits: 2048
                  status: verified
                  record:
                    feature: dkim
                    type: TXT
                    host: key._domainkey.mail.example.com
                    name: key._domainkey.mail
                    value: "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
                    ttl: 3600
                    status: verified
                    status_detail: null
                    instructions: Publish this TXT record exactly as given. Values longer than 255 characters may need to be split into multiple quoted strings by your DNS provider; Mumara joins them before comparing.
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: No keys have been generated for this domain (`DKIM_KEYS_NOT_GENERATED`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  code: DKIM_KEYS_NOT_GENERATED
                  message: This domain has no DKIM keys yet. Call POST https://one.mumara.com/api/v2/sending-domains/118/dkim/regenerate to generate them.

  # ============================================
  # SUPPRESSIONS
  # ============================================
  /suppressions:
    get:
      summary: List All Suppressions
      description: |
        Retrieve suppression list entries. You **must** specify the `type` parameter to indicate which suppression list to query (email, domain, or IP).
      operationId: v2GetSuppressions
      tags:
        - Suppressions
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: type
          in: query
          required: true
          description: Type of suppression list to query
          schema:
            type: string
            enum: [email, domain, ip]
        - name: list_id
          in: query
          description: Filter by list ID (for list-scoped suppressions)
          schema:
            type: integer
        - name: reference
          in: query
          description: Filter by reference/label value
          schema:
            type: string
      responses:
        '200':
          description: Suppressions retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuppressionsResponse'
    post:
      summary: Create a Suppression
      description: |
        Add a new entry to the suppression list. Suppressed addresses/domains/IPs will not receive any emails.
      operationId: v2CreateSuppression
      tags:
        - Suppressions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuppressionCreate'
      responses:
        '201':
          description: Suppression created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuppressionResponse'
        '409':
          description: Suppression entry already exists
        '422':
          $ref: '#/components/responses/ValidationError'

  /suppressions/{id}:
    get:
      summary: Get a Suppression
      description: Retrieve a specific suppression entry. The `type` query parameter is required.
      operationId: v2GetSuppression
      tags:
        - Suppressions
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - name: type
          in: query
          required: true
          schema:
            type: string
            enum: [email, domain, ip]
      responses:
        '200':
          description: Suppression retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuppressionResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a Suppression
      description: Remove a suppression entry, allowing future emails to the address/domain/IP.
      operationId: v2DeleteSuppression
      tags:
        - Suppressions
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - name: type
          in: query
          required: true
          schema:
            type: string
            enum: [email, domain, ip]
      responses:
        '200':
          description: Suppression deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /suppressions/batch:
    post:
      summary: Batch Create Suppressions
      description: Add multiple suppression entries in a single request (up to 1,000).
      operationId: v2BatchCreateSuppressions
      tags:
        - Suppressions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuppressionBatchCreate'
      responses:
        '200':
          description: Batch operation completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      summary: Batch Delete Suppressions
      description: Remove multiple suppression entries by ID (up to 1,000). The `type` field is required.
      operationId: v2BatchDeleteSuppressions
      tags:
        - Suppressions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - type
                - ids
              properties:
                type:
                  type: string
                  enum: [email, domain, ip]
                ids:
                  type: array
                  items:
                    type: integer
                  maxItems: 1000
      responses:
        '200':
          description: Suppressions deleted successfully

  # ============================================
  # CUSTOM FIELDS
  # ============================================
  /custom-fields:
    get:
      summary: List All Custom Fields
      description: Retrieve all custom fields. Optionally filter by field type.
      operationId: v2GetCustomFields
      tags:
        - Custom Fields
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: list_id
          in: query
          description: Filter by associated list ID
          schema:
            type: integer
      responses:
        '200':
          description: Custom fields retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomFieldsResponse'
    post:
      summary: Create a Custom Field
      description: |
        Create a new custom field and automatically associate it with the specified list. The field's merge tag is auto-generated from the name.

        For `select`, `checkbox`, and `radio` types, provide the `options` array.
      operationId: v2CreateCustomField
      tags:
        - Custom Fields
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomFieldCreate'
      responses:
        '201':
          description: Custom field created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomFieldResponse'
        '422':
          $ref: '#/components/responses/ValidationError'

  /custom-fields/{id}:
    get:
      summary: Get a Custom Field
      description: Retrieve a specific custom field by ID.
      operationId: v2GetCustomField
      tags:
        - Custom Fields
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Custom field retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomFieldResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a Custom Field
      description: |
        Update a custom field's options or settings. Field name and type cannot be changed
        after creation, and the default fields that ship with the product are refused with
        `403 FIELD_IS_DEFAULT`.
      operationId: v2UpdateCustomField
      tags:
        - Custom Fields
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomFieldUpdate'
      responses:
        '200':
          description: Custom field updated successfully
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      summary: Delete a Custom Field
      description: |
        Delete a custom field. If the field has associated subscriber data, pass `force: true` to confirm deletion. Without `force`, the API returns an error if data exists.
        The default fields that ship with the product cannot be deleted and are refused with `403 FIELD_IS_DEFAULT`.
      operationId: v2DeleteCustomField
      tags:
        - Custom Fields
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - name: force
          in: query
          description: Force deletion even if subscriber data exists
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Custom field deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Field has associated data—use force=true to confirm

  /custom-fields/add-to-list:
    post:
      summary: Link Custom Field to a List
      description: |
        Associate an existing custom field with a list by providing both IDs in the request body. This is an alternative to `POST /lists/{listId}/custom-fields` for when you prefer to pass the list ID in the body rather than the URL.
      operationId: v2LinkCustomFieldToList
      tags:
        - Custom Fields
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - custom_field_id
                - list_id
              properties:
                custom_field_id:
                  type: integer
                  description: ID of the custom field to link
                  example: 5
                list_id:
                  type: integer
                  description: ID of the list to link the field to
                  example: 1
      responses:
        '200':
          description: Custom field linked to list successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Custom field or list not found
        '409':
          description: Custom field is already linked to this list
        '422':
          $ref: '#/components/responses/ValidationError'

  /lists/{listId}/custom-fields:
    get:
      summary: Get Custom Fields for a List
      description: Retrieve all custom fields associated with a specific list.
      operationId: v2GetListCustomFields
      tags:
        - Custom Fields
      parameters:
        - name: listId
          in: path
          required: true
          description: ID of the list
          schema:
            type: integer
      responses:
        '200':
          description: Custom fields retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomFieldsResponse'
        '404':
          description: List not found
    post:
      summary: Add Custom Field to a List
      description: Associate an existing custom field with a list. This allows contacts in the list to have values for this field.
      operationId: v2AddCustomFieldToList
      tags:
        - Custom Fields
      parameters:
        - name: listId
          in: path
          required: true
          description: ID of the list
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - custom_field_id
              properties:
                custom_field_id:
                  type: integer
                  description: ID of the custom field to associate
      responses:
        '200':
          description: Custom field added to list successfully
        '404':
          description: List or custom field not found

  # ============================================
  # SPIN TAGS
  # ============================================
  /spin-tags:
    get:
      summary: List All Spin Tags
      description: Retrieve all spin tags for dynamic content rotation.
      operationId: v2GetSpinTags
      tags:
        - Spin Tags
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Spin tags retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpinTagsResponse'
    post:
      summary: Create a Spin Tag
      description: |
        Create a new spin tag for dynamic content rotation. Provide a placeholder name and a list of word/phrase variations. When used in broadcasts, Mumara randomly selects one variation per recipient.

        **Usage in broadcasts:** Insert `{spin:placeholder_name}` in your email content.
      operationId: v2CreateSpinTag
      tags:
        - Spin Tags
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpinTagCreate'
            example:
              placeholder: greeting
              words:
                - Hello
                - Hi
                - Hey
                - Greetings
      responses:
        '201':
          description: Spin tag created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpinTagResponse'
        '409':
          description: A spin tag with this placeholder already exists
        '422':
          $ref: '#/components/responses/ValidationError'

  /spin-tags/{id}:
    get:
      summary: Get a Spin Tag
      description: Retrieve a specific spin tag by ID.
      operationId: v2GetSpinTag
      tags:
        - Spin Tags
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Spin tag retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpinTagResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a Spin Tag
      description: Update a spin tag's word list. The placeholder name cannot be changed after creation.
      operationId: v2UpdateSpinTag
      tags:
        - Spin Tags
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpinTagUpdate'
      responses:
        '200':
          description: Spin tag updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpinTagResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      summary: Delete a Spin Tag
      description: Delete a spin tag.
      operationId: v2DeleteSpinTag
      tags:
        - Spin Tags
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Spin tag deleted successfully
        '404':
          $ref: '#/components/responses/NotFound'

  /spin-tags/batch:
    delete:
      summary: Batch Delete Spin Tags
      description: Delete multiple spin tags by ID (max 100).
      operationId: v2BatchDeleteSpinTags
      tags:
        - Spin Tags
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchDeleteRequest'
      responses:
        '200':
          description: Spin tags deleted successfully

  # ============================================
  # SEGMENTS
  # ============================================
  /segments:
    get:
      summary: List All Segments
      description: Retrieve all contact segments with pagination.
      operationId: v2GetSegments
      tags:
        - Segments
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: list_id
          in: query
          description: Filter segments that include this list
          schema:
            type: integer
      responses:
        '200':
          description: Segments retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SegmentsResponse'
    post:
      summary: Create a Segment
      description: |
        Create a new contact segment with filtering conditions. Segments dynamically select contacts from one or more lists based on rules.
      operationId: v2CreateSegment
      tags:
        - Segments
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SegmentCreate'
      responses:
        '201':
          description: Segment created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SegmentResponse'
        '422':
          $ref: '#/components/responses/ValidationError'

  /segments/{id}:
    get:
      summary: Get a Segment
      description: Retrieve a specific segment with its filtering conditions and associated lists.
      operationId: v2GetSegment
      tags:
        - Segments
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Segment retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SegmentResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a Segment
      description: Update a segment's name or filtering conditions.
      operationId: v2UpdateSegment
      tags:
        - Segments
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SegmentUpdate'
      responses:
        '200':
          description: Segment updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SegmentResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a Segment
      description: Delete a segment. This does not delete the contacts—it only removes the filter definition.
      operationId: v2DeleteSegment
      tags:
        - Segments
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Segment deleted successfully
        '404':
          $ref: '#/components/responses/NotFound'

  /segments/{id}/contacts-count:
    get:
      summary: Get Segment Contacts Count
      description: Calculate and return the number of contacts that currently match this segment's filtering criteria.
      operationId: v2GetSegmentContactsCount
      tags:
        - Segments
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Contacts count retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      segment_id:
                        type: integer
                      segment_name:
                        type: string
                      contacts_count:
                        type: integer
              example:
                success: true
                data:
                  segment_id: 5
                  segment_name: Active Buyers
                  contacts_count: 1423
        '404':
          $ref: '#/components/responses/NotFound'

  /segments/batch:
    delete:
      summary: Batch Delete Segments
      description: Delete multiple segments by ID (max 100).
      operationId: v2BatchDeleteSegments
      tags:
        - Segments
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchDeleteRequest'
      responses:
        '200':
          description: Segments deleted successfully

  # ============================================
  # DRIPS
  # ============================================
  /drips:
    get:
      summary: List All Drip Campaigns
      description: |
        Retrieve all drip campaigns (autoresponders). The API provides read-only and delete access to drip campaigns. To create or configure drip campaigns, use the Mumara web interface.
      operationId: v2GetDrips
      tags:
        - Drips
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: autoresponder_group_id
          in: query
          description: Filter by autoresponder group ID
          schema:
            type: integer
      responses:
        '200':
          description: Drip campaigns retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DripsResponse'

  /drips/{id}:
    get:
      summary: Get a Drip Campaign
      description: Retrieve a specific drip campaign by ID.
      operationId: v2GetDrip
      tags:
        - Drips
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Drip campaign retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DripResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a Drip Campaign
      description: Delete a drip campaign and all its associated email sequence entries.
      operationId: v2DeleteDrip
      tags:
        - Drips
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Drip campaign deleted successfully
        '404':
          $ref: '#/components/responses/NotFound'

  /drips/batch:
    delete:
      summary: Batch Delete Drip Campaigns
      description: Delete multiple drip campaigns by ID (max 100).
      operationId: v2BatchDeleteDrips
      tags:
        - Drips
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchDeleteRequest'
      responses:
        '200':
          description: Drip campaigns deleted successfully

  # ============================================
  # TRIGGERS
  # ============================================
  /triggers:
    get:
      summary: List All Triggers
      description: |
        Retrieve all automation triggers. Triggers can be filtered by type and active status. To create complex triggers, use the Mumara web interface; the API supports viewing, toggling, and deleting.
      operationId: v2GetTriggers
      tags:
        - Triggers
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: trigger_type
          in: query
          description: Filter by trigger type
          schema:
            type: string
        - name: status
          in: query
          description: Filter by active status (true = active, false = inactive)
          schema:
            type: boolean
      responses:
        '200':
          description: Triggers retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggersResponse'

  /triggers/{id}:
    get:
      summary: Get a Trigger
      description: Retrieve a specific trigger with its configuration and metadata.
      operationId: v2GetTrigger
      tags:
        - Triggers
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Trigger retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a Trigger
      description: |
        Update a trigger's name or status. Use this to enable/disable triggers without deleting them.
      operationId: v2UpdateTrigger
      tags:
        - Triggers
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TriggerUpdate'
      responses:
        '200':
          description: Trigger updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      summary: Delete a Trigger
      description: Permanently delete a trigger.
      operationId: v2DeleteTrigger
      tags:
        - Triggers
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Trigger deleted successfully
        '404':
          $ref: '#/components/responses/NotFound'

  /triggers/batch:
    delete:
      summary: Batch Delete Triggers
      description: Delete multiple triggers by ID (max 100).
      operationId: v2BatchDeleteTriggers
      tags:
        - Triggers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchDeleteRequest'
      responses:
        '200':
          description: Triggers deleted successfully

  # ============================================
  # WEB FORMS
  # ============================================
  /web-forms:
    get:
      summary: List All Web Forms
      description: Retrieve all subscription web forms with pagination. Optionally filter by associated list.
      operationId: v2GetWebForms
      tags:
        - Web Forms
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: list_id
          in: query
          description: Filter by associated list ID
          schema:
            type: integer
      responses:
        '200':
          description: Web forms retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebFormsResponse'

  /web-forms/{id}:
    get:
      summary: Get a Web Form
      description: Retrieve a specific web form with its configuration and settings.
      operationId: v2GetWebForm
      tags:
        - Web Forms
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Web form retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebFormResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a Web Form
      description: Update a web form's name or settings.
      operationId: v2UpdateWebForm
      tags:
        - Web Forms
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebFormUpdate'
      responses:
        '200':
          description: Web form updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebFormResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a Web Form
      description: Delete a web form. Any embedded instances of this form will stop functioning.
      operationId: v2DeleteWebForm
      tags:
        - Web Forms
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Web form deleted successfully
        '404':
          $ref: '#/components/responses/NotFound'

  /web-forms/{id}/embed-code:
    get:
      summary: Get Web Form Embed Code
      description: Retrieve the HTML embed code snippet for embedding this web form on external websites.
      operationId: v2GetWebFormEmbedCode
      tags:
        - Web Forms
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Embed code retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      form_id:
                        type: integer
                      embed_code:
                        type: string
                        description: HTML snippet to embed on your website
        '404':
          $ref: '#/components/responses/NotFound'

  /web-forms/batch:
    delete:
      summary: Batch Delete Web Forms
      description: Delete multiple web forms by ID (max 100).
      operationId: v2BatchDeleteWebForms
      tags:
        - Web Forms
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchDeleteRequest'
      responses:
        '200':
          description: Web forms deleted successfully

  # ============================================
  # GROUPS
  # ============================================
  /groups/{module}:
    get:
      summary: List Groups by Module
      description: |
        Retrieve all groups for a specific module type. Groups provide hierarchical organization for resources.

        **Available modules:** `list`, `broadcast`, `customField`
      operationId: v2GetGroups
      tags:
        - Groups
      parameters:
        - name: module
          in: path
          required: true
          description: The module type to retrieve groups for
          schema:
            type: string
            enum: [list, broadcast, customField]
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Groups retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupsResponse'
        '422':
          description: Invalid module name
    post:
      summary: Create a Group
      description: |
        Create a new group within a module. Optionally nest it under an existing parent group.
      operationId: v2CreateGroup
      tags:
        - Groups
      parameters:
        - name: module
          in: path
          required: true
          description: The module type to create the group in
          schema:
            type: string
            enum: [list, broadcast, customField]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupCreate'
      responses:
        '201':
          description: Group created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupResponse'
        '409':
          description: A group with this name already exists in the module
        '422':
          $ref: '#/components/responses/ValidationError'

  /groups/{module}/{id}:
    get:
      summary: Get a Group
      description: Retrieve a specific group by ID within the given module.
      operationId: v2GetGroup
      tags:
        - Groups
      parameters:
        - name: module
          in: path
          required: true
          schema:
            type: string
            enum: [list, broadcast, customField]
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Group retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a Group
      description: |
        Update a group's name. Groups the system manages (`is_movable: false`) are
        listed like any other but cannot be renamed or moved: they are refused with
        `403 GROUP_NOT_MOVABLE`.
      operationId: v2UpdateGroup
      tags:
        - Groups
      parameters:
        - name: module
          in: path
          required: true
          schema:
            type: string
            enum: [list, broadcast, customField]
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupUpdate'
      responses:
        '200':
          description: Group updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a Group
      description: |
        Delete a group. Items within the group are moved to the root/default group, not deleted.
        Groups the system manages (`is_movable: false`) cannot be deleted and are refused with `403 GROUP_NOT_MOVABLE`.
      operationId: v2DeleteGroup
      tags:
        - Groups
      parameters:
        - name: module
          in: path
          required: true
          schema:
            type: string
            enum: [list, broadcast, customField]
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Group deleted successfully
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # STATISTICS
  # ============================================
  /stats/broadcasts/{id}/summary:
    get:
      summary: Get Broadcast Statistics Summary
      description: |
        Retrieve a statistics summary for a specific broadcast: total scheduled, sent, delivered, opened, clicked, bounced and unsubscribed counts, each with its calculated rate. `delivered` is what was sent minus what bounced, since a bounce is the only signal that an address did not accept the message.
      operationId: v2GetBroadcastStatsSummary
      tags:
        - Statistics
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Statistics retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsSummaryResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /stats/broadcasts/{id}/opens:
    get:
      summary: Get Broadcast Opens
      description: Retrieve detailed open tracking data for a broadcast, including individual recipient open events with timestamps.
      operationId: v2GetBroadcastOpens
      tags:
        - Statistics
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Opens retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OpenEventsResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /stats/broadcasts/{id}/clicks:
    get:
      summary: Get Broadcast Clicks
      description: Retrieve detailed click tracking data for a broadcast, including which links were clicked by which recipients.
      operationId: v2GetBroadcastClicks
      tags:
        - Statistics
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Clicks retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClickEventsResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /stats/broadcasts/{id}/bounces:
    get:
      summary: Get Broadcast Bounces
      description: Retrieve bounce data for a broadcast. Filter by bounce type (hard/soft) to focus on permanent vs. temporary delivery failures.
      operationId: v2GetBroadcastBounces
      tags:
        - Statistics
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Bounces retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BounceEventsResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /stats/broadcasts/{id}/unsubscribes:
    get:
      summary: Get Broadcast Unsubscribes
      description: Retrieve unsubscribe data for a broadcast—recipients who opted out after receiving this email.
      operationId: v2GetBroadcastUnsubscribes
      tags:
        - Statistics
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Unsubscribes retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnsubscribeEventsResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /stats/broadcasts/{id}/complaints:
    get:
      summary: Get Broadcast Complaints
      description: Retrieve spam complaint data for a broadcast—recipients who marked this email as spam.
      operationId: v2GetBroadcastComplaints
      tags:
        - Statistics
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Complaints retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComplaintEventsResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /stats/broadcasts/{id}/logs:
    get:
      summary: Get Broadcast Sending Logs
      description: Retrieve detailed sending logs for a broadcast. Each entry represents an individual email delivery attempt with its status.
      operationId: v2GetBroadcastLogs
      tags:
        - Statistics
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Logs retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendingLogsResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /me:
    get:
      summary: Get Current User Profile
      description: Retrieve the profile of the currently authenticated user.
      operationId: v2GetCurrentUser
      tags:
        - Account
      responses:
        '200':
          description: Profile retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'

# ============================================
# COMPONENTS
# ============================================
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        All V2 API requests require Bearer token authentication.
        Pass your API token in the `Authorization` header:
        ```
        Authorization: Bearer YOUR_API_TOKEN
        ```

  parameters:
    Page:
      name: page
      in: query
      description: Page number (starts at 1)
      schema:
        type: integer
        default: 1
        minimum: 1
    PerPage:
      name: per_page
      in: query
      description: Records per page (1–100)
      schema:
        type: integer
        default: 25
        minimum: 1
        maximum: 100
    ResourceId:
      name: id
      in: path
      required: true
      description: Resource ID
      schema:
        type: integer

  responses:
    Unauthorized:
      description: |
        The key is missing or wrong, or the request came from an IP address the key doesn't allow. A missing or wrong key is answered in the V1 format, before the V2 layer runs.
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/LegacyAuthError'
              - $ref: '#/components/schemas/ErrorResponse'
          examples:
            invalidKey:
              summary: Missing or wrong key (V1 format)
              value:
                status: error
                result: 'Error: Invalid or missing API token.'
                suggestion: If your server isn't accepting the Authorization Bearer header, try passing the API token as a parameter using api_token instead.
            ipNotAllowed:
              summary: IP address not allowed for this key
              value:
                success: false
                error:
                  code: UNAUTHORIZED
                  message: This IP address is not authorized for access.

    Forbidden:
      description: The key doesn't have permission for this action, or it is switched off. A switched-off key is answered in the V1 format.
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/ErrorResponse'
              - $ref: '#/components/schemas/LegacyAuthError'
          examples:
            noPermission:
              summary: The key lacks the permission
              value:
                success: false
                error:
                  code: FORBIDDEN
                  message: 'Error: Access Denied.'
                  details:
                    reason: api_permission
                    route: v2.sending-domains.policy
            keySwitchedOff:
              summary: The key is switched off (V1 format)
              value:
                status: error
                result: 'Error: API token has expired.'

    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: VALIDATION_ERROR
              message: The given data was invalid.
              details:
                email:
                  - The email field is required.

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: NOT_FOUND
              message: Resource not found

    Conflict:
      description: Resource conflict (e.g., duplicate entry)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: CONFLICT
              message: Resource already exists

    # ---- Sending Domains ----
    SendingDomainForbidden:
      description: |
        The domain cannot be changed by this token: it is the platform sandbox domain (`SANDBOX_DOMAIN`), its ownership verification was reset by Mumara ONE (`OWNERSHIP_REVOKED`), or the key doesn't have permission (`FORBIDDEN`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: OWNERSHIP_REVOKED
              message: Ownership verification for mail.example.com was reset by an administrator. Contact support to have it reviewed.
              details:
                next_step:
                  action: contact_administrator
                  description: An administrator must re-verify this domain.

    OwnershipRevoked:
      description: Ownership verification was reset by Mumara ONE and cannot be re-verified through the API
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: OWNERSHIP_REVOKED
              message: Ownership verification for mail.example.com was reset by an administrator. Contact support to have it reviewed.

    OwnershipVerificationRequired:
      description: Ownership must be proven before this action
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: OWNERSHIP_VERIFICATION_REQUIRED
              message: Prove ownership of mail.example.com before configuring it.
              details:
                next_step:
                  action: verify_ownership
                  method: POST
                  endpoint: https://one.mumara.com/api/v2/sending-domains/118/verify-ownership
                  body:
                    method: dns_txt

    OwnershipVerificationFailed:
      description: |
        The ownership record or file could not be confirmed. `details.detail` is one of `not_found`, `value_mismatch`, `lookup_error` (DNS method) or `file_unreachable`, `file_content_mismatch` (file method); `details.url` / `details.http_status` are present for the file method and `details.lookup_error` carries the resolver/HTTP error text when there was one. `details.ownership.methods` repeats both methods so the client can switch.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: OWNERSHIP_VERIFICATION_FAILED
              message: Ownership of mail.example.com could not be verified using the DNS TXT method.
              details:
                method: dns_txt
                host: 3f2b9c1a7e.mail.example.com
                expected: 1724500000d41d8cd98f00b204e9800998ecf8427e
                found: null
                found_all: []
                detail: not_found
                dns_lookup_provider: google_doh
                hint: No TXT record was found at that host. If you published it recently, wait for DNS propagation and retry — or use method "file_upload".
                ownership:
                  required: true
                  verified: false
                  state: pending
                  methods:
                    - method: dns_txt
                      recommended: true
                      record:
                        type: TXT
                        host: 3f2b9c1a7e.mail.example.com
                        name: 3f2b9c1a7e.mail
                        value: 1724500000d41d8cd98f00b204e9800998ecf8427e
                        ttl: 3600
                        instructions: Create a TXT record for host 3f2b9c1a7e.mail.example.com with exactly this value.
                    - method: file_upload
                      recommended: false
                      file:
                        file_name: e4d909c290d0fb1ca068ffaddf22cbd0.html
                        content: e4d909c290d0fb1ca068ffaddf22cbd0
                        url: https://mail.example.com/e4d909c290d0fb1ca068ffaddf22cbd0.html
                        instructions: Upload the file so it is publicly readable at this URL; its body must be exactly the content.
                next_step:
                  action: verify_ownership
                  method: POST
                  endpoint: https://one.mumara.com/api/v2/sending-domains/118/verify-ownership
                  body:
                    method: dns_txt
                  description: Publish the ownership TXT record (or upload the verification file), then call this endpoint. On success every remaining DNS record is returned.

    FeatureForcedByPackage:
      description: |
        The change was refused by policy. Codes: `FEATURE_FORCED_BY_PACKAGE`, `FEATURE_UNAVAILABLE`, `SSL_FORCED`, `DOMAIN_SUSPENDED`, `OWNERSHIP_VERIFICATION_REQUIRED`, `HOOK_ABORT`. (An unknown feature name in the path is a `404`, not a `422`.)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            forcedByPackage:
              summary: Package forces the feature
              value:
                success: false
                error:
                  code: FEATURE_FORCED_BY_PACKAGE
                  message: "DKIM signing cannot be disabled: your package requires it on every sending domain."
                  details:
                    feature: dkim
                    forced_by: package
                    package_id: 3
                    current:
                      enabled: true
                      status: verified
                    policy_endpoint: https://one.mumara.com/api/v2/sending-domains/policy
            sslForced:
              summary: Secure tracking URLs are enforced globally
              value:
                success: false
                error:
                  code: SSL_FORCED
                  message: Secure tracking URLs are enforced for every sending domain (Application Settings > Domains > Force secure URLs) and cannot be disabled per domain.
                  details:
                    feature: ssl
                    setting: force_secure_tracking_urls
            hookAbort:
              summary: Mumara ONE blocked the change
              value:
                success: false
                error:
                  code: HOOK_ABORT
                  message: "'mail.example.com' has been suspended by the administrator. Contact support to have it reviewed."
                  details:
                    code: DOMAIN_SUSPENDED
                    hook: beforeSendingDomainFeatureChange

    DkimRegenerateDisabled:
      description: DKIM key regeneration isn't available to your account
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: DKIM_REGENERATE_DISABLED
              message: Regenerating DKIM keys has been disabled by the administrator (Application Settings > Domains > Disable regenerating the DKIM keys).
              details:
                setting: allow_users_to_regenerate_keys
                current_keys:
                  selector: key
                  host: key._domainkey.mail.example.com
                  status: verified
                  key_size: 2048
                policy_endpoint: https://one.mumara.com/api/v2/sending-domains/policy

    VerificationCooldown:
      description: Re-verification requested before the re-check cooldown expired
      headers:
        Retry-After:
          description: Seconds to wait before verifying again
          schema:
            type: integer
            example: 540
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: VERIFICATION_COOLDOWN
              message: This domain was checked recently. DNS verification can run again in 9 minutes.
              details:
                retry_after_seconds: 540
                cooldown_minutes: 10
                last_verified_at: '2026-08-24T10:15:00+00:00'
                next_allowed_at: '2026-08-24T10:25:00+00:00'

    DomainInUse:
      description: Other resources still reference the domain; pass `reassign_to_domain_id` to move them
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: DOMAIN_IN_USE
              message: mail.example.com is still used by 2 lists, 1 sending nodes. Pass reassign_to_domain_id to move them to another sending domain, or detach them first.
              details:
                connected_assets:
                  lists:
                    - id: 12
                      name: Newsletter
                    - id: 15
                      name: Customers
                  sending_nodes:
                    - id: 3
                      name: Primary SMTP
                hint: 'DELETE the domain again with {"reassign_to_domain_id": <id>} to move these assets.'

    SendingDomainHookAbort:
      description: Mumara ONE blocked the action; `details.code` names the rule
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: HOOK_ABORT
              message: SMTP server 'haraka-01' is being rebuilt; DKIM keys cannot be regenerated until it finishes (usually a few minutes). Try again shortly.
              details:
                code: SERVER_REBUILD_IN_PROGRESS
                hook: beforeSendingDomainDkimRegenerate

  schemas:
    # ============================================
    # COMMON RESPONSE SCHEMAS
    # ============================================
    SuccessResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: VALIDATION_ERROR
            message:
              type: string
            details:
              type: object

    LegacyAuthError:
      type: object
      description: The V1-format body of a missing, wrong or switched-off key, which V2 endpoints return too
      properties:
        status:
          type: string
          const: error
        result:
          type: string
          examples:
            - 'Error: Invalid or missing API token.'
        suggestion:
          type: string

    Meta:
      type: object
      description: Pagination metadata included with paginated responses
      properties:
        current_page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer
          description: Total number of records across all pages
        last_page:
          type: integer
        from:
          type: integer
          description: First record number on this page
        to:
          type: integer
          description: Last record number on this page

    PaginatedResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            type: object
        meta:
          $ref: '#/components/schemas/Meta'

    BatchDeleteRequest:
      type: object
      required:
        - ids
      properties:
        ids:
          type: array
          items:
            type: integer
          maxItems: 100
          description: Array of record IDs to delete (max 100)
          example: [1, 2, 3]

    BatchResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            processed:
              type: integer
              description: Total records processed
            created:
              type: integer
              description: Records successfully created
            updated:
              type: integer
              description: Existing records updated
            failed:
              type: integer
              description: Records that failed
            errors:
              type: array
              description: Details for each failed record
              items:
                type: object
                properties:
                  email:
                    type: string
                  error:
                    type: string
        message:
          type: string

    BatchDeleteResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            processed:
              type: integer
            deleted:
              type: integer
            failed:
              type: integer
        message:
          type: string

    # ============================================
    # LIST SCHEMAS
    # ============================================
    ListCreate:
      type: object
      required:
        - name
        - owner_name
        - owner_email
        - reply_email
      properties:
        name:
          type: string
          maxLength: 255
          description: Mailing list name (must be unique per user)
          example: Newsletter Subscribers
        owner_name:
          type: string
          maxLength: 255
          description: Sender display name used in the "From" field
          example: John Doe
        owner_email:
          type: string
          format: email
          description: Sender email address. The domain must be a verified sending domain.
          example: john@yourdomain.com
        reply_email:
          type: string
          format: email
          description: Reply-to email address
          example: reply@yourdomain.com
        bounce_email:
          type: string
          format: email
          description: Not needed in Mumara ONE, which processes bounces for you. Leave it out.
          example: bounce@yourdomain.com
        group_id:
          type: integer
          description: Group ID. If omitted, a default "Unsorted" group is created/used.
          example: 1

    ListUpdate:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
          example: Updated List Name
        owner_name:
          type: string
          maxLength: 255
          example: Jane Doe
        owner_email:
          type: string
          format: email
          example: jane@yourdomain.com
        reply_email:
          type: string
          format: email
          example: reply@yourdomain.com
        bounce_email:
          type: string
          format: email
          example: bounce@yourdomain.com
        group_id:
          type: integer
          example: 1

    List:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        owner_name:
          type: string
        owner_email:
          type: string
        reply_email:
          type: string
        subscribers_count:
          type: integer
          description: Total number of contacts in this list
        group:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        bounce_email:
          type: string
          nullable: true
        custom_fields:
          type: object
          nullable: true
          description: Map of custom field IDs to names associated with this list
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ListResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/List'
        message:
          type: string

    ListsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/List'
        meta:
          $ref: '#/components/schemas/Meta'

    # ============================================
    # CONTACT SCHEMAS
    # ============================================
    ContactCreate:
      type: object
      required:
        - email
        - list_id
      properties:
        email:
          type: string
          format: email
          description: Contact email address (must be unique within the list)
          example: john.doe@example.com
        list_id:
          type: integer
          description: ID of the list to add the contact to
          example: 1
        first_name:
          type: string
          maxLength: 255
          description: Convenience field (equivalent to custom_fields.first_name)
          example: John
        last_name:
          type: string
          maxLength: 255
          description: Convenience field (equivalent to custom_fields.last_name)
          example: Doe
        phone:
          type: string
          maxLength: 50
          description: Convenience field (equivalent to custom_fields.phone)
          example: "+1-555-123-4567"
        status:
          type: string
          enum: [subscribed, unsubscribed]
          default: subscribed
          description: Initial subscription status
          example: subscribed
        format:
          type: string
          enum: [html, text]
          default: html
          description: Email format preference
          example: html
        custom_fields:
          type: object
          additionalProperties:
            type: string
          description: Custom field values as key-value pairs (use the field's tag as the key)
          example:
            company: Acme Corp
            city: New York

    ContactUpdate:
      type: object
      properties:
        email:
          type: string
          format: email
          example: john.doe@example.com
        first_name:
          type: string
          maxLength: 255
        last_name:
          type: string
          maxLength: 255
        phone:
          type: string
          maxLength: 50
        status:
          type: string
          enum: [subscribed, unsubscribed, bounced, complaint]
          description: New subscription status
        custom_fields:
          type: object
          additionalProperties:
            type: string
          description: Custom field values to update

    Contact:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
        status:
          type: string
          enum: [subscribed, unsubscribed, bounced, complaint]
        list:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        custom_fields:
          type: object
          nullable: true
          description: All custom field values for this contact
        format:
          type: string
          enum: [html, text]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ContactResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/Contact'
        message:
          type: string

    ContactsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Contact'
        meta:
          $ref: '#/components/schemas/Meta'

    ContactBatch:
      type: object
      required:
        - list_id
        - contacts
      properties:
        list_id:
          type: integer
          description: ID of the list to import contacts into
          example: 1
        contacts:
          type: array
          items:
            type: object
            required:
              - email
            properties:
              email:
                type: string
                format: email
                example: contact@example.com
              first_name:
                type: string
              last_name:
                type: string
              custom_fields:
                type: object
                additionalProperties:
                  type: string
          maxItems: 1000
          description: Array of contacts to import (max 1,000)
        update_existing:
          type: boolean
          default: false
          description: If true, updates custom fields for contacts that already exist in the list

    # ============================================
    # BROADCAST SCHEMAS
    # ============================================
    BroadcastCreate:
      type: object
      required:
        - name
        - subject
      properties:
        name:
          type: string
          maxLength: 255
          description: Campaign name (for internal identification)
          example: January Newsletter
        subject:
          type: string
          maxLength: 998
          description: Email subject line
          example: "Your January Newsletter is Here!"
        content_html:
          type: string
          description: HTML email content
        content_text:
          type: string
          description: Plain text fallback content
        group_id:
          type: integer
          description: Group ID for organizing broadcasts. If omitted, an "API Broadcasts" group is auto-created.

    BroadcastUpdate:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
        subject:
          type: string
          maxLength: 998
        content_html:
          type: string
        content_text:
          type: string
        group_id:
          type: integer

    Broadcast:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        subject:
          type: string
        group:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    BroadcastResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/Broadcast'
        message:
          type: string

    BroadcastsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Broadcast'
        meta:
          $ref: '#/components/schemas/Meta'

    # ============================================
    # SENDING DOMAIN SCHEMAS
    # ============================================
    SendingDomainErrorCode:
      type: string
      description: |
        Error codes specific to the Sending Domains endpoints (in addition to the standard codes). The HTTP status is given in parentheses.

        - `INVALID_DOMAIN` (422) — not a valid hostname
        - `DOMAIN_ALREADY_EXISTS` (409) — already registered (unless Mumara ONE allows duplicates)
        - `DOMAIN_LIMIT_REACHED` (422) — your plan's limit is reached; `details.limit`, `details.used`
        - `PRIMARY_DOMAIN_NOT_VERIFIED` (422) — the platform's primary tracking domain is not verified yet
        - `DOMAIN_NOT_ALLOWED` (422) — the application's own hostname cannot be used
        - `DOMAIN_NOT_ELIGIBLE` (422) — failed the domain age / blacklist pre-check
        - `ELIGIBILITY_CHECK_FAILED` (503) — the eligibility service could not be reached; the domain was not added
        - `FEATURE_FORCED_BY_PACKAGE` (422) — your plan requires the feature; it cannot be disabled
        - `FEATURE_UNAVAILABLE` (422) — the feature isn't available in Mumara ONE
        - `MASKING_TYPE_UNAVAILABLE` (422) — the masking type isn't offered in Mumara ONE
        - `INVALID_FEATURE` (422) — unknown feature name (not reachable through the documented routes: an unknown `{feature}` is a `404`)
        - `SSL_FORCED` (422) — secure tracking URLs are enforced globally
        - `SELECTOR_EDIT_DISABLED` (422) — Mumara ONE does not allow changing that selector
        - `INVALID_SELECTOR` (422) — selector is not a valid DNS label
        - `UPDATE_REJECTED` (422) — several changes in one `PUT` were refused; `details.fields` is keyed by request field, each with `code` and `message`
        - `DKIM_REGENERATE_DISABLED` (403) — key regeneration disabled for users
        - `DKIM_KEY_GENERATION_FAILED` (500) — OpenSSL could not generate a key pair
        - `DKIM_KEYS_NOT_GENERATED` (422) — the domain has no DKIM keys yet
        - `OWNERSHIP_VERIFICATION_REQUIRED` (422) — prove ownership first
        - `OWNERSHIP_VERIFICATION_FAILED` (422) — the ownership record/file was not found or did not match
        - `OWNERSHIP_REVOKED` (403) — ownership verification reset by Mumara ONE
        - `DOMAIN_SUSPENDED` (422) — the domain was suspended by Mumara ONE
        - `SANDBOX_DOMAIN` (403) — the platform sandbox domain cannot be changed
        - `VERIFICATION_COOLDOWN` (429) — re-check requested too soon; see `Retry-After`
        - `DOMAIN_IN_USE` (409) — lists, triggers, web forms or other items still use the domain
        - `REASSIGN_TARGET_INVALID` (422) — `reassign_to_domain_id` is not a usable domain of the same account
        - `HOOK_ABORT` (422) — Mumara ONE blocked the action; `details.code` names the rule (see the introduction) and `details.hook` the check that refused it
      enum:
        - INVALID_DOMAIN
        - DOMAIN_ALREADY_EXISTS
        - DOMAIN_LIMIT_REACHED
        - PRIMARY_DOMAIN_NOT_VERIFIED
        - DOMAIN_NOT_ALLOWED
        - DOMAIN_NOT_ELIGIBLE
        - ELIGIBILITY_CHECK_FAILED
        - FEATURE_FORCED_BY_PACKAGE
        - FEATURE_UNAVAILABLE
        - MASKING_TYPE_UNAVAILABLE
        - INVALID_FEATURE
        - SSL_FORCED
        - SELECTOR_EDIT_DISABLED
        - INVALID_SELECTOR
        - UPDATE_REJECTED
        - DKIM_REGENERATE_DISABLED
        - DKIM_KEY_GENERATION_FAILED
        - DKIM_KEYS_NOT_GENERATED
        - OWNERSHIP_VERIFICATION_REQUIRED
        - OWNERSHIP_VERIFICATION_FAILED
        - OWNERSHIP_REVOKED
        - DOMAIN_SUSPENDED
        - SANDBOX_DOMAIN
        - VERIFICATION_COOLDOWN
        - DOMAIN_IN_USE
        - REASSIGN_TARGET_INVALID
        - USER_NOT_FOUND
        - HOOK_ABORT

    DomainStatusCode:
      type: string
      description: |
        Composite status of a sending domain, derived from ownership verification and the authentication state of every enabled feature.

        - `pending_ownership_verification` — ownership verification is enabled and not yet proven; nothing else can be configured
        - `ownership_verification_revoked` — Mumara ONE reset the ownership verification
        - `inactive` — switched off by the user (`PATCH …/features/status`)
        - `pending_authentication` — records issued but not all confirmed yet (or the configuration changed and must be re-confirmed)
        - `authentication_failed` — at least one enabled record failed verification
        - `suspended` — suspended by Mumara ONE
        - `active` — every enabled record is verified; the domain is fully authenticated
      enum:
        - pending_ownership_verification
        - ownership_verification_revoked
        - inactive
        - pending_authentication
        - authentication_failed
        - suspended
        - active

    DomainStatus:
      type: object
      properties:
        code:
          $ref: '#/components/schemas/DomainStatusCode'
        label:
          type: string
          example: Pending authentication
        can_send:
          type: boolean
          description: Whether emails may currently be sent from this domain. For `pending_authentication` / `authentication_failed` this follows Mumara ONE's rule for unauthenticated sending domains.
        description:
          type: string
          description: Human-readable explanation of the current state and what resolves it

    NextStep:
      type: object
      description: Machine-readable recommendation of what to do next
      properties:
        action:
          type: string
          enum: [verify_ownership, contact_administrator, enable_domain, verify_dns_records, fix_dns_records, none]
        method:
          type:
            - string
            - 'null'
          description: HTTP method of the suggested call (`null` when `action` is `none` / `contact_administrator`)
          example: POST
        endpoint:
          type:
            - string
            - 'null'
          description: Absolute URL of the suggested call (`null` when `action` is `none` / `contact_administrator`)
          example: https://one.mumara.com/api/v2/sending-domains/118/verify
        body:
          type: object
          description: Suggested request body, when applicable
          additionalProperties: true
        description:
          type: string

    OwnershipRecord:
      type: object
      description: TXT record proving ownership
      properties:
        type:
          type: string
          enum: [TXT]
        host:
          type: string
          description: Fully-qualified host to create the record at
          example: 3f2b9c1a7e.mail.example.com
        name:
          type: string
          description: Host relative to the registrable domain (what most DNS panels ask for)
          example: 3f2b9c1a7e.mail
        value:
          type: string
          description: Exact TXT value to publish
        ttl:
          type: integer
          description: Suggested TTL in seconds
        current_value:
          type:
            - string
            - 'null'
          description: What is currently published at the host (only on `GET /sending-domains/{id}/dns-records?include_current_values=true` while ownership is pending; `null` when nothing is published)
        lookup_error:
          type: string
          description: Present only when the current-value lookup itself failed
          example: 3600
        instructions:
          type: string

    OwnershipFile:
      type: object
      description: Verification file alternative
      properties:
        file_name:
          type: string
          example: e4d909c290d0fb1ca068ffaddf22cbd0.html
        content:
          type: string
          description: Exact body the file must contain
        url:
          type: string
          format: uri
          description: Public URL Mumara will fetch
        instructions:
          type: string

    OwnershipMethod:
      type: object
      properties:
        method:
          type: string
          enum: [dns_txt, file_upload]
        recommended:
          type: boolean
        record:
          $ref: '#/components/schemas/OwnershipRecord'
        file:
          $ref: '#/components/schemas/OwnershipFile'

    OwnershipBlock:
      type: object
      properties:
        required:
          type: boolean
          description: Whether ownership verification is required
        verified:
          type: boolean
        state:
          type: string
          enum: [not_required, pending, verified, revoked]
        methods:
          type: array
          description: Available verification methods — present only while ownership is pending
          items:
            $ref: '#/components/schemas/OwnershipMethod'

    RecordStatus:
      type: string
      description: Verification state of a single DNS record
      enum: [pending, verified, failed]

    RecordStatusDetail:
      type: string
      description: |
        Why a record is `failed` (per feature): DKIM `not_found`, `value_mismatch`, `lookup_error`; tracking `cname_mismatch`, `redirect_mismatch` (host does not resolve to Mumara ONE), `ssl_broken` (host reachable but HTTPS invalid); SPF `not_found`, `invalid`, `missing_include`, `missing_all`; MX `not_found`, `value_mismatch`; DMARC `not_found`, `invalid`.

        `inherited` is the one detail that accompanies a **verified** record: the subdomain publishes no
        DMARC policy of its own, so the organizational domain's applies (RFC 7489 §6.6.3).
      enum:
        - not_found
        - value_mismatch
        - lookup_error
        - cname_mismatch
        - redirect_mismatch
        - ssl_broken
        - invalid
        - missing_include
        - missing_all
        - inherited

    SendingDomainDkim:
      type: object
      properties:
        enabled:
          type: boolean
        forced:
          type: boolean
          description: Your plan requires DKIM; it cannot be disabled
        selector:
          type: string
          example: key
        host:
          type: string
          example: key._domainkey.mail.example.com
        status:
          $ref: '#/components/schemas/RecordStatus'
        has_keys:
          type: boolean

    SendingDomainTracking:
      type: object
      properties:
        enabled:
          type: boolean
        forced:
          type: boolean
        selector:
          type: string
          example: click
        host:
          type: string
          example: click.mail.example.com
        masking_type:
          type: string
          enum: [cname, htaccess, cloak]
        target:
          type: string
          description: CNAME target / redirect destination
          example: track.mumara-app.com
        status:
          $ref: '#/components/schemas/RecordStatus'
        redirect_status:
          type: string
          description: Result of the HTTP check against the tracking host
          enum: [pending, ok, mismatch, ssl_broken]
        ssl:
          type: object
          properties:
            enabled:
              type: boolean
              description: The domain's own SSL preference
            effective:
              type: boolean
              description: Whether HTTPS is actually used (true when forced by Mumara ONE)
            forced:
              type: boolean
            available:
              type: boolean
              description: Whether the per-domain SSL switch is offered in Mumara ONE

    SendingDomainBounce:
      type: object
      properties:
        available:
          type: boolean
          description: Bounce DNS verification is enabled in Mumara ONE
        enabled:
          type: boolean
        forced:
          type: boolean
        selector:
          type: string
          example: bounce
        host:
          type: string
          example: bounce.mail.example.com
        spf:
          type: object
          properties:
            status:
              $ref: '#/components/schemas/RecordStatus'
            include_domain:
              type: string
              example: spf.mumara.net
        mx:
          type: object
          properties:
            status:
              $ref: '#/components/schemas/RecordStatus'
            target:
              type: string
              example: return.mumara.net
            priority:
              type: integer
              example: 10

    SendingDomainDmarc:
      type: object
      properties:
        enabled:
          type: boolean
        host:
          type: string
          description: Where to publish this domain's own DMARC policy
          example: _dmarc.mail.example.com
        organizational_host:
          type:
            - string
            - 'null'
          description: |
            The organizational domain's DMARC host, whose policy already covers this domain when it
            publishes none of its own (RFC 7489 §6.6.3). `null` when `domain` is itself the
            organizational domain and nothing can be inherited.
          example: _dmarc.example.com
        status:
          $ref: '#/components/schemas/RecordStatus'
        suggested_value:
          type: string
          example: "v=DMARC1; p=none;"

    SendingDomain:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        domain:
          type: string
          example: mail.example.com
        masking_type:
          type: string
          enum: [cname, htaccess, cloak]
          description: Tracking method (internal `index` is exposed as `cloak`)
        is_sandbox:
          type: boolean
          description: Platform-provisioned sandbox domain (read-only)
        status:
          $ref: '#/components/schemas/DomainStatus'
        ownership:
          $ref: '#/components/schemas/OwnershipBlock'
        domain_status_code:
          type: integer
          description: Raw internal status (0 inactive, 1 active, 2 suspended, 3 authentication failed, 4 pending authentication)
          enum: [0, 1, 2, 3, 4]
        is_enabled:
          type: boolean
          description: The domain is not switched off (`domain_status_code != 0`)
        configuration_confirmed:
          type: boolean
          description: The current configuration has been confirmed via `verify` (a changed selector/feature resets this)
        dkim:
          $ref: '#/components/schemas/SendingDomainDkim'
        tracking:
          $ref: '#/components/schemas/SendingDomainTracking'
        bounce:
          $ref: '#/components/schemas/SendingDomainBounce'
        dmarc:
          $ref: '#/components/schemas/SendingDomainDmarc'
        last_verified_at:
          type:
            - string
            - 'null'
          format: date-time
          description: Last time `verify` ran for this domain (UTC)
        dns_records:
          type: array
          description: Records to publish for the enabled features (empty until ownership is verified). Present on create, show and mutation responses.
          items:
            $ref: '#/components/schemas/DnsRecord'
        files:
          type: array
          description: Tracking files for `htaccess` / `cloak` masking
          items:
            $ref: '#/components/schemas/TrackingFile'
        next_step:
          $ref: '#/components/schemas/NextStep'
        warnings:
          type: array
          description: Non-blocking problems encountered (e.g. DKIM key generation failed)
          items:
            type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SendingDomainResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/SendingDomain'
        message:
          type: string

    SendingDomainsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/SendingDomain'
        meta:
          $ref: '#/components/schemas/Meta'

    SendingDomainCreate:
      type: object
      required:
        - domain
      properties:
        domain:
          type: string
          description: Domain name (e.g., example.com or mail.example.com)
          example: mail.example.com
        masking_type:
          type: string
          enum: [cname, htaccess, cloak]
          default: cname
          description: Tracking method. Must be one of the types offered by Mumara ONE (see policy `features.tracking.masking_types`)
        enable_dkim:
          type: boolean
          default: true
          description: Enable DKIM signing (keys are generated immediately). Cannot be `false` when your plan forces DKIM
        enable_tracking:
          type: boolean
          default: true
          description: Enable the branded tracking domain. Cannot be `false` when your plan forces tracking
        enable_bounce:
          type: boolean
          description: Enable the branded bounce (return-path) domain. Defaults to `true` when your plan forces it, else `false`; unavailable when bounce DNS verification isn't offered
        enable_dmarc:
          type: boolean
          description: Track a DMARC record for the domain. Defaults to Mumara ONE's DMARC default
        enable_ssl:
          type: boolean
          description: Serve tracking links over HTTPS. Defaults to `true` when Mumara ONE forces secure tracking URLs — passing `false` in that case is refused with `SSL_FORCED`; `true` is refused with `FEATURE_UNAVAILABLE` when the per-domain switch is not offered
        key_size:
          type: integer
          enum: [512, 1024, 2048, 4096]
          description: DKIM key size in bits (defaults to the application setting)
          example: 2048
        dkim_selector:
          type: string
          description: Custom DKIM selector (DNS label). Only accepted when Mumara ONE allows editing it
          example: key
        tracking_selector:
          type: string
          description: Custom tracking subdomain label. Only accepted when Mumara ONE allows editing it
          example: click
        bounce_selector:
          type: string
          description: Custom bounce subdomain label. Only accepted when Mumara ONE allows editing it
          example: bounce

    SendingDomainUpdate:
      type: object
      properties:
        masking_type:
          type: string
          enum: [cname, htaccess, cloak]
        enable_dkim:
          type: boolean
        enable_tracking:
          type: boolean
        enable_bounce:
          type: boolean
        enable_dmarc:
          type: boolean
        enable_ssl:
          type: boolean
        enabled:
          type: boolean
          description: Switch the whole domain on or off (same as `PATCH …/features/status`)
        dkim_selector:
          type: string
          description: Changing it resets DKIM verification
        tracking_selector:
          type: string
          description: Changing it resets tracking verification
        bounce_selector:
          type: string
          description: Changing it resets SPF/MX verification

    SendingDomainDeleteRequest:
      type: object
      properties:
        reassign_to_domain_id:
          type: integer
          description: Move the lists, triggers, web forms and other items that use this domain to the given sending domain before deleting

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

    ConnectedAssets:
      type: object
      description: Resources that reference a sending domain, grouped by type (a group is omitted when empty)
      properties:
        lists:
          type: array
          items:
            $ref: '#/components/schemas/ConnectedAssetRef'
        sending_nodes:
          type: array
          items:
            $ref: '#/components/schemas/ConnectedAssetRef'
        triggers:
          type: array
          items:
            $ref: '#/components/schemas/ConnectedAssetRef'
        web_forms:
          type: array
          items:
            $ref: '#/components/schemas/ConnectedAssetRef'

    SendingDomainDeleteResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: Sending domain deleted successfully
        data:
          type:
            - object
            - 'null'
          description: Only present when assets were moved with `reassign_to_domain_id`
          properties:
            reassigned_to:
              type: object
              properties:
                id:
                  type: integer
                domain:
                  type: string
            moved_assets:
              $ref: '#/components/schemas/ConnectedAssets'

    FeatureToggleRequest:
      type: object
      required:
        - enabled
      properties:
        enabled:
          type: boolean

    FeatureToggleResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: DKIM signing enabled. Publish the DKIM record and call verify.
        data:
          allOf:
            - $ref: '#/components/schemas/SendingDomain'
            - type: object
              properties:
                changed:
                  type: object
                  description: The transition that was requested; `changed` is `false` when the feature was already in the requested state (idempotent no-op, no hooks fired)
                  properties:
                    feature:
                      type: string
                      enum: [dkim, tracking, bounce, dmarc, ssl, status]
                    from:
                      type: boolean
                    to:
                      type: boolean
                    changed:
                      type: boolean

    DnsRecord:
      type: object
      properties:
        feature:
          type: string
          enum: [dkim, tracking, bounce_spf, bounce_mx, dmarc]
        type:
          type: string
          enum: [TXT, CNAME, MX]
        host:
          type: string
          description: Fully-qualified host to create the record at
          example: key._domainkey.mail.example.com
        name:
          type: string
          description: Host relative to the registrable domain (what most DNS panels ask for)
          example: key._domainkey.mail
        value:
          type: string
          description: Value to publish
        ttl:
          type: integer
          description: Suggested TTL in seconds
          example: 3600
        priority:
          type: integer
          description: MX priority (MX records only)
          example: 10
        status:
          $ref: '#/components/schemas/RecordStatus'
        status_detail:
          $ref: '#/components/schemas/RecordStatusDetail'
        organizational_host:
          type:
            - string
            - 'null'
          description: |
            DMARC only. The organizational domain's DMARC host, whose policy covers this domain when it
            publishes none of its own (RFC 7489 §6.6.3). `null` when the domain is itself the
            organizational domain.
          example: _dmarc.example.com
        current_value:
          type:
            - string
            - 'null'
          description: What is currently published in DNS (only with `include_current_values=true`; `null` when nothing is published)
        current_value_source:
          type: string
          description: |
            DMARC only, and only when `current_value` was read from a host other than `host` — the
            organizational record this subdomain inherits. Absent when the value came from `host` itself.
          example: _dmarc.example.com
        lookup_error:
          type: string
          description: Present only when the current-value lookup itself failed (`include_current_values=true`)
        instructions:
          type: string

    TrackingFile:
      type: object
      description: For `htaccess` and `cloak` masking the tracking host is served by your own web server; publish this file there instead of a CNAME
      properties:
        feature:
          type: string
          enum: [tracking]
        type:
          type: string
          enum: [file]
        file_name:
          type: string
          example: .htaccess
        target_host:
          type: string
          description: Host that must serve the file
          example: click.mail.example.com
        content:
          type: string
          description: Exact file content
        status:
          $ref: '#/components/schemas/RecordStatus'
        status_detail:
          $ref: '#/components/schemas/RecordStatusDetail'
        instructions:
          type: string

    DnsRecordsSummary:
      type: object
      properties:
        total:
          type: integer
        verified:
          type: integer
        pending:
          type: integer
        failed:
          type: integer

    DnsLookupProvider:
      type: string
      description: DNS lookup method Mumara ONE uses
      enum: [dig, google_doh, mumara_toolbox]

    DnsRecordsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            domain:
              type: string
            status:
              $ref: '#/components/schemas/DomainStatus'
            ownership:
              $ref: '#/components/schemas/OwnershipBlock'
            records:
              type: array
              items:
                $ref: '#/components/schemas/DnsRecord'
            files:
              type: array
              items:
                $ref: '#/components/schemas/TrackingFile'
            summary:
              $ref: '#/components/schemas/DnsRecordsSummary'
            dns_lookup_provider:
              $ref: '#/components/schemas/DnsLookupProvider'
            current_value_lookup_provider:
              allOf:
                - $ref: '#/components/schemas/DnsLookupProvider'
              description: Provider used for `current_value` lookups; the verification provider is `dns_lookup_provider`
            next_step:
              $ref: '#/components/schemas/NextStep'

    VerifyOwnershipRequest:
      type: object
      properties:
        method:
          type: string
          enum: [dns_txt, file_upload]
          default: dns_txt

    VerifyOwnershipResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
        data:
          description: The full sending domain (as `GET /sending-domains/{id}`), with `ownership.method` / `ownership.checked` added; `dns_records` and `files` are populated the moment ownership is verified
          allOf:
            - $ref: '#/components/schemas/SendingDomain'
            - type: object
              properties:
                ownership:
                  allOf:
                    - $ref: '#/components/schemas/OwnershipBlock'
                    - type: object
                      properties:
                        method:
                          type: string
                          description: How ownership was established (`forced` = verified by Mumara ONE support without a lookup)
                          enum: [dns_txt, file_upload, forced]
                        checked:
                          type: object
                          description: What was checked and what was found (absent for `forced`)
                          properties:
                            host:
                              type:
                                - string
                                - 'null'
                              description: TXT host checked (`dns_txt`)
                            url:
                              type:
                                - string
                                - 'null'
                              description: File URL fetched (`file_upload`)
                            expected:
                              type: string
                            found:
                              type:
                                - string
                                - 'null'
                dns_records:
                  type: array
                  description: All records to publish next — issued the moment ownership is verified
                  items:
                    $ref: '#/components/schemas/DnsRecord'
                files:
                  type: array
                  items:
                    $ref: '#/components/schemas/TrackingFile'

    VerifyRecordsRequest:
      type: object
      properties:
        records:
          type: array
          description: Only check these records (defaults to every record of every enabled feature). `bounce` expands to both bounce records; a record whose feature is disabled is reported under `summary.skipped`
          items:
            type: string
            enum: [dkim, tracking, bounce, spf, mx, dmarc]

    VerifyRecordResult:
      type: object
      properties:
        feature:
          type: string
          enum: [dkim, tracking, bounce_spf, bounce_mx, dmarc]
        type:
          type: string
          enum: [TXT, CNAME, MX]
        host:
          type: string
        expected:
          type: string
        found:
          type:
            - string
            - 'null'
          description: Value found in DNS (`null` when nothing was found)
        status:
          $ref: '#/components/schemas/RecordStatus'
        status_detail:
          $ref: '#/components/schemas/RecordStatusDetail'
        message:
          type: string
          description: Human-readable outcome, including how to fix a failure
        lookup_error:
          type: string
          description: "Present only when the DNS lookup itself failed (`status_detail` = `lookup_error`)"

    VerifyDomainResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: "Verification completed: 2 verified, 3 failed."
        data:
          description: The full sending domain after the check (as `GET /sending-domains/{id}`), plus the verification report
          allOf:
            - $ref: '#/components/schemas/SendingDomain'
            - type: object
              properties:
                checked_at:
                  type: string
                  format: date-time
                records:
                  type: array
                  items:
                    $ref: '#/components/schemas/VerifyRecordResult'
                summary:
                  type: object
                  properties:
                    total:
                      type: integer
                    verified:
                      type: integer
                    failed:
                      type: integer
                    pending:
                      type: integer
                    skipped:
                      type: array
                      description: Requested records that were not checked because their feature is disabled
                      items:
                        type: string
                dns_lookup_provider:
                  $ref: '#/components/schemas/DnsLookupProvider'
                hooks_fired:
                  type: array
                  description: Add-on hooks queued by this run (`SendingDomainDKIMPassed`, `SendingDomainTrackingPassed`, `SendingDomainSpfPassed`, `SendingDomainMXPassed` on a record's first pass; `SendingDomainDnsFailed` when the domain ends up `authentication_failed`)
                  items:
                    type: string
                cooldown:
                  type: object
                  description: The re-check cooldown that now applies to this domain
                  properties:
                    cooldown_minutes:
                      type: integer
                      description: "`0` when Mumara ONE configured no cooldown"
                    next_verification_allowed_at:
                      type:
                        - string
                        - 'null'
                      format: date-time
                      description: "`null` when there is no cooldown"
                    applies:
                      type: boolean
                      description: Whether the cooldown applies to the calling token

    DkimRegenerateRequest:
      type: object
      properties:
        key_size:
          type: integer
          enum: [512, 1024, 2048, 4096]
          description: Key size in bits (defaults to the application setting)

    DkimKeysResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            domain:
              type: string
              example: mail.example.com
            selector:
              type: string
              example: key
            host:
              type: string
              example: key._domainkey.mail.example.com
            dns_value:
              type: string
              description: The TXT value to publish
            public_key_pem:
              type: string
              description: Public key in PEM format
            key_size_bits:
              type:
                - integer
                - 'null'
              description: RSA modulus size (`null` when the stored key cannot be parsed)
              example: 2048
            status:
              $ref: '#/components/schemas/RecordStatus'
            record:
              allOf:
                - $ref: '#/components/schemas/DnsRecord'
              description: The DKIM record as returned by `GET /sending-domains/{id}/dns-records`

    SendingDomainPolicy:
      type: object
      description: What the account may do with sending domains, resolved from your plan
      properties:
        user_id:
          type: integer
        license:
          type: object
          properties:
            type:
              type:
                - string
                - 'null'
              example: Commercial ESP
            commercial_esp:
              type: boolean
              description: '`true` in Mumara ONE: your plan can require features (`forced`)'
        ownership_verification_required:
          type: boolean
        ownership_methods:
          type: array
          description: Ownership proof methods accepted by `POST /sending-domains/{id}/verify-ownership`
          items:
            type: string
            enum: [dns_txt, file_upload]
        primary_domain_ready:
          type: boolean
          description: The platform's primary tracking domain is verified; domains cannot be added until it is
        domain_quota:
          type: object
          properties:
            limit:
              type:
                - integer
                - 'null'
              description: "`null` when unlimited (see `unlimited`)"
            unlimited:
              type: boolean
            used:
              type: integer
            remaining:
              type:
                - integer
                - 'null'
              description: "`null` when unlimited"
            can_add:
              type: boolean
        duplicates_allowed:
          type: boolean
          description: The same domain may be registered by more than one account
        eligibility_checks:
          type: object
          properties:
            enabled:
              type: boolean
            domain_age:
              type: object
              properties:
                enabled:
                  type: boolean
                min_days:
                  type: integer
            blacklist:
              type: object
              properties:
                enabled:
                  type: boolean
        features:
          type: object
          properties:
            dkim:
              type: object
              properties:
                available:
                  type: boolean
                forced:
                  type: boolean
                forced_by:
                  $ref: '#/components/schemas/ForcedBy'
                default_enabled:
                  type: boolean
                selector_editable:
                  type: boolean
                default_selector:
                  type: string
                key_size_default:
                  type: integer
                key_sizes:
                  type: array
                  items:
                    type: integer
                regenerate_allowed:
                  type: boolean
            tracking:
              type: object
              properties:
                available:
                  type: boolean
                forced:
                  type: boolean
                forced_by:
                  $ref: '#/components/schemas/ForcedBy'
                default_enabled:
                  type: boolean
                selector_editable:
                  type: boolean
                default_selector:
                  type: string
                masking_types:
                  type: array
                  items:
                    type: string
                    enum: [cname, htaccess, cloak]
                default_masking_type:
                  type:
                    - string
                    - 'null'
                  description: "`null` when no masking type is offered"
                cname_targets:
                  type: array
                  description: Hosts a new domain's tracking CNAME may be pointed at (one is picked per domain; see `tracking.target` on the domain)
                  items:
                    type: string
            bounce:
              type: object
              properties:
                available:
                  type: boolean
                forced:
                  type: boolean
                forced_by:
                  $ref: '#/components/schemas/ForcedBy'
                default_enabled:
                  type: boolean
                selector_editable:
                  type: boolean
                default_selector:
                  type: string
                spf_include:
                  type: string
                mx_target:
                  type: string
            dmarc:
              type: object
              properties:
                available:
                  type: boolean
                default_enabled:
                  type: boolean
                suggested_record:
                  type: string
                policy:
                  type: string
                  enum: [none, quarantine, reject]
            ssl:
              type: object
              properties:
                available:
                  type: boolean
                forced:
                  type: boolean
                default_enabled:
                  type: boolean
        verification:
          type: object
          properties:
            cooldown_minutes:
              type: integer
              description: "`0` means no cooldown"
            dns_lookup_provider:
              $ref: '#/components/schemas/DnsLookupProvider'
            current_value_lookup_provider:
              $ref: '#/components/schemas/DnsLookupProvider'
            unauthenticated_sending_allowed:
              type: boolean
        addons:
          type: object
          description: Extra information from Mumara ONE about your plan's sending-domain limit and required features, and your tracking and bounce setup
          additionalProperties: true

    ForcedBy:
      type:
        - string
        - 'null'
      description: Who mandates the feature — `package` (your plan requires it) or `user` (required for your account specifically); `null` when it is not forced
      enum: [package, user, null]

    SendingDomainPolicyResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/SendingDomainPolicy'

    # ============================================
    # SUPPRESSION SCHEMAS
    # ============================================
    SuppressionCreate:
      type: object
      required:
        - type
        - value
        - scope
        - reference
      properties:
        type:
          type: string
          enum: [email, domain, ip]
          description: Type of suppression entry
          example: email
        value:
          type: string
          maxLength: 255
          description: The email, domain, or IP to suppress
          example: bounced@example.com
        scope:
          type: string
          enum: [global, list]
          description: Whether this suppression applies globally or to a specific list
          example: global
        reference:
          type: string
          maxLength: 255
          description: Reference note (e.g., reason for suppression)
          example: hard-bounce
        list_id:
          type: integer
          description: Required when scope is 'list'

    SuppressionBatchCreate:
      type: object
      required:
        - type
        - values
        - scope
        - reference
      properties:
        type:
          type: string
          enum: [email, domain, ip]
          example: email
        values:
          type: array
          items:
            type: string
          maxItems: 1000
          description: List of values to suppress (max 1,000)
          example: ["bounced1@example.com", "bounced2@example.com"]
        scope:
          type: string
          enum: [global, list]
          example: global
        reference:
          type: string
          maxLength: 255
          example: bulk-import
        list_id:
          type: integer

    Suppression:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        type:
          type: string
          enum: [email, domain, ip]
          description: Suppression entry type
        list_id:
          type: integer
          description: Associated list ID (0 = global)
        is_global:
          type: boolean
          description: True if this suppression applies globally (list_id = 0)
        value:
          type: string
          description: The suppressed email, domain, or IP range
        reference:
          type: string
          description: Reference label/note
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SuppressionResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/Suppression'
        message:
          type: string

    SuppressionsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Suppression'
        meta:
          $ref: '#/components/schemas/Meta'

    # ============================================
    # CUSTOM FIELD SCHEMAS
    # ============================================
    CustomFieldCreate:
      type: object
      required:
        - name
        - type
        - list_id
      properties:
        name:
          type: string
          maxLength: 255
          description: Field display name
          example: Company Name
        type:
          type: string
          enum: [text, textarea, checkbox, radio, select, date, json]
          description: Field input type
          example: text
        list_id:
          type: integer
          description: List to associate this field with
          example: 1
        is_required:
          type: boolean
          description: Whether this field is required when adding contacts
          example: false
        field_order:
          type: integer
          description: Display order position
          example: 1
        options:
          type: array
          items:
            type: string
            maxLength: 255
          description: Options for select/checkbox/radio field types
          example: ["Option A", "Option B", "Option C"]

    CustomFieldUpdate:
      type: object
      properties:
        is_required:
          type: boolean
        field_order:
          type: integer
        options:
          type: array
          items:
            type: string
            maxLength: 255

    CustomField:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
        tag:
          type: string
          description: Merge tag for use in broadcasts (auto-generated from name)
        type:
          type: string
          enum: [text, textarea, checkbox, radio, select, date, json]
        is_required:
          type: boolean
        is_default:
          type: boolean
          description: System-provided fields cannot be modified or deleted
        field_order:
          type: integer
        options:
          type: array
          nullable: true
          items:
            type: string
          description: Options list (for checkbox/radio/select types only)
        list_ids:
          type: array
          items:
            type: integer
          description: IDs of lists this field is linked to
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CustomFieldResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/CustomField'
        message:
          type: string

    CustomFieldsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/CustomField'
        meta:
          $ref: '#/components/schemas/Meta'

    # ============================================
    # SPIN TAG SCHEMAS
    # ============================================
    SpinTagCreate:
      type: object
      required:
        - placeholder
        - words
      properties:
        placeholder:
          type: string
          maxLength: 255
          pattern: '^[a-zA-Z0-9_]+$'
          description: Unique placeholder name (alphanumeric and underscores only). Used in broadcasts as `{spin:placeholder_name}`.
          example: greeting
        words:
          type: array
          items:
            type: string
            maxLength: 255
          minItems: 1
          description: List of word/phrase variations for random rotation
          example: ["Hello", "Hi", "Hey", "Greetings"]

    SpinTagUpdate:
      type: object
      required:
        - words
      properties:
        words:
          type: array
          items:
            type: string
            maxLength: 255
          minItems: 1
          description: Updated list of word/phrase variations
          example: ["Hello", "Hi", "Hey", "Greetings", "Welcome"]

    SpinTag:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        placeholder:
          type: string
          description: Placeholder name
        tag:
          type: string
          description: Full spin tag (e.g., `{spin:greeting}`)
        words:
          type: array
          items:
            type: string
          description: Word/phrase variations
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SpinTagResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/SpinTag'
        message:
          type: string

    SpinTagsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/SpinTag'
        meta:
          $ref: '#/components/schemas/Meta'

    # ============================================
    # SEGMENT SCHEMAS
    # ============================================
    SegmentCreate:
      type: object
      required:
        - name
        - list_ids
      properties:
        name:
          type: string
          description: Segment name
          example: Active Buyers
        list_ids:
          type: array
          items:
            type: integer
          description: Lists to segment from
          example: [1, 2, 3]
        conditions:
          type: array
          description: Filtering conditions for the segment
          items:
            type: object
            properties:
              field:
                type: string
                description: Field to filter on
              operator:
                type: string
                enum: [equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than]
              value:
                type: string

    SegmentUpdate:
      type: object
      properties:
        name:
          type: string

    Segment:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
        list_ids:
          type: array
          items:
            type: integer
          description: IDs of lists included in this segment
        segment_query:
          type: object
          nullable: true
          description: Parsed segment conditions/query structure
        status:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SegmentResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/Segment'
        message:
          type: string

    SegmentsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Segment'
        meta:
          $ref: '#/components/schemas/Meta'

    SegmentContactsCountResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            segment_id:
              type: integer
            segment_name:
              type: string
            contacts_count:
              type: integer
        message:
          type: string

    # ============================================
    # TRIGGER SCHEMAS
    # ============================================
    TriggerUpdate:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
          description: Trigger display name
          example: Welcome Email Trigger
        status:
          type: boolean
          description: Enable (true) or disable (false) the trigger
          example: true

    Trigger:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
        trigger_type:
          type: string
        status:
          type: boolean
        meta_attributes:
          type: object
          nullable: true
          description: Trigger configuration metadata
        first_activity_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TriggerResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/Trigger'
        message:
          type: string

    TriggersResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Trigger'
        meta:
          $ref: '#/components/schemas/Meta'

    # ============================================
    # DRIP SCHEMAS
    # ============================================
    Drip:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        autoresponder_group_id:
          type: integer
          nullable: true
        name:
          type: string
        subject:
          type: string
        from_name:
          type: string
        from_email:
          type: string
        reply_to:
          type: string
          nullable: true
        delay_type:
          type: string
          nullable: true
        delay_value:
          type: integer
          nullable: true
        delay_unit:
          type: string
          nullable: true
        campaign_id:
          type: integer
          nullable: true
        status:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DripResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/Drip'
        message:
          type: string

    DripsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Drip'
        meta:
          $ref: '#/components/schemas/Meta'

    # ============================================
    # WEB FORM SCHEMAS
    # ============================================
    WebFormUpdate:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
          description: Web form name
          example: Newsletter Signup Form

    WebForm:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
        type:
          type: string
        form_attributes:
          type: object
          nullable: true
          description: Form configuration and field layout
        design_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    WebFormResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/WebForm'
        message:
          type: string

    WebFormsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/WebForm'
        meta:
          $ref: '#/components/schemas/Meta'

    WebFormEmbedCodeResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            form_id:
              type: integer
            form_name:
              type: string
            embed_code:
              type: string
              description: HTML iframe snippet for embedding the form
            direct_url:
              type: string
              format: uri
              description: Direct URL to the standalone form page
        message:
          type: string

    # ============================================
    # GROUP SCHEMAS
    # ============================================
    GroupCreate:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          maxLength: 255
          description: Group name
          example: Marketing Lists
        parent_group_id:
          type: integer
          description: Parent group ID for nesting (omit for top-level)

    GroupUpdate:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
          example: Updated Group Name

    Group:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
        module:
          type: string
          nullable: true
          description: Module this group belongs to
        section_id:
          type: integer
          description: "Section identifier (1=list, 2=sendingNode, 3=broadcast, 4=customField)"
        parent_group_id:
          type: integer
          nullable: true
        is_movable:
          type: boolean
          description: Whether this group can be renamed or deleted
        items_count:
          type: integer
          description: Number of items in this group
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    GroupResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/Group'
        message:
          type: string

    GroupsResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Group'
        meta:
          $ref: '#/components/schemas/Meta'

    # ============================================
    # STATISTICS SCHEMAS
    # ============================================
    StatsSummaryResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            broadcast_id:
              type: integer
            broadcast_name:
              type: string
            status:
              type: string
              description: Broadcast delivery status
            total_scheduled:
              type: integer
              description: Total emails scheduled for delivery
            sent:
              type: integer
              description: Total emails sent
            delivered:
              type: object
              properties:
                count:
                  type: integer
                  description: Sent minus bounced
                rate:
                  type: number
                  format: float
                  description: Delivered rate as a percentage of sent
            opened:
              type: object
              properties:
                count:
                  type: integer
                  description: Unique opens
                rate:
                  type: number
                  format: float
                  description: Open rate as a percentage
            clicked:
              type: object
              properties:
                count:
                  type: integer
                  description: Unique clicks
                rate:
                  type: number
                  format: float
                  description: Click rate as a percentage
            ctr:
              type: number
              format: float
              description: Click-to-open rate (clicks / opens * 100)
            bounced:
              type: object
              properties:
                count:
                  type: integer
                  description: Total bounced emails
                rate:
                  type: number
                  format: float
                  description: Bounce rate as a percentage
            unsubscribed:
              type: object
              properties:
                count:
                  type: integer
                  description: Recipients who unsubscribed
                rate:
                  type: number
                  format: float
                  description: Unsubscribe rate as a percentage
            tracking:
              type: object
              properties:
                opens_enabled:
                  type: boolean
                  description: Whether open tracking was enabled for this broadcast
                clicks_enabled:
                  type: boolean
                  description: Whether click tracking was enabled for this broadcast
            timing:
              type: object
              properties:
                started_at:
                  type: string
                  format: date-time
                  nullable: true
                  description: When sending started
                finished_at:
                  type: string
                  format: date-time
                  nullable: true
                  description: When sending completed
            created_at:
              type: string
              format: date-time

    OpenEvent:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
        subscriber_id:
          type: integer
        smtp:
          type: string
          description: Name of the pool the broadcast was sent through
        created_at:
          type: string
          format: date-time

    OpenEventsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/OpenEvent'
        meta:
          $ref: '#/components/schemas/Meta'

    ClickEvent:
      type: object
      properties:
        id:
          type: integer
        subscriber_email:
          type: string
        subscriber_id:
          type: integer
        link_url:
          type: string
          description: The URL that was clicked
        created_at:
          type: string
          format: date-time

    ClickEventsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/ClickEvent'
        meta:
          $ref: '#/components/schemas/Meta'

    BounceEvent:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
        type:
          type: string
          description: Bounce classification (hard/soft)
        code:
          type: string
          description: Bounce status code
        reason:
          type: string
          description: Bounce reason text
        smtp:
          type: string
          description: Name of the pool
        message_id:
          type: string
        subscriber_id:
          type: integer
        created_at:
          type: string
          format: date-time

    BounceEventsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/BounceEvent'
        meta:
          $ref: '#/components/schemas/Meta'

    UnsubscribeEvent:
      type: object
      properties:
        subscriber_id:
          type: integer
        email:
          type: string
        unsubscribed_at:
          type: string
          format: date-time

    UnsubscribeEventsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/UnsubscribeEvent'
        meta:
          $ref: '#/components/schemas/Meta'

    ComplaintEvent:
      type: object
      properties:
        id:
          type: integer
        subscriber_id:
          type: integer
        created_at:
          type: string
          format: date-time

    ComplaintEventsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/ComplaintEvent'
        meta:
          $ref: '#/components/schemas/Meta'

    SendingLogEntry:
      type: object
      properties:
        subscriber_id:
          type: integer
        email:
          type: string
        message_id:
          type: string
        is_bounced:
          type: boolean
        is_sent:
          type: boolean
        is_delivered:
          type: boolean
        is_delayed:
          type: boolean
        is_spammed:
          type: boolean
        is_injected:
          type: boolean
        smtp:
          type: string
          description: Name of the pool the broadcast was sent through
        created_at:
          type: string
          format: date-time

    SendingLogsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/SendingLogEntry'
        meta:
          $ref: '#/components/schemas/Meta'

    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
        role:
          type: string
        is_client:
          type: boolean
        status:
          type: string
          enum: [active, provisioning, suspended, terminated]
        package_id:
          type: integer
          nullable: true
        timezone:
          type: string
        country:
          type: string
        country_code:
          type: string
        address_line_1:
          type: string
        address_line_2:
          type: string
          nullable: true
        city:
          type: string
        state:
          type: string
        post_code:
          type: string
        phone:
          type: string
        mobile:
          type: string
        login_ips:
          description: Comma-separated allowed login IPs or array
        last_login_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    UserResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/User'
        message:
          type: string
