openapi: 3.1.0
info:
  title: Peeker Partner API
  version: '1.0.0'
  description: |
    White-label Peeker's email infrastructure: Google Workspace and Microsoft
    365 inboxes, deliverability monitoring, and self-healing domain swaps,
    from your own app. Create a provider workspace target, order licenses with imported or
    Peeker-managed domains, and Peeker's backend handles provisioning,
    DNS, forwarding, and lifecycle webhooks.

    Create a provider workspace target with `POST /workspaces`, then send its
    `wrk_...` ID to `POST /orders`. For Smartlead, create one main workspace,
    then add `client_id` to an order only when it should go to a client
    workspace. Omit it to order into the main workspace.

    ## Read first

    - **[Authentication](/authentication)**: bearer keys, live vs sandbox, request IDs.
    - **[Rate limits](/rate-limits)**: 600/min per key, 429 backoff.
    - **[Webhooks](/webhooks)**: signed events and delivery rules.
    - **[Async jobs](/concepts/async-jobs)**: what to poll vs subscribe to.
    - **[Best practices](/best-practices)**: errors, paging, idempotency.

    ## Concepts

    - **[Orders](/concepts/orders)**: statuses, sizing, pending actions, cancel.
    - **[Domains](/concepts/domains)**: inventory, `usable_for`, import, forwarding.
    - **[Bundles](/concepts/bundles)**: saved templates and capacity.
    - **[Swaps](/concepts/swaps)**: standard vs premium, renames.

    ## Workflow guides

    - **[Provider ordering](/guides/provider-ordering)** — create a provider workspace target and order through it.
    - **[Buying domains & ordering](/guides/buying-domains)** — the default flow: bundle, availability, order, webhooks.
    - **[Importing domains](/guides/importing-domains)** — bring in existing customer domains.
    - **[Forwarding](/guides/forwarding)** — point a domain at a marketing site.
    - **[How to implement domain swaps](/guides/domain-swaps)** — standard vs premium swaps.
    - **[Renaming inbox senders](/guides/user-name-swaps)** — change mailbox personas on a warm domain.

    ## Public ID prefixes

    | Prefix | Resource |
    | --- | --- |
    | `cus_` | Customer tracking record |
    | `wrk_` | Provider workspace |
    | `bun_` | Bundle |
    | `ord_` | Order |
    | `dom_` | Domain |
    | `pcl_` | Pool reservation |
    | `prp_` | Pool replacement |
    | `prl_` | Pool release |
    | `imp_` | Domain import job |
    | `fwj_` | Domain forwarding job |
    | `brj_` | Burned-domain replacement job |
    | `swp_` | Domain swap |
    | `act_` | Pending action |
    | `evt_` | Webhook event |

servers:
  - url: https://api.peeker.ai/partner/v1
    description: Same base URL for live and sandbox. The key prefix (`pk_live_…` / `pk_test_…`) picks the environment.

security:
  - BearerAuth: []

