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

# Assign pre-warmed inboxes

> Add paid Google domains, assign them to customers, and return or replace them.

The pool holds paid Google domains with warmed inboxes, commonly two per
domain. Add domains once, preview an exact quantity, attach them to a workspace,
and release them when the assignment ends.

This flow is separate from normal ordering. `POST /orders` never reads,
reserves, attaches, or changes pool inventory. Only `POST /pool/attach` assigns
pre-warmed domains.

## 1. Create the routing target

Call `POST /workspaces` first and keep the returned `workspace_id`. Workspace
creation does not use an `Idempotency-Key`.

For Smartlead, connect the main workspace. Do not send `client_id` or customer
data. The login pair is optional, but it must be complete when sent:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
	"provider": "smartlead",
	"label": "Agency Smartlead",
	"credentials": {
		"api_key": "<smartlead-main-api-key>",
		"login_email": "automation@agency.com",
		"login_password": "<smartlead-login-password>"
	}
}
```

Peeker never returns the login values. If the pair is missing when an upload
needs it, attach returns `422 provider_login_credentials_required` before
reserving any pool domain. Connect the same workspace again with the pair, then
retry the attach.

## 2. Add paid domains

`POST /pool/add` adds domains from a paid Google order that already has active
inboxes.

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl -X POST "https://api.peeker.ai/partner/v1/pool/add" \
  -H "Authorization: Bearer pk_live_<your-key>" \
  -H "Content-Type: application/json" \
  --data '{
    "domains": ["paid-one.example", "paid-two.example"]
  }'
```

Each domain must have active billing and at least one active Google inbox
(commonly two). Every inbox must also have the provider mailbox IDs needed for a
later username swap. The whole request passes or nothing changes. This call
does not create an order or change billing.

A domain becomes `available` only after all of these are true:

* Its source order is at least 14 days old.
* Its recorded warmup time has been served, when a warmup start is known.
* Its inboxes are active.

Provider health stays visible on the pool row but does not block an otherwise
ready domain, including an explicit `burned` result. The source order, recorded
warmup clocks, active billing, mailbox identity, and forwarding checks still
apply.

With a sandbox key, initial and post-release warmups are five seconds instead
of 14 days. `POST /pool/add` may therefore return a new domain with status
`warming`. Poll `GET /pool?availability=available` until it appears before
previewing or attaching it.

## 3. Preview an exact quantity

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl -X POST "https://api.peeker.ai/partner/v1/pool/preview" \
  -H "Authorization: Bearer pk_live_<your-key>" \
  -H "Content-Type: application/json" \
  --data '{ "domain_count": 20 }'
```

You get all 20 domain names or `insufficient_inventory`. A preview does not
reserve them. Peeker tries not to show the same domains in two concurrent
previews for five minutes, but can reuse an unclaimed preview when inventory is
tight.

Use `GET /pool?availability=available` when you need to list ready inventory.
Follow the `next` link until it is `null`.

## 4. Attach the domains

Send the previewed domain names and the public workspace target:

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl -X POST "https://api.peeker.ai/partner/v1/pool/attach" \
  -H "Authorization: Bearer pk_live_<your-key>" \
  -H "Idempotency-Key: pool_attach_01HZX0NP1A2B3C4D" \
  -H "Content-Type: application/json" \
  --data '{
    "domains": ["warm-one.example", "warm-two.example"],
    "destination": {
	  "provider": "smartlead",
      "workspace_id": "wrk_01HZX0NP1A2B3C4D5E6F7G8H"
    },
    "personas": [
      {
        "first_name": "Alex",
        "last_name": "Rivera",
        "usernames": ["alex", "alex.r"]
      }
    ],
    "forwarding_url": "https://listkit.io"
  }'
```

Generate a new idempotency key for each attach. Reuse that key only when
retrying the same attach request. Existing callers that omit the header remain
supported: an exact retry reuses the live reservation instead of reserving
again. An unfinished release still blocks a duplicate. A failed attach or
completed release permits a fresh reservation, so the header is still the
safest option.

