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

# Creating an order & user

> Place an order for one customer. Pass one `user` string, Smartlead
routing and login credentials in `sequencer`, domains, and personas.
Pick a saved bundle OR set custom Google/Microsoft license counts and
inboxes-per-domain — not both.

Re-sending the exact same order body returns the original order
(no second charge, no duplicate provisioning).




## OpenAPI

````yaml /openapi.yaml post /orders
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. Add a user, order licenses with imported or

    Peeker-managed domains, and Peeker's backend handles provisioning,

    DNS, forwarding, and lifecycle webhooks.


    The API is opinionated. Most of the heavy logic lives on Peeker's side,

    so a single `POST /orders` provisions everything end-to-end.


    ## Read first


    - **[Authentication](/authentication)** — bearer keys, live vs. sandbox,
    request IDs.

    - **[Rate limits](/rate-limits)** — flat 600/min per key, 429 backoff.

    - **[Webhooks](/webhooks)** — signed event delivery for every async job.

    - **[Best practices](/best-practices)** — errors, paging, idempotency,
    pending actions, webhook hygiene.


    ## Workflow guides


    - **[Quickstart](/guides/quickstart)** — first end-to-end call.

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

    - **[Changing user names on active orders](/guides/user-name-swaps)** —
    rename inboxes on a warm domain.

    - **[Domain swaps & renames](/guides/domain-swaps)** — standard vs premium
    swaps and user-name swaps.


    ## Public ID prefixes


    | Prefix | Resource |

    | --- | --- |

    | `usr_` | User |

    | `bun_` | Bundle |

    | `ord_` | Order |

    | `dom_` | Domain |

    | `imp_` | Domain import job |

    | `fwj_` | Domain forwarding job |

    | `swp_` | Domain swap |

    | `act_` | Pending action |

    | `evt_` | Webhook event |
servers:
  - url: https://api.peeker.ai/partner/v1
    description: >-
      Live and sandbox share one base URL — the `pk_live_…` vs `pk_test_…` key
      prefix routes the request.
security:
  - BearerAuth: []
