> ## Documentation Index
> Fetch the complete documentation index at: https://docs.peeker.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Attach pool domains

> 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.




## OpenAPI

````yaml /openapi.yaml post /pool/attach
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).
paths:
  /pool/attach:
    post:
      tags:
        - Domain pool
      summary: 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.
      operationId: attachPoolDomains
      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'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ProviderUnavailable'
components:
  schemas:
    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.
    SuccessEnvelope:
      type: object
      required:
        - data
      properties:
        data: {}
    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
    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`.
    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'
    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
    PoolOperationError:
      type: object
      additionalProperties: false
      required:
        - code
        - message
        - retryable
      properties:
        code:
          type: string
        message:
          type: string
        retryable:
          type: boolean
    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
  responses:
    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'
    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'
    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'
    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'
    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'
  headers:
    PeekerRequestId:
      description: Stable request identifier. Log it with the status and response body.
      schema:
        type: string
        example: 550e8400-e29b-41d4-a716-446655440000
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Partner API key (`pk_live_…` or `pk_test_…`).
      x-default: Bearer pk_test_<your-test-key>

````