Omit `client_id` to attach to the Smartlead main workspace. Include it for one
client workspace:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
	"destination": {
		"provider": "smartlead",
		"workspace_id": "wrk_01HZX0SK1A2B3C4D5E6F7G8H",
		"client_id": "366903"
	}
}
```

Peeker keeps the submitted main `workspace_id` in the reservation and resolves
the client behind it. No email, user, or customer object is needed.

You can send 1 to 10 personas. Prefer `usernames` for exact mailbox local parts:
one persona may list multiple aliases (for example `alex` and `alex.r`). For
`usernames`, Peeker keeps the first local parts that fit each domain's inbox
count, generates any missing seats from the persona names, and ignores extras.
Legacy `email` (a full address, one per persona) still works when every persona
includes it; those domains must have at least that many inboxes. Otherwise omit
both fields and Peeker creates the usernames from the names.

If a submitted domain is no longer ready, Peeker uses the oldest ready domain
instead. If it cannot fill the whole request, nothing is assigned.

Before reserving anything, Peeker checks that every selected domain has complete
provider mailbox identity. It also checks that Smartlead and PlusVibe targets
have a complete saved login pair. Missing target login returns `422` with
`provider_login_credentials_required` and reserves nothing. When
`forwarding_url` is present, Peeker checks that every selected domain has an
active Cloudflare zone and account. A domain failure returns `422` with code
`domains_not_submittable` and one reason per blocked domain:

* `google_engine_identity_incomplete`
* `forwarding_not_ready`

No inventory is reserved when this check fails. Fix the reported setup issue,
then retry the same request and idempotency key.

`202` means the assignment was accepted. Peeker then updates forwarding,
submits one Google username swap per domain, and uploads the inboxes to the
destination workspace. The response maps each `requested_domain` to the final
`domain` and says if it was `substituted`.

Poll the returned `reservation_id`:

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl "https://api.peeker.ai/partner/v1/pool/reservations/pcl_01HZX0PC1A2B3C4D5E6F7G8H" \
  -H "Authorization: Bearer pk_live_<your-key>"
```

You can also listen for `pool.claim.completed` and `pool.claim.failed`.

`reserved` and `renaming` remain in progress while Peeker retries provider or
network attempts. `action_required` is used only when fulfillment needs a
customer or partner change. `failed` means fulfillment was explicitly stopped;
Peeker does not fail an accepted reservation because a retry limit was reached.

### Sandbox behavior

Sandbox uses the same requests, response shapes, polling, and webhooks as live.
Workspace credentials are checked for the right shape and stored, but sandbox
uses a local provider identity instead of validating them with the sequencer.
Use placeholder credentials, never production provider keys.
Sandbox orders create local Cloudflare test records, but they do not create or
change pool rows. Pool rows still enter through `POST /pool/add` and leave only
through the pool lifecycle. Sandbox attach records forwarding changes and
simulates username swaps without calling Cloudflare, the mailbox provider, or
the destination sequencer.

Before switching to a live key:

1. Confirm `GET /me` reports `api_key.environment: "live"`.
2. Confirm the destination workspace credentials are valid.
3. Confirm each pool domain has complete provider mailbox identity and an active
   Cloudflare connection when forwarding is requested. Review its health signal
   separately; a burned signal does not block attach.
4. Ask Peeker to configure the live pool recycle forwarding URL before releasing
   any reservation that used forwarding.
5. Use a unique idempotency key for each new attach.
6. Treat `202` as accepted work and poll until the reservation is terminal.

## 5. Return the domains when the customer cancels

When a customer cancels, release their reservation. By default the domains
come back on the next monthly date from the original attach, so the customer
keeps sending until the end of their current month:

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl -X POST "https://api.peeker.ai/partner/v1/pool/release" \
  -H "Authorization: Bearer pk_live_<your-key>" \
  -H "Content-Type: application/json" \
  --data '{
    "reservation_id": "pcl_01HZX0PC1A2B3C4D5E6F7G8H"
  }'
```

The response includes `effective_at`. Add `"forced": true` to take the
domains back immediately. If a release is already scheduled, sending the
forced request moves that same release to now. It does not create a second
release.

Poll `GET /pool/releases/{id}` or listen for `pool.release.completed` and
`pool.release.failed`.

Peeker retries temporary forwarding or Google Engine errors up to three total
attempts, one minute apart. The release stays scheduled during those retries.
Permanent setup errors fail immediately, and a temporary error emits
`pool.release.failed` only after the final attempt.

Healthy and underperforming domains return to your pool, get fresh sender names
and redirects, then warm up again before they can be assigned. They are not
treated as burned.

If the reservation used `forwarding_url`, a live pool must already have its
recycle forwarding URL configured. Peeker configures this live-only setting
during production setup. Sandbox supplies a local recycle target automatically.

## What happens when a domain is burned

Peeker runs two separate replacements:

1. The customer gets an available warmed domain. Pool health stays visible but
   does not block that candidate. Based on your pool setting, this
   starts at once, waits for your approval, or stays off. It has its own
   reservation and `pool.replacement.*` webhooks.
2. The pool gets a bought backup domain. Peeker cancels the old inboxes, clears
   the old stats, and creates new inboxes under the same original order and
   billing. Billing is not changed.

The backup gets new sender names, the pool forwarding URL, matching inbox count
for that domain, and a full warmup period. It cannot be shown as available
until provisioning finishes.

If no bought backup is ready yet, the pool waits until one is available, then
continues automatically.
