---
title: "Create Plan"
method: POST
path: "/mailboxes/schedule-buy/plans"
tags: ["Schedule Buy"]
---

# Create Plan

`POST /mailboxes/schedule-buy/plans`

## Request

- Base URL: `https://be.graph8.com/api/v1`
- URL: `https://be.graph8.com/api/v1/mailboxes/schedule-buy/plans`
- Auth: HTTP bearer

Create and LOCK a buying plan. PLACES REAL CREDIT HOLDS IMMEDIATELY.

Locking reserves credits for the WHOLE plan up front - one hold per batch,
up to two hundred of them - even though the domains are bought a batch a
day afterwards. The credits are gone from your spendable balance the
moment this returns. `total_credits_held` in the response is the figure.

Org admins and owners only, and only for an org on a paid or pre-funded
plan. 402 if the balance cannot cover the plan, 429 if the org has locked
more than five plans this hour, 400 for an unsupported extension or an
empty plan. With `reuse_domains`, 409 when a domain's mailbox count changed
(or it filled up) while the plan was locking: nothing stays held, re-read
the domains and lock again.

At least one `persona_ids` entry is required. Manual `registrants` /
`mailbox_aliases` are not accepted for new plans. `idle_mailboxes_snapshot`
is your own count, used to detect drift - the server recomputes it and
returns its own as `server_idle_mailboxes`.

Send an `Idempotency-Key` header. A retried lock without one places a
second full set of holds.

## Headers

- `Authorization` string, nullable
- `X-Target-Org-Id` string, nullable

## Request body

- ScheduleBuyPlanRequest: Create and LOCK a staggered buying plan. PLACES REAL CREDIT HOLDS. Mirrors the app's ``CreatePlanRequest`` field for field, including the rule that new plans require ``persona_ids``. Manual ``registrants`` / ``mailbox_aliases`` are still parsed so a legacy payload 422s here rather than inside the proxy.
  - `target_monthly_volume` integer, required: Contacts you intend to reach per month
  - `idle_mailboxes_snapshot` integer, required: Idle mailboxes you already hold, from /suggest
  - `warmup_provider` 'warmy' | 'instantly' | 'smartlead': Warmup engine the bought mailboxes are enrolled with
  - `domain_tld` string: Extension to buy, e.g. '.com'
  - `registrants` object[]: Not accepted for new plans. Use persona_ids.
  - `mailbox_aliases` object[]: Not accepted for new plans. Use persona_ids.
  - `persona_ids` integer[], nullable: Sender identity IDs. At least one is required to start a schedule.
  - `workspace_uid` string, nullable: Existing provisioning workspace to reuse
  - `daily_send_cap` integer: Sends per mailbox per day
  - `sending_days_per_month` integer: Sending days assumed per month
  - `mailboxes_per_domain` integer: Mailboxes bought on each domain
  - `domains_per_batch` integer: Domains bought per daily batch
  - `reuse_domains` string[], nullable: Adopt these owned domains instead of buying. Each must have fewer live mailboxes than mailboxes_per_domain; the plan tops it up and holds credits for exactly the missing ones
  - `domain_names` string[], nullable: Approved names to buy, in order

## Response `200`

Successful Response

- ApiResponseMailboxDomainPayload
  - `data` MailboxDomainPayload, required: An open envelope for one provider/deliverability result. Open on purpose. These payloads are provider state and advisory reads whose contents grow with the product, and a dropped field reads as "nothing to report" rather than "we did not tell you". Every value passes through ``scrub_vendor_names`` on the way in.
  - `pagination` PaginationMeta
    - `page` integer, required: Current page number (1-indexed)
    - `limit` integer, required: Items per page
    - `total` integer, required: Total number of items
    - `has_next` boolean, required: Whether there are more pages
    - `next_cursor` string, nullable: Opaque cursor for the next page; null when there are no more items. Pass it back as the `cursor` query param to fetch the following page without computing page numbers.
    - `total_available` integer, nullable: Search endpoints only: how many records really match, when known. Can be larger than `total`, which is capped at the 10,000 results that can be paged. For company search this is a distinct-company estimate. Null when unknown; absent outside search.
    - `total_is_lower_bound` boolean, nullable: Search endpoints only: true when more records match than `total` shows (the result set is larger than the 10,000 results that can be paged). Absent outside search.
    - `notes` string[], nullable: Search endpoints only: plain-language notes about the result, e.g. that more records match than can be paged, or that a reversed `between` range was put in order.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/graph8/apis/graph8-developer-api.md) · [All operations](https://skmtc.dev/graph8/apis/graph8-developer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/graph8/apis/graph8-developer-api/revisions/2e7cfa0ca921?raw)