tags:
  - name: Account
    x-group: Account
    description: |
      Confirm an API key is valid and inspect the partner record it belongs
      to. Use `GET /me` to check the key, see which environment it targets
      (live or sandbox), and read the partner profile.
  - name: Users
    x-group: Users
    description: |
      A user is one real person you're buying email sending for. Create a
      user record once, or let `POST /orders` create/reuse it from email.
      Every order, domain, and swap they buy from you afterwards links back
      to it. IDs look like `usr_01HZX`.

      **Status values**

      | Value | What it means |
      | --- | --- |
      | `active` | The user is in good standing. |
      | `deleted` | You marked them as deleted. Their order history stays. |

      **Things to know**

      - A user's email is unique inside your account. Adding the same
        email twice gives you back the existing user in the error
        response so you can match on `id`.
        - `sequencer.client_id` controls Smartlead routing for future orders.
          Send a Smartlead client ID for client routing, or `null`/`""` to
          use the configured default/admin route.
      - Deleting a user is reversible from the portal. The API doesn't
        hard-delete — past orders, domains, and swaps stay attached.
      - Users don't log in — they're identity-only records. If you want
        them to log in, send them an invite from the portal.
  - name: Bundles
    x-group: Bundles
    description: |
      A bundle is a saved order template. Set it up once with a monthly
      sending volume (and optionally a Google/Microsoft split), then re-use
      it on every order. IDs look like `bun_01HZX`.

      **Status values**

      | Value | What it means |
      | --- | --- |
      | `active` | Selectable for new orders. |
      | `archived` | Read-only. Old orders still show it. |

      **How the split works**

      If you set both `google_percent` and `microsoft_percent`, they have to
      add up to 100. If you leave them both empty, Peeker picks the
      recommended split for you. Either way, the response shows you the
      resolved values in the `effective` block.

      The recommended values aren't stored on the bundle row. When Peeker
      adjusts the recommended split, bundles inheriting it pick up the new
      values automatically.

      **Things to know**

      - Bundles are sized in monthly sending volume. By default Peeker does
        not send on weekends, so monthly volume is spread across 22 sending
        days. Set `send_on_weekends` to `true` to spread the same monthly
        volume across 30 days.
      - Google domains use `google_inboxes_per_domain` as the density. Allowed
        values are `2` and `3`; the default is `2`. Each Google inbox sends
        20 emails/day, so Google capacity is 40 or 60 emails/domain/day.
      - Microsoft capacity is 150 emails/domain/day.
      - Deleting a bundle that no order ever used removes it. Otherwise
        it's archived so old orders stay linked correctly.
  - name: Domains
    x-group: Domains
    description: >
      A domain is a website your inboxes will send from. Domains live in

      your partner account whether or not they're attached to an order, so

      the same `dom_01HZX` ID stays with the domain forever.


      **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`, `user_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: Orders
    x-group: Orders
    description: >
      An order is one purchase: domains plus Google or Microsoft licenses for

      one customer. Each license becomes one or more inboxes once

      provisioning finishes. Pick either a saved bundle OR custom

      Google/Microsoft license counts plus inboxes-per-domain — not both.

      IDs look like `ord_01HZX`.


      **Status values**


      | Value | What it means |

      | --- | --- |

      | `in_progress` | Accepted; we're setting it up. |

      | `completed` | All domains and inboxes are live. |

      | `failed` | Something broke that we can't fix automatically. |

      | `action_required` | We need your help to finish (see "Pending actions"
      below). |

      | `cancel_scheduled` | Cancellation accepted; not finished yet. |

      | `cancelled` | Cancelled. The domains are free to use on a new order. |


      **How many domains you need**


      - Microsoft: 1 license → 1 domain. Custom orders must send
        `microsoft_inboxes_per_domain` as `25`, `50`, `75`, or `99`.
      - Google: 1 license → 1 inbox. Custom orders must send
        `google_inboxes_per_domain` as `2` or `3`.

      So total domains needed equals

      `microsoft_licenses + ceil(google_licenses / google_inboxes_per_domain)`.

      If your count is off, you'll get back `domain_count_mismatch` with the

      gap in `details`.


      Partners import owned domains first through `/domains/import`. Domains

      that have not been imported are treated as registration candidates and

      are checked cache-first before the order is accepted. Unavailable,

      premium, unknown, or provider-error domains fail before wallet debit

      with `domains_not_submittable`.


      **Re-sending the same order**


      Re-sending the exact same order body — same customer, same domains,

      same everything — returns the original order. No second charge, no

      duplicate provisioning, no extra webhook.


      If you send a different order body but it overlaps with a domain

      that's on another active order (or one scheduled to cancel), the

      whole new order is rejected with `domain_already_in_active_order`.

      Cancel the other order first, or pick different domains.


      **Pending actions**


      A pending action is a public blocker that needs partner/customer input.

      Open actions make the order read as `action_required`. Use

      `GET /orders/pending` to list them. Allowed reasons:

      `update_nameservers`, `replace_domain`, and

      `profile_picture_not_accessible`.


      **Cancellations**


      Call `POST /orders/{id}/cancel` with no body to cancel the whole

      order. Send `domains` only when you want to cancel specific domains.

      The order's status moves to `cancel_scheduled` immediately and

      finalizes after a short grace window.


      Cancelling an order schedules the inboxes to be deleted at the end of

      the renewal period. Cancelling an individual Google domain cancels the

      2-3 licenses on that domain and only reduces that part of the order

      amount.
  - name: Domain swaps
    x-group: Domain swaps
    description: |
      A domain swap replaces a domain inside an existing order without
      losing the order. IDs look like `swp_01HZX`.

      **Status values**

      | Value | What it means |
      | --- | --- |
      | `created` | We've accepted the swap. |
      | `in_progress` | We're working on it. |
      | `completed` | The new domain is up; the old one is detached. |
      | `failed` | Something broke that we can't fix automatically. |
      | `action_required` | We need your help to finish. |

      **Standard vs premium**

      - **Standard.** The old domain is retired right away.
      - **Premium.** We keep the old domain warm for 14 days alongside the
        new one before retiring it. Use this when you want a smoother
        handoff. Premium swaps are **Microsoft domains only**, capped per
        billing tier (Base 20% / Premium 30% / VIP 40% / VIP+ 60% of your
        active Microsoft inboxes), and each domain has a **30-day cooldown**
        between premium swaps.

      A swap we reject as a duplicate of one already in flight is reported
      as `failed`.

      **Rename inboxes on a domain**

      The user-names swap is a different kind of swap: the domain stays, but
      the people whose names we use to generate inboxes change. Use it when
      a customer wants different names on the inboxes of an already-running
      domain.
  - name: Webhooks
    x-group: Webhooks
    description: >
      Peeker POSTs signed JSON events to a URL you configure when async work

      changes state — orders, domains, imports, and swaps. Register

      the URL and shared secret in the [Partner
      portal](https://app.peeker.com/partner/webhooks).

      Event IDs look like `evt_01HZX`.


      Delivery is at-least-once with up to 5 retries over 24 hours. Verify

      the `Peeker-Signature` header (`v1=<hex hmac-sha256>`) against the

      string `<timestamp>.<event_id>.<body>` before trusting any payload,

      and dedupe on `Peeker-Event-Id` for at least 24 hours.


      Full event catalog, signature verification snippets, and retry policy

      live on the [Webhooks](/webhooks) page.
paths:
  /orders:
    post:
      tags:
        - Orders
      summary: Creating an order & user
      description: |
        Place an order for one customer. Pass one `user` string, Smartlead
        routing and login credentials in `sequencer`, domains, and personas.
        Pick a saved bundle OR set custom Google/Microsoft license counts and
        inboxes-per-domain — not both.

        Re-sending the exact same order body returns the original order
        (no second charge, no duplicate provisioning).
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - title: Bundle order
                  type: object
                  required:
                    - user
                    - bundle_id
                    - sequencer
                    - domains
                    - personas
                  properties:
                    user:
                      type: string
                      description: >-
                        Existing `usr_…` customer ID, or an email to
                        reuse/create.
                      example: client@example.com
                    bundle_id:
                      type: string
                      description: >-
                        Saved bundle to use for license counts and inbox
                        density.
                      example: bun_123
                    sequencer:
                      type: object
                      required:
                        - provider
                        - client_id
                        - login_email
                        - login_password
                      properties:
                        provider:
                          type: string
                          enum:
                            - smartlead
                        client_id:
                          type:
                            - number
                            - string
                            - 'null'
                          example: 366903
                          description: >-
                            Smartlead client ID. `null` or `""` uses the
                            default/admin route.
                        login_email:
                          type: string
                          example: smartlead-login@example.com
                          description: >-
                            Smartlead login email used only for internal
                            provider submission.
                        login_password:
                          type: string
                          format: password
                          example: secret-password
                          description: >-
                            Smartlead login password used only for internal
                            provider submission.
                    domains:
                      type: array
                      description: |
                        One entry per domain. Use a plain string for the common
                        case (the domain inherits the order-level
                        `forwarding_url`). Pass an object only when a single
                        domain needs its own forwarding URL.
                      items:
                        oneOf:
                          - type: string
                            description: >-
                              Domain name. Inherits `forwarding_url` from the
                              order.
                            example: acme-mail.com
                          - type: object
                            required:
                              - domain
                            description: >-
                              Per-domain override of the order-level forwarding
                              URL.
                            properties:
                              domain:
                                type: string
                                example: shop-acme.com
                              forwarding_url:
                                type: string
                                example: https://shop.acme.com
                      example:
                        - domain-01.com
                        - domain-02.com
                        - domain-03.com
                        - domain-04.com
                        - domain-05.com
                        - domain-06.com
                        - domain-07.com
                        - domain-08.com
                        - domain-09.com
                        - domain-10.com
                    personas:
                      type: array
                      maxItems: 10
                      description: >
                        Up to 10 personas to use when generating inbox names.

                        Peeker auto-generates the inbox usernames/local parts
                        from these names.
                      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: ''
                            description: Public image URL, `null`, or `""`.
                    forwarding_url:
                      type: string
                      description: A default redirect for every domain in this order.
                - title: Custom order
                  type: object
                  required:
                    - user
                    - sequencer
                    - domains
                    - personas
                  properties:
                    user:
                      type: string
                      description: >-
                        Existing `usr_…` customer ID, or an email to
                        reuse/create.
                      example: client@example.com
                    sequencer:
                      type: object
                      required:
                        - provider
                        - client_id
                        - login_email
                        - login_password
                      properties:
                        provider:
                          type: string
                          enum:
                            - smartlead
                        client_id:
                          type:
                            - number
                            - string
                            - 'null'
                          example: 366903
                          description: >-
                            Smartlead client ID. `null` or `""` uses the
                            default/admin route.
                        login_email:
                          type: string
                          example: smartlead-login@example.com
                          description: >-
                            Smartlead login email used only for internal
                            provider submission.
                        login_password:
                          type: string
                          format: password
                          example: secret-password
                          description: >-
                            Smartlead login password used only for internal
                            provider submission.
                    google_licenses:
                      type: integer
                      example: 20
                      description: >-
                        Custom orders only. Required with
                        `google_inboxes_per_domain` when Google is used.
                    google_inboxes_per_domain:
                      type: integer
                      enum:
                        - 2
                        - 3
                      example: 2
                      description: >-
                        Custom Google orders only. Number of Google inboxes per
                        submitted Google domain.
                    microsoft_licenses:
                      type: integer
                      example: 2
                      description: >-
                        Custom orders only. Required with
                        `microsoft_inboxes_per_domain` when Microsoft is used.
                    microsoft_inboxes_per_domain:
                      type: integer
                      enum:
                        - 25
                        - 50
                        - 75
                        - 99
                      example: 50
                      description: >-
                        Custom Microsoft orders only. Number of Microsoft
                        inboxes per submitted Microsoft domain.
                    domains:
                      type: array
                      description: |
                        One entry per domain. Use a plain string for the common
                        case (the domain inherits the order-level
                        `forwarding_url`). Pass an object only when a single
                        domain needs its own forwarding URL.
                      items:
                        oneOf:
                          - type: string
                            description: >-
                              Domain name. Inherits `forwarding_url` from the
                              order.
                            example: acme-mail.com
                          - type: object
                            required:
                              - domain
                            description: >-
                              Per-domain override of the order-level forwarding
                              URL.
                            properties:
                              domain:
                                type: string
                                example: shop-acme.com
                              forwarding_url:
                                type: string
                                example: https://shop.acme.com
                      example:
                        - domain-01.com
                        - domain-02.com
                        - domain-03.com
                        - domain-04.com
                        - domain-05.com
                        - domain-06.com
                        - domain-07.com
                        - domain-08.com
                        - domain-09.com
                        - domain-10.com
                        - domain-11.com
                        - domain-12.com
                    personas:
                      type: array
                      maxItems: 10
                      description: >
                        Up to 10 personas to use when generating inbox names.

                        Peeker auto-generates the inbox usernames/local parts
                        from these names.
                      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: ''
                            description: Public image URL, `null`, or `""`.
                    forwarding_url:
                      type: string
                      description: A default redirect for every domain in this order.
            examples:
              bundle_order:
                summary: Bundle order
                value:
                  user: client@example.com
                  bundle_id: bun_123
                  sequencer:
                    provider: smartlead
                    client_id: 366903
                    login_email: smartlead-login@example.com
                    login_password: secret-password
                  domains:
                    - domain-01.com
                    - domain-02.com
                    - domain-03.com
                    - domain-04.com
                    - domain-05.com
                    - domain-06.com
                    - domain-07.com
                    - domain-08.com
                    - domain-09.com
                    - domain-10.com
                  personas:
                    - first_name: Sam
                      last_name: Lee
                      profile_picture_url: ''
              existing_user_order:
                summary: Order for an existing user
                value:
                  user: usr_01HZX0C6Z3K4M5N6P7Q8R9S0
                  bundle_id: bun_123
                  sequencer:
                    provider: smartlead
                    client_id: 366903
                    login_email: smartlead-login@example.com
                    login_password: secret-password
                  domains:
                    - domain-01.com
                    - domain-02.com
                    - domain-03.com
                    - domain-04.com
                    - domain-05.com
                    - domain-06.com
                    - domain-07.com
                    - domain-08.com
                    - domain-09.com
                    - domain-10.com
                  personas:
                    - first_name: Sam
                      last_name: Lee
                      profile_picture_url: ''
              custom_order:
                summary: Custom order
                value:
                  user: client@example.com
                  sequencer:
                    provider: smartlead
                    client_id: 366903
                    login_email: smartlead-login@example.com
                    login_password: secret-password
                  google_licenses: 20
                  google_inboxes_per_domain: 2
                  microsoft_licenses: 2
                  microsoft_inboxes_per_domain: 50
                  domains:
                    - domain-01.com
                    - domain-02.com
                    - domain-03.com
                    - domain-04.com
                    - domain-05.com
                    - domain-06.com
                    - domain-07.com
                    - domain-08.com
                    - domain-09.com
                    - domain-10.com
                    - domain-11.com
                    - domain-12.com
                  personas:
                    - first_name: Sam
                      last_name: Lee
                      profile_picture_url: null
      responses:
        '200':
          $ref: '#/components/responses/OrderSummaryResponse'
        '400':
          description: Bad input or one of the domains is already in another order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                domain_count_mismatch:
                  summary: License count requires more domains than were sent
                  value:
                    error:
                      code: domain_count_mismatch
                      message: Submitted domain count does not match the order plan
                      field: domains
                      details:
                        required_count: 4
                        submitted_count: 2
                        missing_count: 2
                domain_already_in_active_order:
                  summary: One or more domains are already on another order
                  value:
                    error:
                      code: domain_already_in_active_order
                      message: >-
                        One or more domains are already in an active or
                        pending-cancel order. Cancel that order or use different
                        domains.
                      details:
                        domains:
                          - acme-mail.com
                order_shape_conflict:
                  summary: Mixed bundle and license fields
                  value:
                    error:
                      code: order_shape_conflict
                      message: >-
                        bundle_id may not be combined with google_licenses,
                        microsoft_licenses, google_inboxes_per_domain, or
                        microsoft_inboxes_per_domain
                domains_not_submittable:
                  summary: Registration candidates are blocked
                  value:
                    error:
                      code: domains_not_submittable
                      message: Some domains cannot be submitted for registration
                      field: domains
                      details:
                        required_replacements: 2
                        domains_not_submittable:
                          - domain: taken-domain.com
                            reason: unavailable
                          - domain: premium-domain.com
                            reason: premium
                domain_not_usable_for_provider:
                  summary: Domain can't host the requested provider
                  value:
                    error:
                      code: domain_not_usable_for_provider
                      message: The domain is not usable for the requested provider
                      field: domains
                      details:
                        domain: shared-domain.com
                        provider: microsoft
                        usable_for:
                          - google
components:
  responses:
    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
                  user_id: usr_01HZX0C6Z3K4M5N6P7Q8R9S0
                  email: client@example.com
                  bundle_id: bun_123
                  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'
  schemas:
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - invalid_request
                - unauthorized
                - forbidden
                - not_found
                - rate_limited
                - provider_not_supported
                - smartlead_connection_required
                - smartlead_client_not_found
                - order_shape_conflict
                - domain_count_mismatch
                - domain_not_imported
                - domains_not_submittable
                - premium_domain_not_supported
                - domain_not_usable_for_provider
                - domain_already_in_active_order
                - internal_error
              example: domain_count_mismatch
            message:
              type: string
              example: Submitted domain count does not match the order plan
            field:
              type: string
              example: domains
            details:
              type: object
              additionalProperties: true
              example:
                required_count: 20
                submitted_count: 18
                missing_count: 2
    SuccessEnvelope:
      type: object
      required:
        - data
      properties:
        data: {}
    OrderSummary:
      type: object
      required:
        - id
        - user_id
        - email
        - status
        - domain_count
        - costs
        - renews_at
        - created_at
      properties:
        id:
          type: string
          example: ord_01HZX0OR1A2B3C4D5E6F7G8H
        user_id:
          type: string
          nullable: true
          example: usr_01HZX0C6Z3K4M5N6P7Q8R9S0
        email:
          type: string
          nullable: true
          example: alex@acme.com
        bundle_id:
          type: string
          nullable: true
          example: bun_01HZX0BU1A2B3C4D5E6F7G8H
        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'
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Your partner API key. Use `pk_live_…` against the live system or
        `pk_test_…` against the sandbox.
      x-default: Bearer pk_test_<your-test-key>

````