tags:
  - name: Account
    x-group: Account
    description: |
      Confirm an API key with `GET /me`. Returns environment (live or sandbox),
      permission preset, and the partner profile. See [Authentication](/authentication).
  - name: Provider workspaces
    x-group: Provider workspaces
    description: |
      A provider workspace is one reusable provider connection. Create it once,
      keep the returned `wrk_...` ID, and use it in orders and pool attach.
      Optional customer tracking stays on the target. Provider credentials stay
      internal and are never echoed.

      EmailBison requires a workspace API-user key. Super-admin keys are
      rejected. Instantly and Smartlead connections require `api_key`.
      Smartlead workspace creation takes a main API key and a label. Do not send
      `client_id` or customer data. Its login email and password are optional,
      but must be sent together. Google orders do not need the pair, and orders
      never accept it. Smartlead Microsoft orders and pool attach still use the
      workspace-saved pair. Add
      `client_id` to the order destination when the order belongs to a client
      workspace; omit it for the main workspace.
      PlusVibe setup requires `api_key`, remote `workspace_id`, and its login
      email/password pair so orders and pool attach work immediately. Instantly
      login credentials belong on an order destination.
  - name: Bundles
    x-group: Bundles
    description: |
      Saved order template sized by monthly sending volume (`bun_…`). Pass
      `bundle_id` on an order instead of custom license counts.

      Lifecycle: [How bundles work](/concepts/bundles).
  - name: Domains
    x-group: Domains
    description: |
      Sending domains in your partner account (`dom_…`). Same ID whether or not
      the domain is on an order.

      **Status values**

      | Value | What it means |
      | --- | --- |
      | `available` | The domain is in inventory but not on an order yet. |
      | `in_progress` | We're setting it up. |
      | `active` | Live and warming or healthy. |
      | `failed` | Something broke that we can't fix automatically. |
      | `action_required` | We need your help (e.g. update nameservers). |

      **Inventory vs connected**

      - `category=inventory` — yours but not on any order. Inventory rows
        don't include `provider`.
      - `category=connected` — on an active order. The row carries the link
        fields `provider`, `customer_id`, `order_id`, `usable_for`, and
        `forwarding_url`.

      **`usable_for` matrix**

      When you check a domain's availability:

      | Who's on the domain | `usable_for` |
      | --- | --- |
      | Nobody, or just Google | `["google", "microsoft"]` |
      | Microsoft (or both) | `["google"]` |
      | Premium / invalid / errored | not orderable; `available: false` with a `reason` |

      **Imports and forwarding are jobs**

      Both **import** and **forwarding** are async jobs. Posting to those
      endpoints starts the job and gives you back a row with `status:
      "in_progress"`. Poll the job's GET endpoint to see when it finishes:

      - Import jobs: prefix `imp_…`. Includes the `nameserver_groups` your
        customer needs to set at their registrar.
        - Forwarding jobs: prefix `fwj_…`. Already-correct domains are
          returned in `completed` with newly updated domains.
  - name: Domain pool
    x-group: Domain pool
    description: |
      Assign an exact quantity from pre-warmed Google inventory. Each domain
      needs at least one active Google inbox on a paid Google order (commonly
      two), complete provider mailbox identity, and no explicit burned provider
      result. Attach is all-or-nothing: full quantity or
      `insufficient_inventory` with no pool change. Attach and release are
      async. Poll the resource or subscribe to `pool.*` webhooks.

      Guide: [Pre-warmed pool](/guides/pre-warmed-pool).
  - name: Orders
    x-group: Orders
    description: |
      One purchase of domains plus Google and/or Microsoft licenses for one
      customer (`ord_…`). Use a saved `bundle_id` **or** custom license counts,
      never both. Requires `Idempotency-Key`.

      Lifecycle: [How orders work](/concepts/orders).
  - name: Domain swaps
    x-group: Domain swaps
    description: |
      Replace a domain inside an existing order without losing the order
      (`swp_…`). For persona-only changes use `POST /swaps/user_names`.

      Statuses and standard vs premium: [How swaps work](/concepts/swaps).
  - name: Webhooks
    x-group: Webhooks
    description: |
      Signed JSON POSTs for orders, domains, imports, forwarding, pool, and
      swaps. Register the URL and secret in the
      [Partner portal](https://app.peeker.ai/partner/webhooks). Event IDs look
      like `evt_01HZX`.

      Delivery is at least once (up to 5 attempts). Verify `Peeker-Signature`
      and dedupe `Peeker-Event-Id` for at least 24 hours.

      Full catalog: [Webhooks](/webhooks).

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Partner API key (`pk_live_…` or `pk_test_…`).
      x-default: Bearer pk_test_<your-test-key>

  parameters:
    PerPage:
      in: query
      name: per_page
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
      description: How many rows per page (default 25, max 100).
    PageToken:
      in: query
      name: page_token
      schema:
        type: string
      description: Opaque cursor from `links.next`. Pass it through as-is.

    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 256
      description: |
        Required for order creation. Pool attach also supports this header for
        durable retries. Workspace creation does not use it. The value is
        unique across the Partner account.
        Reuse it only with the same endpoint and unchanged request body. When
        retrying an Instantly order after
        `422 provider_login_credentials_required`, you may add or replace only
        `destination.login_credentials`; every other field must stay unchanged.
        For a Smartlead Microsoft or PlusVibe order with that error, repair the
        target through `POST /workspaces`, then retry the unchanged order with
        the same key.
        Retry `503 provider_unavailable` or `500 internal_error` with the same
        key and unchanged body.
        After pool attach returns `409 insufficient_inventory`, wait for pool
        inventory to be replenished, then retry the unchanged body with the
        same key. After `422 provider_login_credentials_required` or
        `422 provider_credentials_invalid`, repair the target workspace, then
        retry the unchanged body with the same key.
  responses:
    InvalidRequest:
      description: Bad request body, or fields that don’t belong to this provider.
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Unauthorized:
      description: Missing, invalid, or revoked API key.
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: Valid key, but it doesn’t allow this action.
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotFound:
      description: A referenced workspace, customer, bundle, domain, or order was not found.
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Conflict:
      description: Conflicts with an existing resource or idempotency key.
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    ProviderCredentialsRejected:
      description: Provider rejected the credentials (or an EmailBison super-admin key was used).
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            invalid_credentials:
              value:
                error:
                  code: provider_credentials_invalid
                  message: Provider credentials were rejected.
                  request_id: 550e8400-e29b-41d4-a716-446655440000
                  field: credentials
            super_admin_key:
              value:
                error:
                  code: emailbison_super_admin_key
                  message: Use an EmailBison workspace/user API key; super-admin keys are not accepted.
                  request_id: 550e8400-e29b-41d4-a716-446655440000
                  field: credentials.api_key
    RateLimited:
      description: Too many requests. Wait `error.details.retry_after_seconds`, then retry.
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    InternalError:
      description: Unexpected server error. Retry with backoff and keep the request ID.
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    ProviderUnavailable:
      description: Provider is temporarily unavailable. Retry with backoff.
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    UnprocessableEntity:
      description: Credentials missing/rejected, or the order can’t be processed as sent.
      headers:
        Peeker-Request-Id:
          $ref: '#/components/headers/PeekerRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    BundleResponse:
      description: A bundle.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Bundle'
          examples:
            bundle:
              summary: A starter bundle
              value:
                data:
                  id: bun_01HZX0BU1A2B3C4D5E6F7G8H
                  name: Starter 25k emails/month
                  monthly_sending_volume: 25000
                  google_percent: null
                  microsoft_percent: null
                  send_on_weekends: false
                  google_licenses: 2
                  status: active
                  effective:
                    google_percent: 50
                    microsoft_percent: 50
                  required_domains:
                    google: 15
                    microsoft: 4
                    total: 19
                  created_at: '2026-05-08T12:00:00Z'
    BundleListResponse:
      description: A page of bundles.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Bundle'
                  links:
                    $ref: '#/components/schemas/ListLinks'
                  meta:
                    $ref: '#/components/schemas/ListMeta'
          examples:
            typical_page:
              summary: Two bundles
              value:
                data:
                  - id: bun_01HZX0BU1A2B3C4D5E6F7G8H
                    name: Starter 25k emails/month
                    monthly_sending_volume: 25000
                    google_percent: null
                    microsoft_percent: null
                    send_on_weekends: false
                    google_licenses: 2
                    status: active
                    effective: { google_percent: 50, microsoft_percent: 50 }
                    required_domains: { google: 15, microsoft: 4, total: 19 }
                    created_at: '2026-05-08T12:00:00Z'
                  - id: bun_01HZX0BU2A2B3C4D5E6F7G8H
                    name: Pro 100k emails/month
                    monthly_sending_volume: 100000
                    google_percent: 70
                    microsoft_percent: 30
                    send_on_weekends: false
                    google_licenses: 2
                    status: active
                    effective: { google_percent: 70, microsoft_percent: 30 }
                    required_domains: { google: 80, microsoft: 10, total: 90 }
                    created_at: '2026-04-01T10:00:00Z'
                links:
                  first: https://api.peeker.ai/partner/v1/bundles?per_page=25
                  next: null
                meta:
                  path: https://api.peeker.ai/partner/v1/bundles
                  per_page: 25
                  returned: 2
    BundleDeleteResponse:
      description: A deleted or archived bundle.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    oneOf:
                      - $ref: '#/components/schemas/Bundle'
                      - type: object
                        required: [id, status]
                        properties:
                          id:
                            type: string
                            example: bun_01HZX0BU1A2B3C4D5E6F7G8H
                          status:
                            type: string
                            enum: [deleted]
          examples:
            deleted:
              summary: Bundle was never used, so it was deleted
              value:
                data:
                  id: bun_01HZX0BU1A2B3C4D5E6F7G8H
                  status: deleted
            archived:
              summary: Bundle had historical orders, so it was archived
              value:
                data:
                  id: bun_01HZX0BU1A2B3C4D5E6F7G8H
                  name: Starter 25k emails/month
                  monthly_sending_volume: 25000
                  google_percent: null
                  microsoft_percent: null
                  send_on_weekends: false
                  google_licenses: 2
                  status: archived
                  effective: { google_percent: 50, microsoft_percent: 50 }
                  required_domains: { google: 15, microsoft: 4, total: 19 }
                  created_at: '2026-05-08T12:00:00Z'
    DomainResponse:
      description: A domain.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DomainListItem'
          examples:
            connected:
              summary: Connected to an active order
              value:
                data:
                  id: dom_01HZX0D01A2B3C4D5E6F7G8H
                  domain: acme-mail.com
                  category: connected
                  status: active
                  provider: google
                  customer_id: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
                  order_id: ord_01HZX0OR1A2B3C4D5E6F7G8H
                  usable_for: [google, microsoft]
                  forwarding_url: https://acme.com
                  smartlead_client_id: '301'
                  created_at: '2026-05-08T12:00:00Z'
            inventory:
              summary: Inventory (not on an order)
              value:
                data:
                  id: dom_01HZX0D03A2B3C4D5E6F7G8H
                  domain: spare-acme.com
                  category: inventory
                  status: available
                  usable_for: [google, microsoft]
                  created_at: '2026-04-30T10:00:00Z'
    DomainListResponse:
      description: A page of domains.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DomainListItem'
                  links:
                    $ref: '#/components/schemas/ListLinks'
                  meta:
                    $ref: '#/components/schemas/ListMeta'
          examples:
            typical_page:
              summary: Mix of connected + inventory
              value:
                data:
                  - id: dom_01HZX0D01A2B3C4D5E6F7G8H
                    domain: acme-mail.com
                    category: connected
                    status: active
                    provider: google
                    customer_id: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
                    order_id: ord_01HZX0OR1A2B3C4D5E6F7G8H
                    usable_for: [google, microsoft]
                    forwarding_url: https://acme.com
                    smartlead_client_id: '301'
                    created_at: '2026-05-08T12:00:00Z'
                  - id: dom_01HZX0D03A2B3C4D5E6F7G8H
                    domain: spare-acme.com
                    category: inventory
                    status: available
                    usable_for: [google, microsoft]
                    created_at: '2026-04-30T10:00:00Z'
                links:
                  first: https://api.peeker.ai/partner/v1/domains?per_page=25
                  next: https://api.peeker.ai/partner/v1/domains?per_page=25&page_token=eyJjdXJzb3IiOiJ4eXoifQ
                meta:
                  path: https://api.peeker.ai/partner/v1/domains
                  per_page: 25
                  returned: 2
    DomainImportJobResponse:
      description: Current state of an import job.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DomainImportJob'
          examples:
            in_progress:
              summary: Async job still running
              value:
                data:
                  id: imp_01HZX0IM1A2B3C4D5E6F7G8H
                  status: in_progress
                  submitted_count: 2
                  completed_count: 0
                  failed_count: 0
                  nameserver_groups:
                    - nameserver_one: helena.ns.cloudflare.com
                      nameserver_two: idris.ns.cloudflare.com
                      domains: [acme-mail.com, team-acme.com]
                  failed: []
                  created_at: '2026-05-08T12:00:00Z'
            completed:
              summary: All domains imported
              value:
                data:
                  id: imp_01HZX0IM1A2B3C4D5E6F7G8H
                  status: completed
                  submitted_count: 2
                  completed_count: 2
                  failed_count: 0
                  nameserver_groups:
                    - nameserver_one: helena.ns.cloudflare.com
                      nameserver_two: idris.ns.cloudflare.com
                      domains: [acme-mail.com, team-acme.com]
                  failed: []
                  created_at: '2026-05-08T12:00:00Z'
                  completed_at: '2026-05-08T12:08:43Z'
    DomainImportJobListResponse:
      description: Import jobs for your account.
      content:
        application/json:
          schema:
            type: object
            required: [data, links, meta]
            properties:
              data:
                type: array
                items:
                  $ref: '#/components/schemas/DomainImportJobListItem'
              links:
                $ref: '#/components/schemas/ListLinks'
              meta:
                $ref: '#/components/schemas/ListMeta'
          examples:
            import_jobs:
              summary: Newest import jobs
              value:
                data:
                  - id: imp_01HZX0IM1A2B3C4D5E6F7G8H
                    status: completed
                    submitted_count: 25
                    completed_count: 25
                    failed_count: 0
                    nameserver_groups:
                      - nameserver_one: helena.ns.cloudflare.com
                        nameserver_two: idris.ns.cloudflare.com
                        domains: [acme-mail.com, team-acme.com]
                    created_at: '2026-05-08T12:00:00Z'
                    completed_at: '2026-05-08T12:08:43Z'
                links:
                  first: https://api.peeker.ai/partner/v1/domains/import/jobs?per_page=25
                  next: null
                meta:
                  path: https://api.peeker.ai/partner/v1/domains/import/jobs
                  per_page: 25
                  returned: 1
    DomainImportLookupResponse:
      description: Import job and nameserver lookup by domain.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DomainImportLookup'
          examples:
            focused:
              summary: One requested domain
              value:
                data:
                  domains:
                    - domain: acme-mail.com
                      status: completed
                      import_job_id: imp_01HZX0IM1A2B3C4D5E6F7G8H
                      nameserver_one: helena.ns.cloudflare.com
                      nameserver_two: idris.ns.cloudflare.com
            include_group:
              summary: Include full nameserver group
              value:
                data:
                  domains:
                    - domain: acme-mail.com
                      status: completed
                      import_job_id: imp_01HZX0IM1A2B3C4D5E6F7G8H
                      nameserver_one: helena.ns.cloudflare.com
                      nameserver_two: idris.ns.cloudflare.com
                  nameserver_groups:
                    - import_job_id: imp_01HZX0IM1A2B3C4D5E6F7G8H
                      nameserver_one: helena.ns.cloudflare.com
                      nameserver_two: idris.ns.cloudflare.com
                      domains: [acme-mail.com, team-acme.com]
    BurnedReplaceJobResponse:
      description: >-
        Status and counts for a burned-domain replace job. Embedded result
        arrays are capped. Page `/results` for every row.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/BurnedReplaceJob'
          examples:
            completed:
              summary: Both burned domains replaced
              value:
                data:
                  id: brj_01HZX0BR1A2B3C4D5E6F7G8H
                  status: completed
                  submitted_count: 2
                  completed_count: 2
                  failed_count: 0
                  results_returned_count: 2
                  results_limit: 5000
                  results_truncated: false
                  completed:
                    - domain: acme-mail.com
                      replacement_domain: acme-fresh.com
                      swap_id: swp_01HZX0SW1A2B3C4D5E6F7G8H
                    - domain: team-acme.com
                      replacement_domain: team-acme-2.com
                      swap_id: swp_01HZX0SW2A2B3C4D5E6F7G8H
                  failed: []
                  created_at: '2026-05-08T12:00:00Z'
                  completed_at: '2026-05-08T12:01:30Z'

    DomainForwardingJobResponse:
      description: Current state of a forwarding job.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DomainForwardingJob'
          examples:
            completed:
              summary: Every domain is now pointed at the requested URL
              value:
                data:
                  id: fwj_01HZX0FW1A2B3C4D5E6F7G8H
                  status: completed
                  completed_count: 3
                  failed_count: 0
                  forwarding_url: https://acme.com
                  completed: [acme-mail.com, team-acme.com, news-acme.com]
                  failed: []
                  created_at: '2026-05-08T12:00:00Z'
                  completed_at: '2026-05-08T12:01:30Z'
    OrderSummaryResponse:
      description: Lean order receipt returned by create and list endpoints.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/OrderSummary'
          examples:
            new_order:
              summary: Newly accepted order receipt
              value:
                data:
                  id: ord_01HZX0OR1A2B3C4D5E6F7G8H
                  customer_id: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
                  bundle_id: bun_123
                  smartlead_client_id: null
                  destination:
                    provider: plusvibe
                    workspace_id: wrk_01HZX0PV1A2B3C4D5E6F7G8H
                  status: in_progress
                  domain_count: 2
                  costs:
                    total_cents: 6800
                    currency: usd
                  renews_at: '2026-06-08T12:00:00Z'
                  created_at: '2026-05-08T12:00:00Z'

    OrderResponse:
      description: Full order from `GET /orders/{id}`. Webhooks send a smaller snapshot. Fetch here for the latest full state.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Order'
          examples:
            detail:
              summary: Order detail
              value:
                data:
                  id: ord_01HZX0OR1A2B3C4D5E6F7G8H
                  customer_id: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
                  bundle_id: bun_123
                  smartlead_client_id: null
                  destination:
                    provider: instantly
                    workspace_id: wrk_01HZX0JN1A2B3C4D5E6F7G8H
                  status: in_progress
                  domain_count: 2
                  costs:
                    total_cents: 12400
                    currency: usd
                    line_items:
                      - type: microsoft_license
                        quantity: 2
                        amount_cents: 10000
                      - type: domain_registration
                        quantity: 2
                        amount_cents: 2400
                  domains:
                    registered: [domain-01.com, domain-02.com]
                    imported: []
                  personas:
                    - first_name: Sam
                      last_name: Lee
                      profile_picture_url: ''
                  renews_at: '2026-06-08T12:00:00Z'
                  created_at: '2026-05-08T12:00:00Z'
    OrderCancellationResponse:
      description: Cancellation schedule for the order or selected domains.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/OrderCancellation'
          examples:
            whole_order:
              summary: Whole order cancellation scheduled
              value:
                data:
                  id: ord_01HZX0OR1A2B3C4D5E6F7G8H
                  status: cancel_scheduled
                  cancellation_scheduled_at: '2026-05-08T12:00:00Z'
                  cancels_at: '2026-06-07T12:00:00Z'
                  domains:
                    - id: dom_01HZX0D01A2B3C4D5E6F7G8H
                      domain: domain-01.com
                    - id: dom_01HZX0D02A2B3C4D5E6F7G8H
                      domain: domain-02.com
            selected_domains:
              summary: Selected domains cancellation scheduled
              value:
                data:
                  id: ord_01HZX0OR1A2B3C4D5E6F7G8H
                  status: cancel_scheduled
                  cancellation_scheduled_at: '2026-05-08T12:00:00Z'
                  cancels_at: '2026-06-07T12:00:00Z'
                  domains:
                    - id: dom_01HZX0D02A2B3C4D5E6F7G8H
                      domain: acme-mail.com
    OrderListResponse:
      description: A page of orders.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/OrderSummary'
                  links:
                    $ref: '#/components/schemas/ListLinks'
                  meta:
                    $ref: '#/components/schemas/ListMeta'
          examples:
            typical_page:
              summary: Two recent orders
              value:
                data:
                  - id: ord_01HZX0OR1A2B3C4D5E6F7G8H
                    customer_id: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
                    bundle_id: bun_01HZX0BU1A2B3C4D5E6F7G8H
                    smartlead_client_id: '301'
                    destination:
                      provider: smartlead
                      workspace_id: wrk_01HZX0SK1A2B3C4D5E6F7G8H
                      client_id: '301'
                    status: in_progress
                    domain_count: 2
                    costs:
                      total_cents: 13000
                      currency: usd
                    renews_at: '2026-06-08T12:00:00Z'
                    created_at: '2026-05-08T12:00:00Z'
                  - id: ord_01HZX0OR2A2B3C4D5E6F7G8H
                    customer_id: cus_01HZX0C7Z3K4M5N6P7Q8R9S0
                    bundle_id: null
                    smartlead_client_id: null
                    destination:
                      provider: plusvibe
                      workspace_id: wrk_01HZX0PV1A2B3C4D5E6F7G8H
                    status: completed
                    domain_count: 1
                    costs:
                      total_cents: 4000
                      currency: usd
                    renews_at: '2026-05-30T16:22:00Z'
                    created_at: '2026-04-30T16:22:00Z'
                links:
                  first: https://api.peeker.ai/partner/v1/orders?per_page=25
                  next: https://api.peeker.ai/partner/v1/orders?per_page=25&page_token=eyJjdXJzb3IiOiJ4eXoifQ
                meta:
                  path: https://api.peeker.ai/partner/v1/orders
                  per_page: 25
                  returned: 2
    PendingActionListResponse:
      description: A page of pending action rows.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PendingAction'
                  links:
                    $ref: '#/components/schemas/ListLinks'
                  meta:
                    $ref: '#/components/schemas/ListMeta'
          examples:
            open_actions:
              summary: Open actions for one order
              value:
                data:
                  - id: act_01HZX0AC1A2B3C4D5E6F7G8H
                    type: google_profile_picture
                    reason: profile_picture_not_accessible
                    status: open
                    domain_id: dom_01HZX0D01A2B3C4D5E6F7G8H
                    domain: acme-mail.com
                    user:
                      first_name: Sam
                      last_name: Lee
                      profile_picture_url: https://cdn.acme.com/sam.jpg
                    message: The profile picture URL must be a publicly accessible image URL.
                    created_at: '2026-05-08T12:00:00Z'
                links:
                  first: https://api.peeker.ai/partner/v1/orders/pending?status=open&per_page=25
                  next: null
                meta:
                  path: https://api.peeker.ai/partner/v1/orders/pending
                  per_page: 25
                  returned: 1
    SwapResponse:
      description: A swap.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Swap'
          examples:
            premium:
              summary: Premium swap (old domain warm 14 days)
              value:
                data:
                  id: swp_01HZX0SW1A2B3C4D5E6F7G8H
                  type: premium
                  domain_id: dom_01HZX0D01A2B3C4D5E6F7G8H
                  replacement_domain: acme-fresh.com
                  status: created
                  created_at: '2026-05-08T12:00:00Z'
            standard:
              summary: Standard swap (old domain retired immediately)
              value:
                data:
                  id: swp_01HZX0SW2A2B3C4D5E6F7G8H
                  type: standard
                  domain_id: dom_01HZX0D02A2B3C4D5E6F7G8H
                  replacement_domain: team-acme-2.com
                  status: created
                  created_at: '2026-05-08T12:00:00Z'
    SwapListResponse:
      description: A page of swaps.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Swap'
                  links:
                    $ref: '#/components/schemas/ListLinks'
                  meta:
                    $ref: '#/components/schemas/ListMeta'
          examples:
            typical_page:
              summary: One premium + one user-names swap
              value:
                data:
                  - id: swp_01HZX0SW1A2B3C4D5E6F7G8H
                    type: premium
                    domain_id: dom_01HZX0D01A2B3C4D5E6F7G8H
                    replacement_domain: acme-fresh.com
                    status: in_progress
                    created_at: '2026-05-08T12:00:00Z'
                  - id: swp_01HZX0SW3A2B3C4D5E6F7G8H
                    type: user_names
                    domain_id: dom_01HZX0D02A2B3C4D5E6F7G8H
                    replacement_domain: null
                    status: completed
                    created_at: '2026-05-07T15:42:00Z'
                links:
                  first: https://api.peeker.ai/partner/v1/swaps?per_page=25
                  next: null
                meta:
                  path: https://api.peeker.ai/partner/v1/swaps
                  per_page: 25
                  returned: 2
    SwapBatchResponse:
      description: Accepted user-name swaps.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: object
                    required: [swaps]
                    properties:
                      swaps:
                        type: array
                        items:
                          $ref: '#/components/schemas/Swap'
          examples:
            two_swaps:
              summary: Renamed inboxes for two new personas
              value:
                data:
                  swaps:
                    - id: swp_01HZX0SW4A2B3C4D5E6F7G8H
                      type: user_names
                      domain_id: dom_01HZX0D02A2B3C4D5E6F7G8H
                      replacement_domain: null
                      status: in_progress
                      created_at: '2026-05-08T12:00:00Z'
    AckResponse:
      description: Acknowledged.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: object
    PoolReservationResponse:
      description: Durable pool reservation and attach state.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PoolReservation'
    PoolReleaseResponse:
      description: Durable pool release state.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PoolRelease'
    PoolAddResponse:
      description: Full active-subscription enrollment result.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: object
                    required: [added, existing, domains]
                    properties:
                      added: { type: integer, minimum: 0 }
                      existing: { type: integer, minimum: 0 }
                      domains:
                        type: array
                        items:
                          type: object
                          required: [domain, status]
                          properties:
                            domain: { type: string }
                            status:
                              type: string
                              enum:
                                [
                                  warming,
                                  available,
                                  reserved,
                                  renaming,
                                  assigned,
                                  releasing,
                                  quarantined
                                ]

  schemas:
    SuccessEnvelope:
      type: object
      required: [data]
      properties:
        data: {}

    PoolHealth:
      type: object
      additionalProperties: false
      required: [verdict, reasons, checked_at]
      properties:
        verdict: { type: string, enum: [healthy, warming, unhealthy] }
        reasons:
          type: array
          description: |
            Machine-readable health signals. For now the pool only emits
            `deliverability_burned`.
          items: { type: string }
        checked_at: { type: string, format: date-time }

    PoolDomain:
      type: object
      additionalProperties: false
      required:
        [
          domain,
          provider,
          inbox_count,
          assignable_inbox_count,
          assigned_inbox_count,
          held_inbox_count,
          availability,
          status,
          health,
          available_at,
          updated_at
        ]
      properties:
        domain: { type: string, example: acme-mail.com }
        provider: { type: string, enum: [google] }
        inbox_count: { type: integer, minimum: 1, description: Physical inboxes on the domain. }
        assignable_inbox_count: { type: integer, minimum: 0, maximum: 2 }
        assigned_inbox_count: { type: integer, minimum: 0 }
        held_inbox_count: { type: integer, minimum: 0 }
        availability: { type: string, enum: [available, claimed, unavailable] }
        status:
          type: string
          enum: [warming, available, reserved, renaming, assigned, releasing, quarantined]
        health:
          $ref: '#/components/schemas/PoolHealth'
        available_at:
          type: [string, 'null']
          format: date-time
        updated_at: { type: string, format: date-time }

    PoolAttachedDomain:
      type: object
      additionalProperties: false
      required:
        [
          requested_domain,
          substituted,
          domain,
          provider,
          inbox_count,
          assignable_inbox_count,
          assigned_inbox_count,
          held_inbox_count,
          availability,
          status,
          health,
          available_at,
          updated_at
        ]
      properties:
        requested_domain: { type: string, example: warm-domain.example }
        substituted:
          type: boolean
          description: True when the submitted name was no longer usable and Peeker assigned a fallback.
        domain: { type: string, example: acme-mail.com }
        provider: { type: string, enum: [google] }
        inbox_count: { type: integer, minimum: 1, description: Physical inboxes on the domain. }
        assignable_inbox_count: { type: integer, minimum: 0, maximum: 2 }
        assigned_inbox_count: { type: integer, minimum: 0 }
        held_inbox_count: { type: integer, minimum: 0 }
        availability: { type: string, enum: [available, claimed, unavailable] }
        status:
          type: string
          enum: [warming, available, reserved, renaming, assigned, releasing, quarantined]
        health:
          $ref: '#/components/schemas/PoolHealth'
        available_at:
          type: [string, 'null']
          format: date-time
        updated_at: { type: string, format: date-time }

    PoolDestination:
      type: object
      additionalProperties: false
      required: [workspace_id]
      properties:
        provider:
          type: string
          enum: [emailbison, instantly, smartlead, plusvibe]
          description: Provider for the connected workspace. When sent, it must match the target.
        workspace_id:
          type: string
          pattern: ^wrk_
          description: |
            Public workspace ID returned by `POST /workspaces`.
        client_id:
          type: string
          pattern: ^[1-9][0-9]*$
          maxLength: 64
          description: |
            Optional Smartlead client ID. Send only with `provider: smartlead`.
            Omit it for the main Smartlead workspace.

    PoolReservationDestination:
      type: object
      additionalProperties: false
      required: [provider, workspace_id, client_id]
      properties:
        provider: { type: string, enum: [emailbison, instantly, smartlead, plusvibe] }
        workspace_id: { type: string, pattern: ^wrk_ }
        client_id: { type: [string, 'null'] }

    PoolReservation:
      type: object
      additionalProperties: false
      required:
        [
          reservation_id,
          status,
          step,
          domain_count,
          inbox_count,
          destination,
          domains,
          error,
          created_at,
          updated_at,
          completed_at,
          released_at
        ]
      properties:
        reservation_id: { type: string, example: pcl_01HZX0PC1A2B3C4D5E6F7G8H }
        status:
          {
            type: string,
            enum:
              [
                reserved,
                renaming,
                action_required,
                completed,
                failed,
                releasing,
                release_failed,
                released
              ],
            description: 'reserved and renaming include automatic retries; action_required needs customer or partner action; failed means fulfillment was explicitly stopped'
          }
        step: { type: string }
        domain_count: { type: integer, minimum: 1, maximum: 500 }
        inbox_count: { type: integer, minimum: 2, maximum: 1000, multipleOf: 2 }
        destination:
          $ref: '#/components/schemas/PoolReservationDestination'
        domains:
          type: array
          items:
            $ref: '#/components/schemas/PoolAttachedDomain'
        error:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/PoolOperationError'
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        released_at: { type: [string, 'null'], format: date-time }

    PoolRelease:
      type: object
      additionalProperties: false
      required:
        [
          id,
          reservation_id,
          status,
          step,
          domain_count,
          inbox_count,
          effective_at,
          forced,
          rewarming_domain_count,
          quarantined_domain_count,
          error,
          created_at,
          updated_at,
          completed_at
        ]
      properties:
        id: { type: string, example: prl_01HZX0PR1A2B3C4D5E6F7G8H }
        reservation_id: { type: string, example: pcl_01HZX0PC1A2B3C4D5E6F7G8H }
        status: { type: string, enum: [scheduled, processing, completed, failed] }
        step: { type: string }
        domain_count: { type: integer, minimum: 1 }
        inbox_count: { type: integer, minimum: 1 }
        effective_at: { type: [string, 'null'], format: date-time }
        forced: { type: boolean }
        rewarming_domain_count: { type: [integer, 'null'], minimum: 0 }
        quarantined_domain_count: { type: [integer, 'null'], minimum: 0 }
        error:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/PoolOperationError'
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }

    PoolOperationError:
      type: object
      additionalProperties: false
      required: [code, message, retryable]
      properties:
        code: { type: string }
        message: { type: string }
        retryable: { type: boolean }

    Me:
      type: object
      required: [api_key, partner]
      properties:
        api_key:
          type: object
          required: [environment, permission_preset]
          properties:
            environment:
              type: string
              nullable: true
              enum: [live, test, null]
              description: Whether this key hits the live system or the sandbox.
              example: live
            permission_preset:
              type: string
              nullable: true
              enum: [full, read, limited, null]
              example: full
        partner:
          type: object
          required: [id, name]
          properties:
            id:
              type: string
              description: Your partner organization ID.
              example: org_01HZX0OG1A2B3C4D5E6F7G8H
            name:
              type: string
              example: Acme Resellers

    WebhookEvent:
      type: object
      required: [id, type, created_at, data]
      properties:
        id:
          type: string
          example: evt_01HZX0EV1A2B3C4D5E6F7G8H
          description: Unique event ID. Use to dedupe on your side.
        type:
          type: string
          enum:
            - domain.forwarding_failed
            - domain.forwarding_updated
            - domain_import.action_required
            - domain_import.completed
            - domain_import.failed
            - order.cancel_scheduled
            - order.cancelled
            - order.completed
            - order.domains_cancelled
            - order.failed
            - order.in_progress
            - pool.claim.completed
            - pool.claim.failed
            - pool.release.completed
            - pool.release.failed
            - pool.replacement.pending
            - pool.replacement.started
            - pool.replacement.completed
            - pool.replacement.declined
            - pool.replacement.failed
            - swap.completed
            - swap.created
            - swap.failed
            - swap.in_progress
          example: order.in_progress
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'
        data:
          type: object
          additionalProperties: true
          description: Event-specific resource snapshot. Fetch the matching resource when you need its latest full state.
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              description: Stable machine-readable code. Branch on this field.
              enum:
                - invalid_request
                - unauthorized
                - forbidden
                - not_found
                - rate_limited
                - provider_not_supported
                - provider_mismatch
                - provider_credentials_invalid
                - provider_login_credentials_required
                - provider_unavailable
                - emailbison_super_admin_key
                - smartlead_connection_required
                - smartlead_client_id_required
                - smartlead_client_not_found
                - idempotency_key_required
                - idempotency_conflict
                - idempotency_in_progress
                - workspace_promotion_required
                - workspace_credentials_conflict
                - order_shape_conflict
                - domain_count_mismatch
                - insufficient_inventory
                - domain_not_imported
                - domains_not_submittable
                - premium_domain_not_supported
                - domain_not_usable_for_provider
                - domain_already_in_active_order
                - internal_error
              example: emailbison_super_admin_key
            message:
              type: string
              description: Human-readable explanation. Wording may change.
              example: Use an EmailBison workspace/user API key; super-admin keys are not accepted.
            request_id:
              type: string
              description: Stable request identifier, identical to the `Peeker-Request-Id` response header.
              example: 550e8400-e29b-41d4-a716-446655440000
            field:
              type: string
              description: Dot path to the invalid request property, when applicable.
              example: credentials.api_key
            details:
              type: object
              additionalProperties: true
              description: Structured context specific to `error.code`.
    ListLinks:
      type: object
      required: [first, next]
      properties:
        first:
          type: string
          format: uri
          example: https://api.peeker.ai/partner/v1/domains?per_page=25
        next:
          type: string
          format: uri
          nullable: true
          example: https://api.peeker.ai/partner/v1/domains?per_page=25&page_token=eyJ...

    ListMeta:
      type: object
      required: [path, per_page, returned]
      properties:
        path:
          type: string
          format: uri
          example: https://api.peeker.ai/partner/v1/domains
        per_page:
          type: integer
          example: 25
        returned:
          type: integer
          example: 25

    EmailBisonCredentials:
      type: object
      additionalProperties: false
      required:
        - api_url
        - api_key
      properties:
        api_url:
          type: string
          minLength: 1
          maxLength: 2048
          description: Public EmailBison URL (stored as HTTPS). Private or local addresses are rejected.
          example: https://dedi.emailbison.com
        api_key:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 4096
          description: EmailBison workspace/user API key (not a super-admin key).
          example: <emailbison-workspace-api-key>
    InstantlyCredentials:
      type: object
      additionalProperties: false
      required:
        - api_key
      properties:
        api_key:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 4096
          description: Instantly API key for the workspace you want to connect.
          example: <instantly-api-key>
    SmartleadCredentials:
      type: object
      additionalProperties: false
      required:
        - api_key
      properties:
        api_key:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 4096
          description: Smartlead main API key.
          example: <smartlead-api-key>
    SmartleadWorkspaceCredentials:
      type: object
      additionalProperties: false
      required:
        - api_key
      dependentRequired:
        login_email:
          - login_password
        login_password:
          - login_email
      properties:
        api_key:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 4096
          description: Smartlead main API key.
          example: <smartlead-api-key>
        login_email:
          type: string
          format: email
          writeOnly: true
          maxLength: 320
          description: Optional Smartlead login. Send it together with login_password.
          example: automation@agency.com
        login_password:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 4096
          description: Optional Smartlead password. Send it together with login_email.
          example: <smartlead-login-password>
    PlusVibeCredentials:
      type: object
      additionalProperties: false
      required:
        - api_key
        - workspace_id
      properties:
        api_key:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 4096
          description: PlusVibe API key that can access this workspace.
          example: <plusvibe-api-key>
        workspace_id:
          type: string
          minLength: 1
          maxLength: 256
          description: |
            PlusVibe’s own workspace ID (not a Peeker `wrk_…` ID).
          example: 692577ed8543f416f83ad252
    PlusVibeSetupCredentials:
      type: object
      additionalProperties: false
      required:
        - api_key
        - workspace_id
        - login_email
        - login_password
      properties:
        api_key:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 4096
          description: PlusVibe API key that can access the selected PlusVibe workspace.
          example: <plusvibe-api-key>
        workspace_id:
          type: string
          minLength: 1
          maxLength: 256
          description: Remote PlusVibe workspace ID. This is not a Peeker `wrk_...` ID.
          example: 692577ed8543f416f83ad252
        login_email:
          type: string
          format: email
          writeOnly: true
          maxLength: 320
          description: PlusVibe login saved to the execution workspace for orders and pool attach.
          example: automation@agency.com
        login_password:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 4096
          description: PlusVibe login password saved to the execution workspace.
          example: <plusvibe-login-password>
    ProviderLoginCredentials:
      type: object
      additionalProperties: false
      required:
        - login_email
        - login_password
      properties:
        login_email:
          type: string
          format: email
          writeOnly: true
          maxLength: 320
          example: automation@agency.com
        login_password:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 4096
          example: <provider-login-password>
      description: |
        Optional Instantly login used to provision an order. Peeker saves a
        valid pair for later Instantly orders. EmailBison, Smartlead, and
        PlusVibe reject this field.
    WorkspaceCustomerTracking:
      type: object
      additionalProperties: false
      required: [reference]
      properties:
        reference:
          type: string
          maxLength: 256
          description: Partner-owned customer or account identifier.
        name: { type: string, maxLength: 256 }
        email: { type: string, format: email, maxLength: 320 }
      description: Optional partner-owned tracking data. It does not create a Peeker user or affect routing.
    WorkspaceCustomer:
      type: object
      additionalProperties: false
      required: [customer_id, reference, status]
      properties:
        customer_id:
          type: string
          pattern: ^cus_
          description: Stable Peeker ID for filtering orders, domains, pending actions, and swaps.
          example: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
        reference:
          type: string
          description: Partner-owned customer or account identifier.
          example: customer-42
        name:
          type: string
          example: Acme
        email:
          type: string
          format: email
          example: ops@acme.com
        status:
          type: string
          enum: [active, archived]
          example: active
    EmailBisonWorkspaceCreateRequest:
      title: EmailBison
      type: object
      additionalProperties: false
      required:
        - provider
        - credentials
      properties:
        provider:
          type: string
          enum:
            - emailbison
        promote_existing:
          type: boolean
          default: false
          description: Set `true` to reuse an existing Peeker EmailBison workspace with Partner API (not copy it).
        credentials:
          $ref: '#/components/schemas/EmailBisonCredentials'
        customer:
          $ref: '#/components/schemas/WorkspaceCustomerTracking'
    InstantlyWorkspaceCreateRequest:
      title: Instantly
      type: object
      additionalProperties: false
      required:
        - provider
        - credentials
      properties:
        provider:
          type: string
          enum:
            - instantly
        credentials:
          $ref: '#/components/schemas/InstantlyCredentials'
        customer:
          $ref: '#/components/schemas/WorkspaceCustomerTracking'
    SmartleadWorkspaceCreateRequest:
      title: Smartlead
      type: object
      additionalProperties: false
      required:
        - provider
        - label
        - credentials
      properties:
        provider:
          type: string
          enum:
            - smartlead
        label:
          type: string
          minLength: 1
          maxLength: 256
          description: Partner-facing name for this Smartlead connection.
          example: Agency Smartlead
        credentials:
          $ref: '#/components/schemas/SmartleadWorkspaceCredentials'
    PlusVibeWorkspaceCreateRequest:
      title: PlusVibe
      type: object
      additionalProperties: false
      required:
        - provider
        - credentials
      properties:
        provider:
          type: string
          enum:
            - plusvibe
        credentials:
          $ref: '#/components/schemas/PlusVibeSetupCredentials'
        customer:
          $ref: '#/components/schemas/WorkspaceCustomerTracking'
    WorkspaceCreateRequest:
      oneOf:
        - $ref: '#/components/schemas/EmailBisonWorkspaceCreateRequest'
        - $ref: '#/components/schemas/InstantlyWorkspaceCreateRequest'
        - $ref: '#/components/schemas/SmartleadWorkspaceCreateRequest'
        - $ref: '#/components/schemas/PlusVibeWorkspaceCreateRequest'
      discriminator:
        propertyName: provider
        mapping:
          emailbison: '#/components/schemas/EmailBisonWorkspaceCreateRequest'
          instantly: '#/components/schemas/InstantlyWorkspaceCreateRequest'
          smartlead: '#/components/schemas/SmartleadWorkspaceCreateRequest'
          plusvibe: '#/components/schemas/PlusVibeWorkspaceCreateRequest'
    WorkspacePatchRequest:
      description: >-
        Rotate provider credentials. EmailBison, Instantly, and PlusVibe can
        also update or clear customer tracking. Smartlead can update its label.
      oneOf:
        - $ref: '#/components/schemas/EmailBisonWorkspacePatchRequest'
        - $ref: '#/components/schemas/InstantlyWorkspacePatchRequest'
        - $ref: '#/components/schemas/SmartleadWorkspacePatchRequest'
        - $ref: '#/components/schemas/PlusVibeWorkspacePatchRequest'
      discriminator:
        propertyName: provider
        mapping:
          emailbison: '#/components/schemas/EmailBisonWorkspacePatchRequest'
          instantly: '#/components/schemas/InstantlyWorkspacePatchRequest'
          smartlead: '#/components/schemas/SmartleadWorkspacePatchRequest'
          plusvibe: '#/components/schemas/PlusVibeWorkspacePatchRequest'
    Workspace:
      type: object
      required:
        - workspace_id
        - name
        - provider
        - provider_workspace_id
        - status
        - created_at
        - updated_at
      properties:
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker workspace target ID (`wrk_…`) for orders and pool attach.
          example: wrk_01HZX0WK1A2B3C4D5E6F7G8H
        name:
          type: string
          example: Acme PlusVibe
        provider:
          type: string
          enum:
            - emailbison
            - instantly
            - smartlead
            - plusvibe
          example: plusvibe
        provider_workspace_id:
          type: string
          description: Remote workspace ID from the provider (not Peeker’s `wrk_…`).
          example: 692577ed8543f416f83ad252
        client_id:
          type: string
          pattern: ^[1-9][0-9]*$
          description: Present when this target routes to a Smartlead client.
          example: '366903'
        label:
          type: string
          description: Optional label owned by this workspace target. Omitted when no label was supplied.
          example: Agency Smartlead
        customer:
          $ref: '#/components/schemas/WorkspaceCustomer'
        status:
          type: string
          enum:
            - active
            - disconnected
          example: active
        created_at:
          type: string
          format: date-time
          example: 2026-07-09T12:00:00Z
        updated_at:
          type: string
          format: date-time
          example: 2026-07-09T12:00:00Z
    EmailBisonDestination:
      title: Existing EmailBison workspace
      type: object
      additionalProperties: false
      required:
        - provider
        - workspace_id
      properties:
        provider:
          type: string
          enum:
            - emailbison
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker workspace ID (`wrk_…`).
          example: wrk_01HZX0EB1A2B3C4D5E6F7G8H
    InstantlyDestination:
      title: Existing Instantly workspace
      type: object
      additionalProperties: false
      required:
        - provider
        - workspace_id
      properties:
        provider:
          type: string
          enum:
            - instantly
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker workspace ID (`wrk_…`).
          example: wrk_01HZX0JN1A2B3C4D5E6F7G8H
    SmartleadDestination:
      title: Existing Smartlead workspace
      type: object
      additionalProperties: false
      required:
        - provider
        - workspace_id
      properties:
        provider:
          type: string
          enum:
            - smartlead
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker ID for the connected Smartlead main workspace.
          example: wrk_01HZX0SK1A2B3C4D5E6F7G8H
        client_id:
          type: string
          pattern: ^[1-9][0-9]*$
          maxLength: 64
          description: Optional Smartlead client ID. Omit it for the main workspace.
          example: '366903'
    PlusVibeDestination:
      title: Existing PlusVibe workspace
      type: object
      additionalProperties: false
      required:
        - provider
        - workspace_id
      properties:
        provider:
          type: string
          enum:
            - plusvibe
        workspace_id:
          type: string
          pattern: ^wrk_
          description: |
            Peeker workspace ID (`wrk_…`), not PlusVibe’s remote ID.
          example: wrk_01HZX0PV1A2B3C4D5E6F7G8H
    Destination:
      description: |
        Provider workspace target that received or will receive the mailboxes.
        The JSON field name is `destination`.
      oneOf:
        - $ref: '#/components/schemas/EmailBisonDestination'
        - $ref: '#/components/schemas/InstantlyDestination'
        - $ref: '#/components/schemas/SmartleadDestination'
        - $ref: '#/components/schemas/PlusVibeDestination'
      discriminator:
        propertyName: provider
        mapping:
          emailbison: '#/components/schemas/EmailBisonDestination'
          instantly: '#/components/schemas/InstantlyDestination'
          smartlead: '#/components/schemas/SmartleadDestination'
          plusvibe: '#/components/schemas/PlusVibeDestination'
    EmailBisonOrderDestination:
      type: object
      additionalProperties: false
      required: [provider, workspace_id]
      properties:
        provider: { type: string, enum: [emailbison] }
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker workspace ID returned by `POST /workspaces`.
    InstantlyOrderDestination:
      type: object
      additionalProperties: false
      required: [provider, workspace_id]
      properties:
        provider: { type: string, enum: [instantly] }
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker workspace ID returned by `POST /workspaces`.
        login_credentials:
          allOf:
            - $ref: '#/components/schemas/ProviderLoginCredentials'
          description: Optional Instantly login used for this order and saved for later orders.
    SmartleadOrderDestination:
      type: object
      additionalProperties: false
      required: [provider, workspace_id]
      properties:
        provider: { type: string, enum: [smartlead] }
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker ID for the connected Smartlead main workspace.
        client_id:
          type: string
          pattern: ^[1-9][0-9]*$
          maxLength: 64
          description: Optional Smartlead client ID. Omit it for the main workspace.
          example: '366903'
    PlusVibeOrderDestination:
      type: object
      additionalProperties: false
      required: [provider, workspace_id]
      properties:
        provider: { type: string, enum: [plusvibe] }
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker workspace ID returned by `POST /workspaces`.
    OrderDestination:
      oneOf:
        - $ref: '#/components/schemas/EmailBisonOrderDestination'
        - $ref: '#/components/schemas/InstantlyOrderDestination'
        - $ref: '#/components/schemas/SmartleadOrderDestination'
        - $ref: '#/components/schemas/PlusVibeOrderDestination'
      discriminator:
        propertyName: provider
    DestinationResource:
      type: object
      required:
        - provider
        - workspace_id
        - name
      properties:
        provider:
          type: string
          enum:
            - emailbison
            - instantly
            - smartlead
            - plusvibe
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker workspace ID (`wrk_…`).
          example: wrk_01HZX0SK1A2B3C4D5E6F7G8H
        client_id:
          type: string
          pattern: ^[1-9][0-9]*$
          description: Smartlead client ID (Smartlead only).
          example: '366903'
        name:
          type: string
          description: Display name from the provider.
          example: Acme Client
    DomainInput:
      oneOf:
        - type: string
          minLength: 1
          maxLength: 253
          example: acme-mail.com
        - type: object
          additionalProperties: false
          required:
            - domain
          properties:
            domain:
              type: string
              minLength: 1
              maxLength: 253
              example: shop-acme.com
            forwarding_url:
              type: string
              format: uri
              maxLength: 2048
              description: Optional site URL for this domain.
              example: https://shop.acme.com
    Persona:
      type: object
      additionalProperties: false
      required:
        - first_name
        - last_name
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 128
          example: Alex
        last_name:
          type: string
          minLength: 1
          maxLength: 128
          example: Rivera
        usernames:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          description: |
            Optional mailbox names before the `@`. Peeker keeps these values
            and generates any missing Google seats from the persona name.
          items:
            type: string
            minLength: 1
            maxLength: 64
            pattern: ^[a-zA-Z0-9._-]+$
          example: [alex, alex.rivera]
        profile_picture_url:
          type: string
          format: uri
          description: Optional public image URL. Private/local URLs are rejected.
          maxLength: 2048
          example: https://cdn.acme.com/alex.jpg
    OrderInputFields:
      type: object
      additionalProperties: false
      required:
        - domains
        - personas
      properties:
        bundle_id:
          type: string
          pattern: ^bun_
          description: Saved bundle ID. Don’t also send custom license fields.
          example: bun_01HZX0BK1A2B3C4D5E6F7G8H
        quantity:
          type: integer
          minimum: 1
          maximum: 100
          default: 1
          description: Number of bundle copies to order. Valid only with `bundle_id`.
          example: 2
        google_licenses:
          type: integer
          enum: [1, 2, 3]
          description: Google inboxes created on every submitted Google domain.
          example: 2
        microsoft_licenses:
          type: integer
          minimum: 0
          maximum: 5000
          description: How many Microsoft licenses (1 license = 1 domain). Custom orders only.
          example: 2
        microsoft_inboxes_per_domain:
          type: integer
          enum:
            - 25
            - 50
            - 75
            - 99
          description: Inboxes per Microsoft domain. Required with `microsoft_licenses`.
          example: 50
        forwarding_url:
          type: string
          format: uri
          maxLength: 2048
          description: Default site URL when a domain doesn’t set its own.
          example: https://acme.com
        domains:
          type: array
          minItems: 1
          maxItems: 5000
          items:
            $ref: '#/components/schemas/DomainInput'
        personas:
          type: array
          minItems: 1
          maxItems: 10
          items:
            $ref: '#/components/schemas/Persona'
      oneOf:
        - title: Saved bundle sizing
          required:
            - bundle_id
          not:
            anyOf:
              - required: [google_licenses]
              - required: [microsoft_licenses]
              - required: [microsoft_inboxes_per_domain]
        - title: Custom sizing
          anyOf:
            - title: Google licenses
              required:
                - google_licenses
              properties:
                google_licenses:
                  type: integer
                  minimum: 1
                  maximum: 3
            - title: Microsoft licenses
              required:
                - microsoft_licenses
              properties:
                microsoft_licenses:
                  type: integer
                  minimum: 1
          not:
            anyOf:
              - required:
                  - bundle_id
              - required:
                  - quantity
          allOf:
            - if:
                required:
                  - microsoft_licenses
                properties:
                  microsoft_licenses:
                    type: integer
                    minimum: 1
              then:
                required:
                  - microsoft_inboxes_per_domain
                properties:
                  microsoft_inboxes_per_domain:
                    type: integer
              else:
                not:
                  required:
                    - microsoft_inboxes_per_domain
    StrictOrderRequest:
      type: object
      additionalProperties: false
      required:
        - destination
        - order
      properties:
        destination:
          $ref: '#/components/schemas/OrderDestination'
        order:
          $ref: '#/components/schemas/OrderInputFields'
    WorkspaceEnvelope:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/Workspace'
      example:
        data:
          workspace_id: wrk_01HZX0PV1A2B3C4D5E6F7G8H
          name: Acme PlusVibe
          provider: plusvibe
          provider_workspace_id: 692577ed8543f416f83ad252
          status: active
          created_at: 2026-07-09T12:00:00Z
          updated_at: 2026-07-09T12:00:00Z
    WorkspaceListEnvelope:
      type: object
      additionalProperties: false
      required:
        - data
        - links
        - meta
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Workspace'
        links:
          $ref: '#/components/schemas/ListLinks'
        meta:
          $ref: '#/components/schemas/ListMeta'
    StrictOrderResult:
      type: object
      additionalProperties: false
      required:
        - order
        - workspace_id
        - destination
        - created
      properties:
        order:
          $ref: '#/components/schemas/OrderReceipt'
        customer_id:
          type: string
          pattern: ^cus_
          description: Present when the workspace target tracks a customer.
        workspace_id:
          type: string
          pattern: ^wrk_
          description: Peeker workspace target ID.
        destination:
          $ref: '#/components/schemas/DestinationResource'
        created:
          type: object
          additionalProperties: false
          required:
            - order
          properties:
            order:
              type: boolean
              description: '`true` if this request created the order (false on an idempotent replay).'
              example: true
    OrderEnvelope:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/StrictOrderResult'
      example:
        data:
          order:
            order_id: ord_01HZX0QR1A2B3C4D5E6F7G8H
            status: in_progress
            domain_count: 1
            costs:
              total_cents: 3000
              currency: usd
              line_items:
                - type: domain_registration
                  quantity: 1
                  amount_cents: 1300
                - type: microsoft_license
                  quantity: 1
                  amount_cents: 1700
            renews_at: null
            created_at: 2026-07-09T12:10:00Z
          customer_id: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
          workspace_id: wrk_01HZX0PV1A2B3C4D5E6F7G8H
          destination:
            provider: plusvibe
            workspace_id: wrk_01HZX0PV1A2B3C4D5E6F7G8H
            name: Acme PlusVibe
          created:
            order: true
    EmailBisonWorkspacePatchRequest:
      title: EmailBison
      type: object
      additionalProperties: false
      minProperties: 2
      required:
        - provider
      properties:
        provider:
          type: string
          enum:
            - emailbison
        credentials:
          $ref: '#/components/schemas/EmailBisonCredentials'
        customer:
          oneOf:
            - $ref: '#/components/schemas/WorkspaceCustomerTracking'
            - type: 'null'
          description: Set tracking data, or send `null` to clear the workspace customer link.
    InstantlyWorkspacePatchRequest:
      title: Instantly
      type: object
      additionalProperties: false
      minProperties: 2
      required:
        - provider
      properties:
        provider:
          type: string
          enum:
            - instantly
        credentials:
          $ref: '#/components/schemas/InstantlyCredentials'
        customer:
          oneOf:
            - $ref: '#/components/schemas/WorkspaceCustomerTracking'
            - type: 'null'
          description: Set tracking data, or send `null` to clear the workspace customer link.
    SmartleadWorkspacePatchRequest:
      title: Smartlead
      type: object
      additionalProperties: false
      minProperties: 2
      required:
        - provider
      properties:
        provider:
          type: string
          enum:
            - smartlead
        label:
          type: string
          minLength: 1
          maxLength: 256
          description: New label for this Smartlead connection.
        credentials:
          $ref: '#/components/schemas/SmartleadCredentials'
    PlusVibeWorkspacePatchRequest:
      title: PlusVibe
      type: object
      additionalProperties: false
      minProperties: 2
      required:
        - provider
      properties:
        provider:
          type: string
          enum:
            - plusvibe
        credentials:
          $ref: '#/components/schemas/PlusVibeCredentials'
        customer:
          oneOf:
            - $ref: '#/components/schemas/WorkspaceCustomerTracking'
            - type: 'null'
          description: Set tracking data, or send `null` to clear the workspace customer link.
    Bundle:
      type: object
      required:
        - id
        - name
        - monthly_sending_volume
        - send_on_weekends
        - google_licenses
        - status
        - effective
        - required_domains
        - created_at
      properties:
        id:
          type: string
          example: bun_01HZX0BU1A2B3C4D5E6F7G8H
        name:
          type: string
          example: Starter 25k emails/month
        monthly_sending_volume:
          type: integer
          example: 25000
        google_percent:
          type: integer
          nullable: true
          description: Null when using Peeker’s recommended split. If set, both
            percents must total 100.
          example: 50
        microsoft_percent:
          type: integer
          nullable: true
          example: 50
        send_on_weekends:
          type: boolean
          description: '`false` = 22 weekdays. `true` = 30 days including weekends.'
          default: false
          example: false
        google_licenses:
          type: integer
          enum: [1, 2, 3]
          description: Google inboxes created on each Google domain.
          default: 2
          example: 2
        status:
          type: string
          enum: [active, archived]
          example: active
        effective:
          type: object
          required: [google_percent, microsoft_percent]
          properties:
            google_percent:
              type: integer
              example: 50
            microsoft_percent:
              type: integer
              example: 50
        required_domains:
          type: object
          description: How many available domains an order using this bundle must submit.
          required: [google, microsoft, total]
          properties:
            google:
              type: integer
              example: 15
            microsoft:
              type: integer
              example: 4
            total:
              type: integer
              example: 19
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'

    DomainListItem:
      type: object
      required: [id, domain, category, status, created_at]
      properties:
        id:
          type: string
          example: dom_01HZX0D01A2B3C4D5E6F7G8H
        domain:
          type: string
          example: acme-mail.com
        category:
          type: string
          enum: [inventory, connected]
          example: connected
        status:
          type: string
          enum: [available, in_progress, active, failed, action_required]
          example: active
        provider:
          type: string
          nullable: true
          enum: [google, microsoft, mixed, null]
          description: Only present on `connected` rows. `inventory` rows MUST omit this
            field.
          example: google
        customer_id:
          type: string
          nullable: true
          pattern: ^cus_
          example: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
        order_id:
          type: string
          nullable: true
          example: ord_01HZX0OR1A2B3C4D5E6F7G8H
        usable_for:
          type: array
          items:
            type: string
            enum: [google, microsoft]
          example: [google, microsoft]
        forwarding_url:
          type: string
          nullable: true
          example: https://acme.com
        smartlead_client_id:
          type: string
          nullable: true
          description: Smartlead client ID when the domain is on a Smartlead workspace target; otherwise `null`.
          example: '301'
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'

    TldPricing:
      type: object
      required: [tld, price_cents, renewal_price_cents, currency]
      properties:
        tld:
          type: string
          description: Normalized TLD key from the live `pricingTlds` catalog.
          example: .com
        price_cents:
          type: integer
          description: First-year registration cost in the listed currency.
          example: 1300
        renewal_price_cents:
          type: integer
          description: Yearly renewal cost in the listed currency.
          example: 1500
        currency:
          type: string
          example: usd

    AvailabilityRow:
      type: object
      required: [domain, available]
      properties:
        domain:
          type: string
          example: acme-mail.com
        available:
          type: boolean
          example: true
        price_cents:
          type: integer
          description: 'Registration cost in cents using your partner default currency.
            Present on every `available: true` row. If Peeker does not support
            the TLD, the row returns `available: false` with `reason:
            unsupported_tld`.'
          example: 1300
        renewal_price_cents:
          type: integer
          description: 'Renewal cost in cents for the next yearly renewal. Present
            on every `available: true` row.'
          example: 1500
        currency:
          type: string
          description: 'Present on every `available: true` row.'
          example: usd
        usable_for:
          type: array
          items:
            type: string
            enum: [google, microsoft]
          description: 'Mailbox providers the domain can take an order for,
            from a real Google/Microsoft tenant check (cached up to 35 days).
            Returned on any row: including `available: false` ones: when the
            check answered; omitted when it could not.'
          example: [google, microsoft]
        premium:
          type: boolean
          description: 'Present and `true` only when the domain is sold at a premium
            tier by the registry. Omitted otherwise.'
          example: true
        reason:
          type: string
          enum: [unsupported_tld, premium, stale, provider_error, unknown]
          description: 'Present on `available: false` rows when Peeker can attribute
            a reason: `unsupported_tld` (Peeker does not register the TLD),
            `premium` (registry sells the domain at a premium tier), `stale` /
            `provider_error` / `unknown` (lookup did not return a fresh
            registrability answer). When a domain is simply already registered
            to someone else, the row returns `available: false` with no
            `reason`.'
          example: unsupported_tld

    DomainImportJob:
      type: object
      required:
        [
          id,
          status,
          submitted_count,
          completed_count,
          failed_count,
          nameserver_groups,
          failed,
          created_at
        ]
      properties:
        id:
          type: string
          example: imp_01HZX0IM1A2B3C4D5E6F7G8H
        status:
          type: string
          enum: [in_progress, completed, failed, action_required]
          example: in_progress
        submitted_count:
          type: integer
          example: 2
        completed_count:
          type: integer
          example: 0
        failed_count:
          type: integer
          example: 0
        nameserver_groups:
          type: array
          items:
            type: object
            required: [nameserver_one, nameserver_two, domains]
            properties:
              nameserver_one:
                type: string
                example: helena.ns.cloudflare.com
              nameserver_two:
                type: string
                example: idris.ns.cloudflare.com
              domains:
                type: array
                items:
                  type: string
                example: [acme-mail.com, team-acme.com]
        failed:
          type: array
          items:
            type: object
            required: [domain, reason]
            properties:
              domain:
                type: string
                example: bad-acme.com
              reason:
                type: string
                example: cloudflare_import_failed
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'
        completed_at:
          type: string
          format: date-time
          example: '2026-05-08T12:05:00Z'

    DomainImportJobListItem:
      type: object
      required:
        [id, status, submitted_count, completed_count, failed_count, nameserver_groups, created_at]
      properties:
        id:
          type: string
          example: imp_01HZX0IM1A2B3C4D5E6F7G8H
        status:
          type: string
          enum: [in_progress, completed, failed, action_required]
          example: completed
        submitted_count:
          type: integer
          example: 25
        completed_count:
          type: integer
          example: 25
        failed_count:
          type: integer
          example: 0
        nameserver_groups:
          type: array
          items:
            type: object
            required: [nameserver_one, nameserver_two, domains]
            properties:
              nameserver_one:
                type: string
                example: helena.ns.cloudflare.com
              nameserver_two:
                type: string
                example: idris.ns.cloudflare.com
              domains:
                type: array
                items:
                  type: string
                example: [acme-mail.com, team-acme.com]
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'
        completed_at:
          type: string
          format: date-time
          example: '2026-05-08T12:05:00Z'

    DomainImportLookup:
      type: object
      required: [domains]
      properties:
        domains:
          type: array
          items:
            type: object
            required: [domain, status, import_job_id, nameserver_one, nameserver_two]
            properties:
              domain:
                type: string
                example: acme-mail.com
              status:
                type: string
                enum: [in_progress, completed, failed, action_required, not_found]
                example: completed
              import_job_id:
                type: string
                nullable: true
                example: imp_01HZX0IM1A2B3C4D5E6F7G8H
              nameserver_one:
                type: string
                nullable: true
                example: helena.ns.cloudflare.com
              nameserver_two:
                type: string
                nullable: true
                example: idris.ns.cloudflare.com
        nameserver_groups:
          type: array
          description: Present only when `include_group=true`.
          items:
            type: object
            required: [import_job_id, nameserver_one, nameserver_two, domains]
            properties:
              import_job_id:
                type: string
                example: imp_01HZX0IM1A2B3C4D5E6F7G8H
              nameserver_one:
                type: string
                example: helena.ns.cloudflare.com
              nameserver_two:
                type: string
                example: idris.ns.cloudflare.com
              domains:
                type: array
                items:
                  type: string
                example: [acme-mail.com, team-acme.com]

    DomainForwardingJob:
      type: object
      required:
        - id
        - status
        - completed_count
        - failed_count
        - forwarding_url
        - completed
        - failed
        - created_at
      properties:
        id:
          type: string
          example: fwj_01HZX0FW1A2B3C4D5E6F7G8H
        status:
          type: string
          enum: [in_progress, completed, failed, partial_success]
          example: completed
        completed_count:
          type: integer
          example: 3
        failed_count:
          type: integer
          example: 0
        forwarding_url:
          type: string
          example: https://acme.com
        completed:
          type: array
          items:
            type: string
          example: [acme-mail.com, team-acme.com, news-acme.com]
        failed:
          type: array
          items:
            type: object
            required: [domain, reason]
            properties:
              domain:
                type: string
                example: bad-acme.com
              reason:
                type: string
                enum: [domain_not_found, not_connected_to_cloudflare, forwarding_update_failed]
                example: not_connected_to_cloudflare
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'
        completed_at:
          type: string
          format: date-time
          example: '2026-05-08T12:01:30Z'

    BurnedReplaceJob:
      type: object
      required:
        - id
        - status
        - submitted_count
        - completed_count
        - failed_count
        - results_returned_count
        - results_limit
        - results_truncated
        - completed
        - failed
        - created_at
      properties:
        id:
          type: string
          example: brj_01HZX0BR1A2B3C4D5E6F7G8H
        status:
          type: string
          enum: [in_progress, completed, failed, partial_success]
          example: completed
        submitted_count:
          type: integer
          example: 2
        completed_count:
          type: integer
          example: 2
        failed_count:
          type: integer
          example: 0
        results_returned_count:
          type: integer
          description: Number of rows embedded across `completed` and `failed` in this response.
          example: 2
        results_limit:
          type: integer
          description: Maximum combined number of rows embedded across `completed` and `failed`.
          example: 5000
        results_truncated:
          type: boolean
          description: >-
            `true` if more results exist than the embedded arrays show. Page
            `/results` for the full set.
          example: false
        completed:
          type: array
          description: >-
            Preview of created swaps. `completed` and `failed` share the combined
            `results_limit` (counts stay exact). Each swap also emits
            `swap.created`.
          items:
            type: object
            required: [domain, replacement_domain, swap_id]
            properties:
              domain:
                type: string
                example: acme-mail.com
              replacement_domain:
                type: string
                example: acme-fresh.com
              swap_id:
                type: string
                example: swp_01HZX0SW1A2B3C4D5E6F7G8H
        failed:
          type: array
          description: >-
            Preview of failed rows. `completed` and `failed` share the combined
            `results_limit`. Page `/results` for every failure.
          items:
            type: object
            required: [domain, replacement_domain, reason]
            properties:
              domain:
                type: string
                example: bad-acme.com
              replacement_domain:
                type: string
                example: acme-fresh-2.com
              reason:
                type: string
                example: This domain already has a swap in progress.
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'
        completed_at:
          type: string
          format: date-time
          example: '2026-05-08T12:01:30Z'

    BurnedReplaceResult:
      oneOf:
        - title: Pending replacement
          type: object
          additionalProperties: false
          required: [status, domain, replacement_domain]
          properties:
            status:
              type: string
              enum: [pending]
            domain:
              type: string
              example: acme-mail.com
            replacement_domain:
              type: string
              example: acme-fresh.com
        - title: Completed replacement
          type: object
          additionalProperties: false
          required: [status, domain, replacement_domain, swap_id]
          properties:
            status:
              type: string
              enum: [completed]
            domain:
              type: string
              example: acme-mail.com
            replacement_domain:
              type: string
              example: acme-fresh.com
            swap_id:
              type: string
              example: swp_01HZX0SW1A2B3C4D5E6F7G8H
        - title: Failed replacement
          type: object
          additionalProperties: false
          required: [status, domain, replacement_domain, reason]
          properties:
            status:
              type: string
              enum: [failed]
            domain:
              type: string
              example: bad-acme.com
            replacement_domain:
              type: string
              example: acme-fresh-2.com
            reason:
              type: string
              example: This domain already has a swap in progress.
      discriminator:
        propertyName: status

    OrderLineItem:
      type: object
      required: [type, quantity, amount_cents]
      properties:
        type:
          type: string
          enum: [domain_registration, google_license, microsoft_license]
          example: google_license
        quantity:
          type: integer
          example: 4
        amount_cents:
          type: integer
          example: 10000

    PendingAction:
      type: object
      required: [id, type, reason, status, created_at]
      properties:
        id:
          type: string
          example: act_01HZX0AC1A2B3C4D5E6F7G8H
        type:
          type: string
          enum:
            - google_profile_picture
            - nameservers
            - domain_replacement
          example: google_profile_picture
        reason:
          type: string
          enum:
            - update_nameservers
            - replace_domain
            - profile_picture_not_accessible
          example: profile_picture_not_accessible
        status:
          type: string
          enum: [open, resolved]
          example: open
        domain_id:
          type: string
          nullable: true
          example: dom_01HZX0D01A2B3C4D5E6F7G8H
        domain:
          type: string
          nullable: true
          example: acme-mail.com
        user:
          type: object
          nullable: true
          properties:
            first_name: { type: string, example: Alex }
            last_name: { type: string, example: Rivera }
            profile_picture_url:
              { type: string, nullable: true, example: 'https://example.com/alex.jpg' }
        message:
          type: string
          nullable: true
          example: The profile picture URL must be a publicly accessible image URL.
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'

    OrderSummary:
      type: object
      required:
        [id, customer_id, smartlead_client_id, status, domain_count, costs, renews_at, created_at]
      properties:
        id:
          type: string
          example: ord_01HZX0OR1A2B3C4D5E6F7G8H
        customer_id:
          type: string
          nullable: true
          pattern: ^cus_
          example: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
        bundle_id:
          type: string
          nullable: true
          example: bun_01HZX0BU1A2B3C4D5E6F7G8H
        smartlead_client_id:
          type: string
          nullable: true
          description: Smartlead client ID for this order, or `null`.
          example: '301'
        destination:
          $ref: '#/components/schemas/Destination'
          description: Workspace target used for this order (may be missing on very old orders).
        status:
          type: string
          enum:
            - in_progress
            - completed
            - failed
            - action_required
            - cancel_scheduled
            - cancelled
          example: in_progress
        domain_count:
          type: integer
          example: 2
        costs:
          type: object
          required: [total_cents, currency]
          properties:
            total_cents:
              type: integer
              example: 6800
            currency:
              type: string
              example: usd
        renews_at:
          type: string
          nullable: true
          format: date-time
          description: Next renewal date for the order. `null` until billing is created.
          example: '2026-06-08T12:00:00Z'
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'

    Order:
      allOf:
        - $ref: '#/components/schemas/OrderSummary'
        - type: object
          required: [domains, personas]
          properties:
            costs:
              type: object
              required: [total_cents, currency, line_items]
              properties:
                total_cents:
                  type: integer
                  example: 36800
                currency:
                  type: string
                  example: usd
                line_items:
                  type: array
                  items:
                    $ref: '#/components/schemas/OrderLineItem'
            domains:
              type: object
              required: [registered, imported]
              properties:
                registered:
                  type: array
                  items:
                    type: string
                  example: [domain-01.com, domain-02.com]
                imported:
                  type: array
                  items:
                    type: string
                  example: [team-acme.com]
            personas:
              type: array
              items:
                type: object
                required: [first_name, last_name, profile_picture_url]
                properties:
                  first_name: { type: string, example: Sam }
                  last_name: { type: string, example: Lee }
                  profile_picture_url:
                    { type: string, nullable: true, example: 'https://example.com/sam.jpg' }

    OrderCancellation:
      type: object
      required: [id, status, cancellation_scheduled_at, cancels_at, domains]
      properties:
        id:
          type: string
          example: ord_01HZX0OR1A2B3C4D5E6F7G8H
        status:
          type: string
          enum: [cancel_scheduled]
          example: cancel_scheduled
        cancellation_scheduled_at:
          type: string
          format: date-time
          description: When Peeker accepted the cancellation request.
          example: '2026-05-08T12:00:00Z'
        cancels_at:
          type: string
          format: date-time
          description: When the cancellation is scheduled to finalize.
          example: '2026-06-07T12:00:00Z'
        domains:
          type: array
          description: Domains scheduled for cancellation.
          items:
            type: object
            required: [id, domain]
            properties:
              id:
                type: string
                example: dom_01HZX0D01A2B3C4D5E6F7G8H
              domain:
                type: string
                example: acme-mail.com

    Swap:
      type: object
      required: [id, type, domain_id, status, created_at]
      properties:
        id:
          type: string
          example: swp_01HZX0SW1A2B3C4D5E6F7G8H
        type:
          type: string
          enum: [standard, premium, user_names]
          example: premium
        domain_id:
          type: string
          example: dom_01HZX0D01A2B3C4D5E6F7G8H
        replacement_domain:
          type: string
          nullable: true
          example: acme-fresh.com
        status:
          type: string
          enum: [created, in_progress, completed, failed, action_required]
          example: in_progress
        created_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'

    OrderReceipt:
      type: object
      additionalProperties: false
      required:
        - order_id
        - status
        - domain_count
        - costs
        - renews_at
        - created_at
      properties:
        order_id:
          type: string
          pattern: ^ord_
          example: ord_01HZX0QR1A2B3C4D5E6F7G8H
        status:
          type: string
          enum:
            - in_progress
            - completed
            - failed
            - action_required
            - cancel_scheduled
            - cancelled
          example: in_progress
        domain_count:
          type: integer
          minimum: 0
          example: 1
        costs:
          type: object
          additionalProperties: false
          required:
            - total_cents
            - currency
            - line_items
          properties:
            total_cents:
              type: integer
              minimum: 0
            currency:
              type: string
              enum:
                - usd
              example: usd
            line_items:
              type: array
              items:
                $ref: '#/components/schemas/OrderLineItem'
        renews_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
          example: 2026-07-09T12:10:00Z
    BurnedDomain:
      type: object
      required: [id, domain, provider, managed, customer_id, updated_at]
      properties:
        id:
          type: string
          example: dom_01HZX0D01A2B3C4D5E6F7G8H
        domain:
          type: string
          example: acme-mail.com
        provider:
          type: string
          nullable: true
          enum: [google, microsoft, mixed]
          example: microsoft
        managed:
          type: boolean
          description: >-
            `true` if this burned domain is on an active Peeker subscription
            (replaceable).
          example: true
        customer_id:
          type: string
          nullable: true
          pattern: ^cus_
          example: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
        updated_at:
          type: string
          format: date-time
          example: '2026-05-08T12:00:00Z'

  headers:
    PeekerRequestId:
      description: Stable request identifier. Log it with the status and response body.
      schema:
        type: string
        example: 550e8400-e29b-41d4-a716-446655440000
paths:
  /me:
    get:
      tags: [Account]
      operationId: authenticate
      summary: Check your API key
      x-mint:
        metadata:
          sidebarTitle: Check your API key
      description: |
        Confirms the key works and returns live vs sandbox, the permission
        preset, and your partner profile. Call this first when wiring up.
      responses:
        '200':
          description: API key is valid.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Me'
              examples:
                live:
                  summary: Live key with full access
                  value:
                    data:
                      api_key:
                        environment: live
                        permission_preset: full
                      partner:
                        id: org_01HZX0OG1A2B3C4D5E6F7G8H
                        name: Acme Resellers
                sandbox:
                  summary: Test key (sandbox environment)
                  value:
                    data:
                      api_key:
                        environment: test
                        permission_preset: full
                      partner:
                        id: org_01HZX0OG1A2B3C4D5E6F7G8H
                        name: Acme Resellers (sandbox)
        '401':
          description: API key is missing, malformed, or revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                missing:
                  summary: No Authorization header
                  value:
                    error:
                      code: unauthorized
                      message: Authorization header is required (Bearer <api_key>)
                      request_id: 550e8400-e29b-41d4-a716-446655440000
                invalid:
                  summary: Key not found
                  value:
                    error:
                      code: unauthorized
                      message: Invalid API key
                      request_id: 550e8400-e29b-41d4-a716-446655440000
                revoked:
                  summary: Key was revoked
                  value:
                    error:
                      code: unauthorized
                      message: API key has been revoked
                      request_id: 550e8400-e29b-41d4-a716-446655440000

        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /bundles:
    get:
      tags: [Bundles]
      operationId: listBundles
      summary: List bundles
      x-mint:
        metadata:
          sidebarTitle: List bundles
      description: Returns saved order templates. Filter by `active` or `archived`.
      parameters:
        - in: query
          name: status
          description: '`active` or `archived`.'
          schema: { type: string, enum: [active, archived] }
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          $ref: '#/components/responses/BundleListResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [Bundles]
      operationId: createBundle
      summary: Create a bundle
      x-mint:
        metadata:
          sidebarTitle: Create a bundle
      description: |
        Saves an order template by monthly send volume. Optionally set a
        Google/Microsoft split, or omit it to use Peeker’s recommendation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, monthly_sending_volume]
              properties:
                name:
                  type: string
                  description: Name you will recognize later (for example, "Starter 25k/month").
                monthly_sending_volume:
                  type: integer
                  description: Monthly email volume this bundle is sized for.
                google_percent:
                  type: integer
                  nullable: true
                  description: Google share of volume (0-100). If set, also set
                    `microsoft_percent` (must total 100). Omit both to use Peeker’s
                    recommended split.
                microsoft_percent:
                  type: integer
                  nullable: true
                send_on_weekends:
                  type: boolean
                  default: false
                  description: If `true`, spread volume across 30 days (includes weekends).
                    Default `false` uses 22 weekdays.
                google_licenses:
                  type: integer
                  enum: [1, 2, 3]
                  default: 2
                  description: Google inboxes per domain. Each inbox sends 20 emails/day.
            examples:
              recommended_split:
                summary: Inherit Peeker's recommended split
                value:
                  name: Starter 25k emails/month
                  monthly_sending_volume: 25000
                  send_on_weekends: false
                  google_licenses: 2
              custom_split:
                summary: Custom Google/Microsoft split
                value:
                  name: Google-heavy 50k
                  monthly_sending_volume: 50000
                  google_percent: 70
                  microsoft_percent: 30
                  send_on_weekends: false
                  google_licenses: 3
      responses:
        '201':
          $ref: '#/components/responses/BundleResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /bundles/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: The bundle ID, e.g. `bun_01HZX`.
        schema: { type: string }
    get:
      tags: [Bundles]
      operationId: getBundle
      summary: Get a bundle
      x-mint:
        metadata:
          sidebarTitle: Get a bundle
      description: Returns one saved order template.
      responses:
        '200':
          $ref: '#/components/responses/BundleResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags: [Bundles]
      operationId: updateBundle
      summary: Update a bundle
      x-mint:
        metadata:
          sidebarTitle: Update a bundle
      description: Updates fields on a saved order template.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                monthly_sending_volume: { type: integer }
                google_percent:
                  type: integer
                  nullable: true
                  description: Set with `microsoft_percent`, or send both as `null` to use
                    Peeker’s recommended split.
                microsoft_percent: { type: integer, nullable: true }
                send_on_weekends:
                  type: boolean
                  description: '`false` = weekdays only. `true` = include weekends.'
                google_licenses:
                  type: integer
                  enum: [1, 2, 3]
            examples:
              rename_and_resize:
                summary: Rename and resize
                value:
                  name: Starter 40k emails/month
                  monthly_sending_volume: 40000
                  send_on_weekends: false
              reset_split:
                summary: Use Peeker’s recommended split
                value:
                  google_percent: null
                  microsoft_percent: null
      responses:
        '200':
          $ref: '#/components/responses/BundleResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags: [Bundles]
      operationId: deleteBundle
      summary: Delete a bundle
      x-mint:
        metadata:
          sidebarTitle: Delete a bundle
      description: |
        Removes the bundle if no order ever used it. Otherwise archives it
        so old orders still link correctly.
      responses:
        '200':
          $ref: '#/components/responses/BundleDeleteResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains:
    get:
      tags: [Domains]
      operationId: listDomains
      summary: List domains
      x-mint:
        metadata:
          sidebarTitle: List domains
      description: |
        Returns domains in your account. `category=inventory` = not on an
        order yet. `category=connected` = already on an order.
      parameters:
        - in: query
          name: category
          description: '`inventory` = not on an order yet. `connected` = already on an order.'
          schema: { type: string, enum: [inventory, connected] }
        - in: query
          name: customer_id
          description: Show only domains owned by one customer.
          schema: { type: string, pattern: ^cus_, example: cus_01HZX0C6Z3K4M5N6P7Q8R9S0 }
        - in: query
          name: usable_for
          description: Filter by which mail provider the domain can serve.
          schema: { type: string, enum: [google, microsoft] }
        - in: query
          name: status
          description: Filter by status.
          schema: { type: string, enum: [available, in_progress, active, failed, action_required] }
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          $ref: '#/components/responses/DomainListResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: The domain ID, e.g. `dom_01HZX`.
        schema: { type: string }
    get:
      tags: [Domains]
      operationId: getDomain
      summary: Get a domain
      x-mint:
        metadata:
          sidebarTitle: Get a domain
      description: Returns one domain by ID.
      responses:
        '200':
          $ref: '#/components/responses/DomainResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/burned:
    get:
      tags: [Domains]
      operationId: listBurnedDomains
      summary: List burned domains
      x-mint:
        metadata:
          sidebarTitle: List burned domains
      description: |
        Returns domains marked burned. Use `filter=managed` for ones on an
        active Peeker subscription (the usual replace-all set). Domains with
        a swap already in flight are listed here but skipped by replace-all.
      parameters:
        - in: query
          name: filter
          description: >-
            `all` = every burned domain. `managed` = on an active Peeker
            subscription (usually what replace-all uses).
          schema: { type: string, enum: [all, managed], default: all }
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          description: A page of burned domains.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/BurnedDomain'
                      links:
                        $ref: '#/components/schemas/ListLinks'
                      meta:
                        $ref: '#/components/schemas/ListMeta'
              examples:
                typical_page:
                  summary: Managed and unmanaged burned domains
                  value:
                    data:
                      - id: dom_01HZX0D01A2B3C4D5E6F7G8H
                        domain: acme-mail.com
                        provider: microsoft
                        managed: true
                        customer_id: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
                        updated_at: '2026-05-08T12:00:00Z'
                      - id: dom_01HZX0D02A2B3C4D5E6F7G8H
                        domain: imported-acme.com
                        provider: google
                        managed: false
                        customer_id: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
                        updated_at: '2026-05-07T09:30:00Z'
                    links:
                      first: https://api.peeker.ai/partner/v1/domains/burned?filter=all&per_page=25
                      next: null
                    meta:
                      path: https://api.peeker.ai/partner/v1/domains/burned
                      per_page: 25
                      returned: 2
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/burned/replace:
    post:
      tags: [Domains]
      operationId: replaceBurnedDomains
      summary: Replace burned domains
      x-mint:
        content: |
          <Note>
          Only Peeker-bought domains can be replaced. Send
          `replacement_domains: []` first if you do not know the count. The
          `domain_count_mismatch` error lists every burned domain and how many
          replacements you need.
          </Note>
        metadata:
          sidebarTitle: Replace burned domains
      description: |
        Starts one job that swaps every replaceable burned domain.

        Send exactly one `replacement_domains` entry per burned domain
        (alphabetically paired), or send `[]` to learn the required count
        from a `domain_count_mismatch` error. One job runs at a time.

        Poll `GET /domains/burned/replace/{id}`, then page
        `.../results` for every row. Each swap also emits `swap.created`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [replacement_domains]
              properties:
                replacement_domains:
                  type: array
                  description: >-
                    One new domain per replaceable burned domain. Send an
                    empty array to discover the required count via the
                    `domain_count_mismatch` error.
                  items: { type: string }
            examples:
              two_replacements:
                summary: Replace two burned domains
                value:
                  replacement_domains: [acme-fresh.com, team-acme-2.com]
              discover_count:
                summary: Discover how many replacements are needed
                value:
                  replacement_domains: []
      responses:
        '202':
          description: Replacement job accepted; poll it for progress.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/BurnedReplaceJob'
              examples:
                accepted:
                  summary: Job accepted for two burned domains
                  value:
                    data:
                      id: brj_01HZX0BR1A2B3C4D5E6F7G8H
                      status: in_progress
                      submitted_count: 2
                      completed_count: 0
                      failed_count: 0
                      results_returned_count: 0
                      results_limit: 5000
                      results_truncated: false
                      completed: []
                      failed: []
                      created_at: '2026-05-08T12:00:00Z'
        '400':
          description: Wrong replacement count, or another replace job is already running.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                count_mismatch:
                  summary: Submit exactly the required number of domains
                  value:
                    error:
                      code: domain_count_mismatch
                      message: >-
                        You have 2 burned domains eligible for replacement.
                        Submit exactly 2 replacement domains (got 0).
                      field: replacement_domains
                      details:
                        required: 2
                        submitted: 0
                        burned_domains: [acme-mail.com, imported-acme.com]
                job_in_progress:
                  summary: A replace-all job is already running
                  value:
                    error:
                      code: invalid_request
                      message: >-
                        A burned-domain replacement job is already in progress.
                        Poll it until it finishes.
                      details:
                        job_id: brj_01HZX0BR1A2B3C4D5E6F7G8H
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/burned/replace/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: The replacement job ID, e.g. `brj_01HZX`.
        schema: { type: string, example: brj_01HZX }
    get:
      tags: [Domains]
      operationId: getBurnedReplaceJob
      summary: Get a replace job
      x-mint:
        metadata:
          sidebarTitle: Get a replace job
      description: |
        Returns job status, counts, and up to 5,000 preview result rows.
        If `results_truncated` is true, page the `/results` endpoint.
      responses:
        '200':
          $ref: '#/components/responses/BurnedReplaceJobResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/burned/replace/{id}/results:
    parameters:
      - in: path
        name: id
        required: true
        description: The replacement job ID, e.g. `brj_01HZX`.
        schema: { type: string, example: brj_01HZX }
    get:
      tags: [Domains]
      operationId: listBurnedReplaceResults
      summary: List replace results
      x-mint:
        metadata:
          sidebarTitle: List replace results
      description: |
        Pages every per-domain result for a replace job (no 5,000-row cap).
        Follow `links.next` until it is `null`.
      parameters:
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          description: A page of replacement results.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/BurnedReplaceResult'
                      links:
                        $ref: '#/components/schemas/ListLinks'
                      meta:
                        $ref: '#/components/schemas/ListMeta'
              examples:
                mixed_page:
                  summary: Completed, failed, and pending replacements
                  value:
                    data:
                      - status: completed
                        domain: acme-mail.com
                        replacement_domain: acme-fresh.com
                        swap_id: swp_01HZX0SW1A2B3C4D5E6F7G8H
                      - status: failed
                        domain: bad-acme.com
                        replacement_domain: bad-acme-fresh.com
                        reason: This domain already has a swap in progress.
                      - status: pending
                        domain: team-acme.com
                        replacement_domain: team-acme-2.com
                    links:
                      first: https://api.peeker.ai/partner/v1/domains/burned/replace/brj_01HZX/results?per_page=25
                      next: null
                    meta:
                      path: https://api.peeker.ai/partner/v1/domains/burned/replace/brj_01HZX/results
                      per_page: 25
                      returned: 3
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/availability:
    post:
      tags: [Domains]
      operationId: checkDomainAvailability
      summary: Check domain availability
      x-mint:
        metadata:
          sidebarTitle: Check domain availability
      description: |
        Submit up to 50 domains. Tells you which are free to register and
        which mailbox types each can take (`usable_for`).

        Taken, premium, invalid, or errored names return `available: false`
        with a `reason`. Tenant answers can be cached up to 35 days; see
        [How domains work](/concepts/domains).

        Sandbox keys return deterministic local results and never call a
        registrar or tenant provider.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domains]
              properties:
                domains:
                  type: array
                  maxItems: 50
                  description: Domain names to check (e.g. `acme.com`). Up to 50 per call.
                  items: { type: string }
            examples:
              three_domains:
                summary: Check three candidate domains
                value:
                  domains: [acme-mail.com, team-acme.com, premium-domain.com]
      responses:
        '200':
          description: One row per domain you sent.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          domains:
                            type: array
                            items:
                              $ref: '#/components/schemas/AvailabilityRow'
              examples:
                all_available:
                  summary: All three domains free to register
                  value:
                    data:
                      domains:
                        - domain: acme-mail.com
                          available: true
                          price_cents: 1300
                          renewal_price_cents: 1500
                          currency: usd
                          usable_for: [google, microsoft]
                        - domain: team-acme.com
                          available: true
                          price_cents: 1300
                          renewal_price_cents: 1500
                          currency: usd
                          usable_for: [google, microsoft]
                        - domain: news-acme.com
                          available: true
                          price_cents: 1300
                          renewal_price_cents: 1500
                          currency: usd
                          usable_for: [google, microsoft]
                mixed:
                  summary: One available, one premium, one unsupported TLD
                  value:
                    data:
                      domains:
                        - domain: acme-mail.com
                          available: true
                          price_cents: 1300
                          renewal_price_cents: 1500
                          currency: usd
                          usable_for: [google, microsoft]
                        - domain: premium-acme.com
                          available: false
                          premium: true
                          reason: premium
                        - domain: acme.unsupported
                          available: false
                          reason: unsupported_tld
                all_taken:
                  summary: Every domain already registered
                  value:
                    data:
                      domains:
                        - domain: acme.com
                          available: false
                          usable_for: [google]
                        - domain: amazon.com
                          available: false
                          usable_for: [google]
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/tlds:
    get:
      tags: [Domains]
      operationId: listDomainTlds
      summary: List live TLD pricing
      x-mint:
        metadata:
          sidebarTitle: List TLD pricing
      description: |
        Returns every active, sellable row from the live `pricingTlds`
        catalog, including registration and renewal pricing. This endpoint is
        the authoritative allowlist for `POST /domains/generate`; do not
        hardcode a TLD list. The current catalog contains 22 rows, but clients
        should use the returned array instead of assuming a fixed count.

        `limited` keys can call this endpoint.
      responses:
        '200':
          description: Live sellable TLD pricing, ready for generate `tlds`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [pricing]
                        properties:
                          pricing:
                            type: array
                            items:
                              $ref: '#/components/schemas/TldPricing'
              examples:
                catalog:
                  summary: Live prices from pricingTlds
                  value:
                    data:
                      pricing:
                        - tld: .com
                          price_cents: 1300
                          renewal_price_cents: 1500
                          currency: usd
                        - tld: .ai
                          price_cents: 6900
                          renewal_price_cents: 7900
                          currency: usd
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/generate:
    post:
      tags: [Domains]
      operationId: generateDomains
      summary: Generate available keyword or AI domain names
      x-mint:
        metadata:
          sidebarTitle: Generate domain names
      description: |
        Supports three request shapes:

        - A `keyword` request runs the deterministic Buy Domains engine with
          Partner-safe neutral modifiers. It does not call Spider or AI.
        - `type: branded` requires a URL. Spider reads the website,
          then concurrent AI workers generate brand-led names.
        - `type: generic` accepts either a URL or a description. A
          URL uses Spider; a description skips Spider. Generic names do not
          use the company brand.

        AI requests have a 15-second end-to-end ceiling. If Spider or AI fails,
        times out, or returns no usable names, generation falls back to the
        deterministic Partner prefix and suffix engine inside that same ceiling.

        Fastly registrar availability is always required. Google and
        Microsoft tenant probes are attempted only for Fastly-available names.
        If a tenant probe fails or times out, the endpoint falls back to the
        Fastly result. A confirmed Google or Microsoft tenant claim is still
        excluded.

        An uncached registrar lookup can take several seconds. `limited` keys
        can call this endpoint.

        Sandbox keys use the same request and response shape, but return
        deterministic local availability without calling a registrar or tenant
        provider. A returned sandbox name passes the same sandbox availability
        check when it is submitted to an order, unless another order used it in
        the meantime.

        `limit` is the number of domain names returned in total across every
        requested TLD (default 50, max 100), not a per-TLD count. Defaults to `.com`
        when `tlds` is omitted. Use `GET /domains/tlds` as the live allowlist.

        Names are grouped by TLD. The response also includes the requested
        subset of live `pricingTlds` rows. TLD group keys have no leading dot
        (`com`, not `.com`), and empty groups are omitted. `limited` keys can
        call this endpoint.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - title: Deterministic keyword generation
                  type: object
                  additionalProperties: false
                  required: [keyword]
                  properties:
                    keyword:
                      type: string
                      maxLength: 64
                      description: Brand or search term. Spaces and a trailing TLD are stripped.
                    tlds:
                      type: array
                      minItems: 1
                      maxItems: 50
                      default: ['.com']
                      description: Optional TLD list from `GET /domains/tlds`. Defaults to `.com`.
                      items:
                        type: string
                    limit:
                      type: integer
                      minimum: 1
                      maximum: 100
                      default: 50
                      description: Total number of names across all requested TLDs. Defaults to 50.
                - title: Branded AI generation from a website
                  type: object
                  additionalProperties: false
                  required: [type, content]
                  properties:
                    type:
                      type: string
                      enum: [branded]
                    content:
                      type: string
                      pattern: ^https?://
                      maxLength: 2048
                      description: Website URL Spider should read. Branded AI requires a URL.
                    tlds:
                      type: array
                      minItems: 1
                      maxItems: 50
                      default: ['.com']
                      items:
                        type: string
                    limit:
                      type: integer
                      minimum: 1
                      maximum: 100
                      default: 50
                      description: Total number of names across all requested TLDs. Defaults to 50.
                - title: Generic AI generation from a website or description
                  type: object
                  additionalProperties: false
                  required: [type, content]
                  properties:
                    type:
                      type: string
                      enum: [generic]
                    content:
                      type: string
                      maxLength: 4000
                      description: Website URL (uses Spider) or plain company description (skips Spider).
                    tlds:
                      type: array
                      minItems: 1
                      maxItems: 50
                      default: ['.com']
                      items:
                        type: string
                    limit:
                      type: integer
                      minimum: 1
                      maximum: 100
                      default: 50
                      description: Total number of names across all requested TLDs. Defaults to 50.
            examples:
              keyword:
                summary: Deterministic Buy Domains keyword search
                value:
                  keyword: peeker
                  tlds: ['.com']
                  limit: 50
              branded_ai:
                summary: Branded names from website data
                value:
                  type: branded
                  content: https://peeker.ai
                  tlds: ['.com', '.ai']
                  limit: 50
              generic_ai:
                summary: Generic names from a description
                value:
                  type: generic
                  content: Software that manages outbound email infrastructure
                  tlds: ['.com', '.io']
                  limit: 50
      responses:
        '200':
          description: Available names grouped by TLD with the requested live pricing subset.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [domains, pricing]
                        properties:
                          domains:
                            type: object
                            additionalProperties:
                              type: array
                              items:
                                type: string
                          pricing:
                            type: array
                            items:
                              $ref: '#/components/schemas/TldPricing'
              examples:
                grouped:
                  summary: Names grouped by TLD with requested pricing
                  value:
                    data:
                      domains:
                        com: [peekerhq.com, getpeeker.com]
                        ai: [peekerhq.ai]
                      pricing:
                        - tld: .com
                          price_cents: 1300
                          renewal_price_cents: 1500
                          currency: usd
                        - tld: .ai
                          price_cents: 6900
                          renewal_price_cents: 7900
                          currency: usd
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/import:
    get:
      tags: [Domains]
      operationId: lookupDomainImport
      summary: Find an import by domain
      x-mint:
        metadata:
          sidebarTitle: Find an import by domain
      description: |
        Looks up import jobs when you no longer have the `imp_…` ID.
        Requires `domains=...`. Pass `include_group=true` for the full
        nameserver group.
      parameters:
        - in: query
          name: domains
          required: true
          description: Comma-separated domains (max 50).
          schema:
            type: string
            example: acme-mail.com,other.com
        - in: query
          name: domain
          description: Optional repeated domain parameter. `domains` is the canonical documented form.
          schema:
            type: string
            example: acme-mail.com
        - in: query
          name: include_group
          description: Include the full nameserver group for each match.
          schema:
            type: boolean
            default: false
            example: true
      responses:
        '200':
          $ref: '#/components/responses/DomainImportLookupResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [Domains]
      operationId: createDomainImport
      summary: Import domains
      x-mint:
        metadata:
          sidebarTitle: Import domains
      description: |
        Imports up to 25 customer-owned root domains and waits for the
        result. Returns nameservers the customer may need to set before
        ordering. For larger batches, use `POST /domains/import/jobs`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domains]
              properties:
                domains:
                  type: array
                  maxItems: 25
                  items:
                    type: string
                    example: acme-mail.com
            examples:
              import_domains:
                summary: Import two customer-owned domains
                value:
                  domains:
                    - acme-mail.com
                    - team-acme.com
      responses:
        '200':
          description: Import finished (success or per-domain failures surfaced in the job).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/DomainImportJob'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/import/jobs:
    get:
      tags: [Domains]
      operationId: listDomainImportJobs
      summary: List import jobs
      x-mint:
        metadata:
          sidebarTitle: List import jobs
      description: Returns import jobs, newest first. Filter by `status` or `domain`.
      parameters:
        - in: query
          name: status
          description: Filter by status.
          schema:
            type: string
            enum: [in_progress, completed, failed, action_required]
        - in: query
          name: domain
          description: Exact domain name.
          schema:
            type: string
            example: acme-mail.com
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          $ref: '#/components/responses/DomainImportJobListResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [Domains]
      operationId: submitDomainImportJob
      summary: Import domains (async)
      x-mint:
        metadata:
          sidebarTitle: Import domains (async)
      description: |
        Queues an import for up to 500 customer-owned domains. Returns
        immediately with `in_progress`. Poll the job or listen for
        `domain_import.*` webhooks.

        Retrying the same domain set returns the existing job. No
        `Idempotency-Key` needed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domains]
              properties:
                domains:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    type: string
                    example: acme-mail.com
            examples:
              import_domains_async:
                summary: Queue a bulk customer-owned domain import
                value:
                  domains:
                    - acme-mail.com
                    - team-acme.com
      responses:
        '202':
          description: Import job accepted.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/DomainImportJob'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/import/jobs/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: The import job ID, e.g. `imp_01HZX`.
        schema: { type: string, example: imp_01HZX }
    get:
      tags: [Domains]
      operationId: getAsyncDomainImportJob
      summary: Get an import job
      x-mint:
        metadata:
          sidebarTitle: Get an import job
      description: Returns one async import job.
      responses:
        '200':
          $ref: '#/components/responses/DomainImportJobResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/import/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: The import job ID, e.g. `imp_01HZX`.
        schema: { type: string, example: imp_01HZX }
    get:
      tags: [Domains]
      operationId: getDomainImport
      summary: Get an import
      x-mint:
        metadata:
          sidebarTitle: Get an import
      description: Returns one import job by ID.
      responses:
        '200':
          $ref: '#/components/responses/DomainImportJobResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/forwarding:
    post:
      tags: [Domains]
      operationId: createDomainForwarding
      summary: Set forwarding
      x-mint:
        metadata:
          sidebarTitle: Set forwarding
      description: |
        Points up to 25 domains at one URL. Returns a job. Poll or listen
        for `domain.forwarding_*` when it finishes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domains, forwarding_url]
              properties:
                domains:
                  type: array
                  maxItems: 25
                  description: Each entry can be a `dom_…` ID or a raw domain name. Maximum 25 domains per call.
                  items: { type: string }
                forwarding_url:
                  type: string
                  description: Where the domains should redirect to.
      responses:
        '202':
          description: Forwarding job started.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/DomainForwardingJob'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /domains/forwarding/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: The forwarding job ID, e.g. `fwj_01HZX`.
        schema: { type: string, example: fwj_01HZX }
    get:
      tags: [Domains]
      operationId: getDomainForwarding
      summary: Get a forwarding job
      x-mint:
        metadata:
          sidebarTitle: Get a forwarding job
      description: Returns one forwarding job and its per-domain results.
      responses:
        '200':
          $ref: '#/components/responses/DomainForwardingJobResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /workspaces:
    get:
      tags:
        - Provider workspaces
      operationId: listProviderWorkspacesV1
      summary: List workspaces
      x-mint:
        metadata:
          sidebarTitle: List workspaces
      description: |
        Lists workspaces connected through this API and regular Peeker
        workspaces that were enabled for Partner API use. Smartlead client
        destinations created by orders stay behind their main workspace and do
        not appear as separate rows.
      parameters:
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          description: Provider workspaces owned by this partner.
          headers:
            Peeker-Request-Id:
              $ref: '#/components/headers/PeekerRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceListEnvelope'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Provider workspaces
      operationId: createProviderWorkspaceV1
      summary: Connect a workspace
      x-mint:
        metadata:
          sidebarTitle: Connect a workspace
      description: |
        Connect a provider once, then use the returned `workspace_id` in later
        orders and pool attachments. You do not need an `Idempotency-Key` for
        this request. An exact reconnect reuses the existing workspace.

        For Smartlead, connect the main workspace with its main API key and a
        label. Do not send a `client_id` or customer object here. Add
        `client_id` to an order only when that order belongs to a Smartlead
        client. The Smartlead login email and password are optional, but they
        must be sent together. They are saved on the workspace and never sent
        with an order.

        If an EmailBison target already belongs to a normal Peeker workspace,
        this request returns `workspace_promotion_required`. Repeat it with
        `promote_existing: true` to let the Partner API use that same workspace.

        With a sandbox key, Peeker checks credential shape and stores the
        submitted placeholder values, but creates a local provider identity
        without calling the sequencer. Never submit production provider keys to
        sandbox.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkspaceCreateRequest'
            examples:
              emailbison:
                summary: EmailBison
                value:
                  provider: emailbison
                  promote_existing: false
                  credentials:
                    api_url: https://dedi.emailbison.com
                    api_key: <emailbison-workspace-api-key>
              instantly:
                summary: Instantly
                value:
                  provider: instantly
                  credentials:
                    api_key: <instantly-api-key>
              smartlead:
                summary: Smartlead main workspace
                value:
                  provider: smartlead
                  label: Agency Smartlead
                  credentials:
                    api_key: <smartlead-main-api-key>
                    login_email: automation@agency.com
                    login_password: <smartlead-login-password>
              plusvibe:
                summary: PlusVibe
                value:
                  provider: plusvibe
                  credentials:
                    api_key: <plusvibe-api-key>
                    workspace_id: 692577ed8543f416f83ad252
                    login_email: automation@agency.com
                    login_password: <plusvibe-login-password>
      responses:
        '200':
          description: Exact existing workspace target reused.
          headers:
            Peeker-Request-Id:
              $ref: '#/components/headers/PeekerRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceEnvelope'
        '201':
          description: Workspace connected, including an approved EmailBison promotion.
          headers:
            Peeker-Request-Id:
              $ref: '#/components/headers/PeekerRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceEnvelope'
              examples:
                plusvibe:
                  value:
                    data:
                      workspace_id: wrk_01HZX0PV1A2B3C4D5E6F7G8H
                      name: Acme PlusVibe
                      provider: plusvibe
                      provider_workspace_id: 692577ed8543f416f83ad252
                      status: active
                      created_at: 2026-07-09T12:00:00Z
                      updated_at: 2026-07-09T12:00:00Z
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ProviderCredentialsRejected'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ProviderUnavailable'
  /workspaces/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          pattern: ^wrk_
        description: Public provider routing target ID.
    get:
      tags:
        - Provider workspaces
      operationId: getProviderWorkspaceV1
      summary: Get a workspace
      x-mint:
        metadata:
          sidebarTitle: Get a workspace
      description: Returns one provider workspace (never includes credentials).
      responses:
        '200':
          description: Provider workspace.
          headers:
            Peeker-Request-Id:
              $ref: '#/components/headers/PeekerRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags:
        - Provider workspaces
      operationId: updateProviderWorkspaceV1
      summary: Update a workspace
      x-mint:
        metadata:
          sidebarTitle: Update a workspace
      description: |
        Rotate the full provider credential object. New credentials must point
        to the same remote workspace. EmailBison, Instantly, and PlusVibe can
        also update customer tracking or clear it with `customer: null`.
        Smartlead can update its label. A promoted EmailBison rotation changes
        only its Partner API credentials.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkspacePatchRequest'
      responses:
        '200':
          description: Updated provider workspace.
          headers:
            Peeker-Request-Id:
              $ref: '#/components/headers/PeekerRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceEnvelope'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ProviderCredentialsRejected'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ProviderUnavailable'
    delete:
      tags:
        - Provider workspaces
      operationId: disconnectProviderWorkspaceV1
      summary: Disconnecting a provider workspace
      description: Disconnects only this public routing target. Sibling targets and the internal credential connection stay active. This does not expose or return credentials.
      responses:
        '200':
          description: Disconnected provider workspace.
          headers:
            Peeker-Request-Id:
              $ref: '#/components/headers/PeekerRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /orders:
    get:
      tags:
        - Orders
      operationId: listOrders
      summary: List orders
      x-mint:
        metadata:
          sidebarTitle: List orders
      description: Returns orders, newest first. Filter by customer, bundle, or status.
      parameters:
        - in: query
          name: customer_id
          description: Filter to orders for one customer, by `cus_…` ID.
          schema:
            type: string
            pattern: ^cus_
            example: cus_01HZX0C6Z3K4M5N6P7Q8R9S0
        - in: query
          name: bundle_id
          description: Filter by bundle (`bun_…`).
          schema:
            type: string
            example: bun_01HZX0BK1A2B3C4D5E6F7G8H
        - in: query
          name: status
          description: Filter by status.
          schema:
            type: string
            enum:
              - in_progress
              - completed
              - failed
              - action_required
              - cancel_scheduled
              - cancelled
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          $ref: '#/components/responses/OrderListResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Orders
      operationId: createStrictOrderV1
      summary: Create an order
      x-mint:
        metadata:
          sidebarTitle: Create an order
      description: |
        Send an order to a connected workspace. Every destination includes its
        provider and Peeker `workspace_id`. For Smartlead, omit `client_id` to
        use the main workspace or include it to use that client workspace.

        Orders do not accept Smartlead login credentials, customer data, or
        user data. A Smartlead Google order needs only the connected API key.
        Smartlead Microsoft or mixed orders use the optional login pair saved
        through `POST /workspaces`.

        Use a bundle or custom sizing, never both. A normal order buys and
        provisions its own domains. It never reserves, attaches, or changes
        pre-warmed pool inventory. Only `POST /pool/attach` can do that.
        Bundle orders may send `quantity`. A quantity of `2` doubles the
        bundle domain and inbox counts; omitting it means `1`.
        For a custom Google order, set `google_licenses` to `1`, `2`, or `3`.
        Peeker creates that many Google inboxes on every submitted domain.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StrictOrderRequest'
            examples:
              emailbison_microsoft_order:
                summary: Existing EmailBison workspace target
                value:
                  destination:
                    provider: emailbison
                    workspace_id: wrk_01HZX0EB1A2B3C4D5E6F7G8H
                  order:
                    microsoft_licenses: 1
                    microsoft_inboxes_per_domain: 25
                    domains:
                      - acme-mail.com
                    personas:
                      - first_name: Alex
                        last_name: Rivera
              instantly_google_order:
                summary: Existing Instantly workspace target
                value:
                  destination:
                    provider: instantly
                    workspace_id: wrk_01HZX0JN1A2B3C4D5E6F7G8H
                    login_credentials:
                      login_email: automation@agency.com
                      login_password: <instantly-login-password>
                  order:
                    google_licenses: 2
                    domains:
                      - acme-mail.com
                    personas:
                      - first_name: Alex
                        last_name: Rivera
              smartlead_google_order:
                summary: Smartlead client workspace
                value:
                  destination:
                    provider: smartlead
                    workspace_id: wrk_01HZX0SK1A2B3C4D5E6F7G8H
                    client_id: '366903'
                  order:
                    bundle_id: bun_01HZX0BK1A2B3C4D5E6F7G8H
                    domains:
                      - acme-mail.com
                    personas:
                      - first_name: Connor
                        last_name: Scale
                        usernames: [connor, connorscale]
              plusvibe_google_order:
                summary: Existing PlusVibe target with workspace-saved login
                value:
                  destination:
                    provider: plusvibe
                    workspace_id: wrk_01HZX0PV1A2B3C4D5E6F7G8H
                  order:
                    google_licenses: 2
                    domains:
                      - acme-mail.com
                    personas:
                      - first_name: Alex
                        last_name: Rivera
      responses:
        '200':
          description: Order accepted, or the matching idempotent order returned.
          headers:
            Peeker-Request-Id:
              $ref: '#/components/headers/PeekerRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderEnvelope'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ProviderUnavailable'
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
  /orders/pending:
    get:
      tags: [Orders]
      operationId: listOrderPendingActions
      summary: List pending actions
      x-mint:
        metadata:
          sidebarTitle: List pending actions
      description: |
        Returns blockers that need your input (nameservers, replace domain,
        etc.). Defaults to open actions only. There is no webhook for this.
        Poll here when an order is `action_required`.
      parameters:
        - in: query
          name: order_id
          description: Only actions for this order.
          schema: { type: string, example: ord_01HZX0OR1A2B3C4D5E6F7G8H }
        - in: query
          name: customer_id
          description: Filter actions by one `cus_…` ID.
          schema: { type: string, pattern: ^cus_, example: cus_01HZX0C6Z3K4M5N6P7Q8R9S0 }
        - in: query
          name: domain_id
          description: Only actions for this domain.
          schema: { type: string, example: dom_01HZX0D01A2B3C4D5E6F7G8H }
        - in: query
          name: status
          description: '`open` (default) or `resolved`.'
          schema:
            type: string
            enum: [open, resolved]
            default: open
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          $ref: '#/components/responses/PendingActionListResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /orders/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: Order ID (`ord_…`).
        schema: { type: string, example: ord_01HZX }
    get:
      tags: [Orders]
      operationId: getOrder
      summary: Get an order
      x-mint:
        metadata:
          sidebarTitle: Get an order
      description: Returns one order, including status, domains, and costs.
      responses:
        '200':
          $ref: '#/components/responses/OrderResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /orders/{id}/cancel:
    parameters:
      - in: path
        name: id
        required: true
        description: Order ID (`ord_…`).
        schema: { type: string, example: ord_01HZX0OR1A2B3C4D5E6F7G8H }
    post:
      tags: [Orders]
      operationId: cancelOrder
      summary: Cancel an order
      x-mint:
        content: |
          <Tabs>
            <Tab title="Cancel whole order">
              ```bash
              curl --request POST \
                --url https://api.peeker.ai/partner/v1/orders/ord_01HZX0OR1A2B3C4D5E6F7G8H/cancel \
                --header 'Authorization: Bearer pk_test_<your-test-key>'
              ```
              Cancels the whole order. No body needed.
            </Tab>
            <Tab title="Cancel specific domains">
              ```json
              {
                "domains": ["acme-mail.com", "dom_01HZX0D02A2B3C4D5E6F7G8H"]
              }
              ```
              Cancels only these domains (`dom_…` ID or name).
            </Tab>
          </Tabs>

          <Warning>
          Cancel is accepted right away (`cancel_scheduled`) and finishes 30
          days later (`cancels_at`). Cancelling the whole order ends in
          `order.cancelled`. Cancelling some domains can leave the order
          active (`order.domains_cancelled`). Those seats stop renewing after
          the 30 days. The order `costs` field is not rewritten when you
          schedule the cancel.
          </Warning>
        metadata:
          sidebarTitle: Cancel an order
      description: |
        Cancels the whole order, or only the domains you list. Omit the
        body to cancel everything. Finalization is scheduled 30 days out
        (`cancels_at`). See [How orders work](/concepts/orders).
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              required: [domains]
              additionalProperties: false
              properties:
                domains:
                  type: array
                  minItems: 1
                  description: Domains to cancel (`dom_…` ID or name). Omit the body to cancel the whole order.
                  items: { type: string }
                  example: [acme-mail.com, dom_01HZX0D02A2B3C4D5E6F7G8H]
            examples:
              specific_domains:
                summary: Cancel specific domains
                value:
                  domains: [acme-mail.com, dom_01HZX0D02A2B3C4D5E6F7G8H]
      responses:
        '200':
          $ref: '#/components/responses/OrderCancellationResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pool:
    get:
      tags: [Domain pool]
      operationId: listPoolDomains
      summary: List pool domains
      x-mint:
        metadata:
          sidebarTitle: List pool domains
      description: |
        Returns pre-warmed Google pool inventory and health. Use
        `availability=available` for assignable names. Provider health remains
        visible but does not block eligibility, including an explicit `burned`
        result. The separate source-order, warmup, billing, and readiness gates
        still apply.
      parameters:
        - in: query
          name: availability
          schema:
            type: string
            default: all
            enum: [all, available, claimed]
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          description: One page of pool domains.
          content:
            application/json:
              schema:
                type: object
                required: [data, links, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PoolDomain'
                  links:
                    $ref: '#/components/schemas/ListLinks'
                  meta:
                    $ref: '#/components/schemas/ListMeta'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pool/preview:
    post:
      tags: [Domain pool]
      operationId: previewPoolDomains
      summary: Preview pool domains
      x-mint:
        metadata:
          sidebarTitle: Preview pool domains
      description: |
        Returns exactly N assignable domain names, or `409` with none. Provider
        health is informational, so an otherwise-ready domain can be returned
        even when `GET /pool` reports `deliverability_burned`. Source-order,
        warmup, billing, mailbox identity, and forwarding gates still apply.
        A preview does not reserve the domains. Needs a full-access key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [domain_count]
              properties:
                domain_count: { type: integer, minimum: 1, maximum: 500 }
                requires_forwarding:
                  type: boolean
                  description: When true, preview only domains with exactly two physical inboxes.
      responses:
        '200':
          description: Complete soft preview.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [domains, domain_count, inbox_count, previewed_until]
                        properties:
                          domains:
                            type: array
                            items: { type: string }
                          domain_count: { type: integer }
                          inbox_count: { type: integer }
                          previewed_until: { type: string, format: date-time }
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pool/add:
    post:
      tags: [Domain pool]
      operationId: addSubscriptionDomainsToPool
      summary: Add domains to the pool
      x-mint:
        metadata:
          sidebarTitle: Add domains to the pool
      description: |
        Adds paid Google domains (active billing + active inboxes) into your
        pool. Every inbox must have complete provider mailbox identity.
        All-or-nothing. Does not create an order or change billing. Sandbox
        uses five-second initial and post-release warmups, so poll `GET /pool`
        after adding or releasing. Sandbox also creates local Cloudflare
        readiness and a local recycle forwarding target without provider
        network calls.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [domains]
              properties:
                domains:
                  type: array
                  minItems: 1
                  maxItems: 500
                  uniqueItems: true
                  items: { type: string }
      responses:
        '200':
          $ref: '#/components/responses/PoolAddResponse'
        '201':
          $ref: '#/components/responses/PoolAddResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pool/attach:
    post:
      tags: [Domain pool]
      operationId: attachPoolDomains
      summary: Attach pool domains
      x-mint:
        metadata:
          sidebarTitle: Attach pool domains
      description: |
        Attach an exact set of pre-warmed domains to a connected workspace.
        This is the only order flow that reserves or changes pool inventory.
        Normal `POST /orders` requests never use the pool.

        Ready names stay assigned even when their health signal says burned.
        Missing or claimed names are replaced with the oldest ready options. If
        Peeker cannot fill the full request, nothing changes. `202` means the
        domains are reserved and processing. Poll the reservation or subscribe
        to `pool.claim.*`.

        Send the provider and public `workspace_id`. For Smartlead, omit
        `client_id` for the main workspace or include it for one client. No user
        or customer object is needed. Peeker checks the saved upload credentials
        before it reserves anything. A missing Smartlead or PlusVibe login pair
        returns `422 provider_login_credentials_required` and leaves the pool
        unchanged.

        Peeker also checks every selected domain before reservation. Missing
        Provider mailbox identity, or missing Cloudflare readiness when
        `forwarding_url` is sent, returns `422` with code
        `domains_not_submittable`. `error.details.failures` identifies each
        blocked domain with `google_engine_identity_incomplete` or
        `forwarding_not_ready`. No inventory is reserved.

        Sandbox records forwarding locally and simulates username swaps. It
        does not call Cloudflare, the mailbox provider, or the destination
        sequencer.
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 256
          description: |
            Recommended for durable retries. Without this header, an exact
            replay reuses the same live reservation, including one whose
            release is unfinished. A failed attach or completed release permits
            a fresh reservation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [domains, destination, personas]
              properties:
                domains:
                  type: array
                  minItems: 1
                  maxItems: 500
                  uniqueItems: true
                  description: Domain names from preview. Peeker returns the final name for every entry.
                  items: { type: string }
                destination:
                  $ref: '#/components/schemas/PoolDestination'
                personas:
                  type: array
                  minItems: 1
                  maxItems: 10
                  description: >
                    Either every persona includes `usernames` (preferred mailbox
                    local parts / aliases), every persona includes legacy `email`,
                    or neither. Do not mix modes. Peeker fits the identities to
                    each domain's inbox count, generates missing identities, and
                    ignores extras that do not fit the selected domains.
                  items:
                    type: object
                    additionalProperties: false
                    required: [first_name, last_name]
                    properties:
                      first_name: { type: string, maxLength: 128 }
                      last_name: { type: string, maxLength: 128 }
                      usernames:
                        type: array
                        minItems: 1
                        maxItems: 10
                        description: Mailbox local parts for this persona (no @).
                        items:
                          type: string
                          maxLength: 64
                          example: alex.r
                      email:
                        type: string
                        format: email
                        description: Legacy full address when not using usernames.
                forwarding_url:
                  type: [string, 'null']
                  format: uri
            examples:
              workspace_target:
                summary: Connected workspace with persona usernames
                value:
                  domains: [warm-one.example, warm-two.example]
                  destination:
                    provider: plusvibe
                    workspace_id: wrk_01HZX0NP1A2B3C4D5E6F7G8H
                  personas:
                    - first_name: Alex
                      last_name: Rivera
                      usernames: [alex, alex.r]
              smartlead_main:
                summary: Smartlead main workspace
                value:
                  domains: [warm-one.example, warm-two.example]
                  destination:
                    provider: smartlead
                    workspace_id: wrk_01HZX0SK1A2B3C4D5E6F7G8H
                  personas:
                    - first_name: Ada
                      last_name: Lovelace
              smartlead_client:
                summary: Smartlead client workspace
                value:
                  domains: [warm-one.example, warm-two.example]
                  destination:
                    provider: smartlead
                    workspace_id: wrk_01HZX0SK1A2B3C4D5E6F7G8H
                    client_id: '366903'
                  personas:
                    - first_name: Ada
                      last_name: Lovelace
      responses:
        '202':
          $ref: '#/components/responses/PoolReservationResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ProviderUnavailable'
        '500':
          $ref: '#/components/responses/InternalError'

  /pool/reservations/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: string, pattern: ^pcl_ }
    get:
      tags: [Domain pool]
      operationId: getPoolReservation
      summary: Get a pool reservation
      x-mint:
        metadata:
          sidebarTitle: Get a pool reservation
      description: Returns one attach job and which domains were assigned.
      responses:
        '200':
          $ref: '#/components/responses/PoolReservationResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pool/release:
    post:
      tags: [Domain pool]
      operationId: releasePoolReservation
      summary: Release pool domains
      x-mint:
        metadata:
          sidebarTitle: Release pool domains
      description: |
        Returns every domain in a reservation to your pool when a customer
        cancels. By default they come back on the next monthly date; pass
        `forced: true` to take them back now. Billing on the source order
        is unchanged. A live reservation that used `forwarding_url` requires
        the pool recycle forwarding URL configured by Peeker during production
        setup. Sandbox supplies a local recycle target automatically.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [reservation_id]
              properties:
                reservation_id: { type: string, pattern: ^pcl_ }
                forced:
                  type: boolean
                  default: false
                  description: >-
                    Take the domains back immediately instead of waiting for
                    the next monthly date.
      responses:
        '200':
          $ref: '#/components/responses/PoolReleaseResponse'
        '202':
          $ref: '#/components/responses/PoolReleaseResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pool/releases/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: string, pattern: ^prl_ }
    get:
      tags: [Domain pool]
      operationId: getPoolRelease
      summary: Get a pool release
      x-mint:
        metadata:
          sidebarTitle: Get a pool release
      description: Returns one release job.
      responses:
        '200':
          $ref: '#/components/responses/PoolReleaseResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /swaps:
    get:
      tags: [Domain swaps]
      operationId: listSwaps
      summary: List swaps
      x-mint:
        metadata:
          sidebarTitle: List swaps
      description: Returns domain and sender-rename swaps, newest first.
      parameters:
        - in: query
          name: customer_id
          description: Filter to swaps for one customer.
          schema: { type: string, pattern: ^cus_, example: cus_01HZX0C6Z3K4M5N6P7Q8R9S0 }
        - in: query
          name: order_id
          description: Filter by order (`ord_…`).
          schema: { type: string, example: ord_01HZX0OR1A2B3C4D5E6F7G8H }
        - in: query
          name: domain_id
          description: Filter by domain (`dom_…`).
          schema: { type: string, example: dom_01HZX0D01A2B3C4D5E6F7G8H }
        - in: query
          name: status
          description: Filter by status.
          schema:
            type: string
            enum: [created, in_progress, completed, failed, action_required]
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PageToken'
      responses:
        '200':
          $ref: '#/components/responses/SwapListResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [Domain swaps]
      operationId: createSwap
      summary: Swap a domain
      x-mint:
        content: |
          <Note>
          Need the difference between standard and premium?
          [How swaps work](/concepts/swaps). Want new inbox names on the same
          domain? Use `POST /swaps/user_names`.
          </Note>
        metadata:
          sidebarTitle: Swap a domain
      description: |
        Replaces one domain inside an existing order.
        See [How swaps work](/concepts/swaps) for standard vs premium.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, domain_id, replacement_domain]
              properties:
                type:
                  type: string
                  enum: [standard, premium]
                domain_id:
                  type: string
                  description: Domain to replace (`dom_…`).
                replacement_domain:
                  type: string
                  description: Replacement domain name.
      responses:
        '201':
          $ref: '#/components/responses/SwapResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /swaps/user_names:
    post:
      tags: [Domain swaps]
      operationId: createUserNameSwap
      summary: Rename inbox senders
      x-mint:
        metadata:
          sidebarTitle: Rename inbox senders
      description: |
        Keeps the domain; regenerates mailbox names from new personas.
        Only `first_name` and `last_name` change mailbox identities.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain_id, personas]
              properties:
                domain_id: { type: string }
                personas:
                  type: array
                  description: New name list.
                  items:
                    type: object
                    required: [first_name, last_name]
                    properties:
                      first_name: { type: string }
                      last_name: { type: string }
                      profile_picture_url:
                        type: string
                        nullable: true
                        description: Optional. Accepted but not applied to inboxes today.
      responses:
        '201':
          $ref: '#/components/responses/SwapBatchResponse'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /swaps/{id}:
    parameters:
      - in: path
        name: id
        required: true
        description: The swap ID, e.g. `swp_01HZX`.
        schema: { type: string, example: swp_01HZX }
    get:
      tags: [Domain swaps]
      operationId: getSwap
      summary: Get a swap
      x-mint:
        metadata:
          sidebarTitle: Get a swap
      description: Returns one swap job.
      responses:
        '200':
          $ref: '#/components/responses/SwapResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
