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

# Importing domains & ordering

> Import domains the customer already owns, place an order, and track fulfillment webhooks.

Use this flow when the customer already owns the domains at another registrar. Peeker imports the domains into Cloudflare, provisions inboxes, and pushes them into Smartlead.

## Full workflow

Save this as a sandbox script to verify the chain end-to-end before you ship.

```bash cURL · full workflow lines theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
BASE="https://api.peeker.ai/partner/v1"
AUTH="Authorization: Bearer pk_test_<your-key>"

# Step 1: Submit the import job for domains the customer already owns.
curl -X POST "$BASE/domains/import" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{
    "domains":    ["acme-mail.com", "team-acme.com"]
  }'

# Step 2: Poll until the import completes (or listen for the webhook).
curl -X GET "$BASE/domains/import/imp_01HZX0IM…" -H "$AUTH"

# For bulk async imports up to 500 domains, submit a job instead.
curl -X POST "$BASE/domains/import/jobs" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{
    "domains":    ["acme-mail.com", "team-acme.com"]
  }'

# Step 3: Submit the order - provisions every inbox, routes to Smartlead,
#         runs forwarding, queues the webhook stream.
curl -X POST "$BASE/orders" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{
    "user": "alex@acme.com",
    "bundle_id":          "bun_01HZX0BU…",
    "sequencer": {
      "provider": "smartlead",
      "client_id": 366903,
      "login_email": "ops@acme.com",
      "login_password": "their-smartlead-password"
    },
    "forwarding_url":     "https://acme.com",
    "domains":            ["acme-mail.com", "team-acme.com"],
    "personas": [
      { "first_name": "Alex", "last_name": "Rivera", "profile_picture_url": "https://cdn.acme.com/alex.jpg" },
      { "first_name": "Sam",  "last_name": "Lee",    "profile_picture_url": "https://cdn.acme.com/sam.jpg"  }
    ]
  }'
```

## Steps in detail

<AccordionGroup>
  <Accordion title="1. Submit the import job" defaultOpen>
    Call `POST /domains/import` with domains the customer already owns. The response includes the nameservers your customer must set at their registrar. The domains attach to the customer later, when you place the order.

    Submit root domains only, such as `acme-mail.com`, not subdomains like `mail.acme.com`. Peeker validates import eligibility before returning the job; invalid or non-root domains can come back as failed job rows and may take longer than the normal fast enqueue path.

    ```json 200 OK · nameserver groups theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
    {
    	"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:00:04Z"
    	}
    }
    ```

    Send the customer those nameservers in your UI:

    > Open your registrar's DNS settings for **acme-mail.com** and **team-acme.com**. Replace the existing nameservers with helena/idris. Most registrars apply the change within a few hours.

    For larger batches, call `POST /domains/import/jobs` with up to 500 domains. It returns `202 Accepted` with an `in_progress` job immediately; then poll `GET /domains/import/jobs/{id}` or listen for `domain_import.*` webhooks. Async job creation uses the normal Partner API rate limit and does not fail just because Cloudflare accounts are currently busy. Retrying the same normalized domain set returns the existing import job, so there is no idempotency header to generate.
  </Accordion>

  <Accordion title="2. Wait for the import to complete">
    Poll `GET /domains/import/{id}` or `GET /domains/import/jobs/{id}` every 60 seconds, or subscribe to the `domain_import.completed` webhook. If you no longer have the `imp_...` ID, call `GET /domains/import?domains=acme-mail.com,team-acme.com`; the domain query is required because this endpoint is a lookup, not a general import-history list. Async job status moves from `in_progress` → `completed` once nameservers are ready.

    If you get `domain_import.action_required`, surface "Acme - waiting on registrar" in your UI and re-show the same nameservers. Peeker keeps re-checking; you'll get `domain_import.completed` automatically when the customer flips DNS.

    <CodeGroup>
      ```json Webhook · domain_import.completed theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
      {
      	"id": "evt_01HZX0EVBA2B3C4D5E6F7G8H",
      	"type": "domain_import.completed",
      	"created_at": "2026-05-08T12:08:43Z",
      	"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"
      	}
      }
      ```

      ```json Webhook · domain_import.action_required theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
      {
      	"type": "domain_import.action_required",
      	"data": {
      		"id": "imp_01HZX0IM1A2B3C4D5E6F7G8H",
      		"status": "action_required",
      		"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"
      	}
      }
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="3. Submit the order">
    One call. Pass a `bundle_id`, Smartlead route and login credentials, imported domains, and personas.

    Smartlead orders require both `sequencer.login_email` and `sequencer.login_password`. A Smartlead client ID or API key alone is not enough for Peeker to submit the provider work.

    If the `user` email already exists, Peeker reuses that user and updates their saved Smartlead routing for future orders. Re-sending the same body returns the original order - orders dedupe for 24 hours.

    ```json 200 OK · order theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
    {
    	"data": {
    		"id": "ord_01HZX0OR1A2B3C4D5E6F7G8H",
    		"user_id": "usr_01HZX0C6Z3K4M5N6P7Q8R9S0",
    		"email": "alex@acme.com",
    		"bundle_id": "bun_01HZX0BU…",
    		"status": "in_progress",
    		"domain_count": 2,
    		"costs": {
    			"total_cents": 6800,
    			"currency": "usd"
    		},
    		"created_at": "2026-05-08T12:00:00Z"
    	}
    }
    ```

    Use `GET /orders/{id}` or order webhooks when you need the full detail response with imported vs registered domains, line items, and personas.
  </Accordion>

  <Accordion title="4. Watch fulfillment webhooks">
    The progression for a Smartlead-backed order:

    ```
    order.in_progress
      → domain.connected (×N)   (one per domain as DNS + provider land)
      → order.completed         (every inbox is live in Smartlead)
    ```

    When `order.completed` fires, the inboxes are already pushed into Smartlead. They can start warming and sending. No additional API call from your side.

    <CodeGroup>
      ```json Webhook · domain.connected theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
      {
      	"id": "evt_01HZX0EV7A2B3C4D5E6F7G8H",
      	"type": "domain.connected",
      	"created_at": "2026-05-08T12:08:00Z",
      	"data": {
      		"id": "dom_01HZX0D01A2B3C4D5E6F7G8H",
      		"domain": "acme-mail.com",
      		"category": "connected",
      		"status": "active",
      		"provider": "google",
      		"user_id": "usr_01HZX0C6Z3K4M5N6P7Q8R9S0",
      		"order_id": "ord_01HZX0OR1A2B3C4D5E6F7G8H",
      		"usable_for": ["google", "microsoft"],
      		"forwarding_url": "https://acme.com",
      		"created_at": "2026-05-08T12:00:00Z"
      	}
      }
      ```

      ```json Webhook · order.completed theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
      {
      	"id": "evt_01HZX0EV2A2B3C4D5E6F7G8H",
      	"type": "order.completed",
      	"created_at": "2026-05-08T12:14:33Z",
      	"data": {
      		"id": "ord_01HZX0OR1A2B3C4D5E6F7G8H",
      		"user_id": "usr_01HZX0C6Z3K4M5N6P7Q8R9S0",
      		"email": "alex@acme.com",
      		"bundle_id": "bun_01HZX0BU…",
      		"status": "completed",
      		"domain_count": 2,
      		"costs": {
      			"total_cents": 6800,
      			"currency": "usd",
      			"line_items": []
      		},
      		"domains": {
      			"registered": [],
      			"imported": ["acme-mail.com", "team-acme.com"]
      		},
      		"personas": [
      			{
      				"first_name": "Alex",
      				"last_name": "Rivera",
      				"profile_picture_url": "https://cdn.acme.com/alex.jpg"
      			},
      			{
      				"first_name": "Sam",
      				"last_name": "Lee",
      				"profile_picture_url": "https://cdn.acme.com/sam.jpg"
      			}
      		],
      		"created_at": "2026-05-08T12:00:00Z"
      	}
      }
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

## Things to handle in production

A few real-world cases that show up once you're past sandbox:

* **Customer hasn't flipped nameservers yet.** You'll see `domain_import.action_required` after \~30 minutes. Re-show the nameservers in your UI and surface "Acme - waiting on registrar." Peeker keeps re-checking; you'll get `domain_import.completed` when they propagate.
* **Profile picture URL is private/invalid.** You'll see `order.action_required`; call `GET /orders/pending?order_id=...` and look for `reason: "profile_picture_not_accessible"`. Get a fresh public URL from the customer and place a corrected order.
* **Domain needs partner action.** `GET /orders/pending` returns `update_nameservers` or `replace_domain` only when there is a concrete customer/partner action to take.

## What's next

<CardGroup cols={2}>
  <Card title="Buying domains from your registrar" href="/guides/buying-domains">
    Same flow, but Peeker buys the domains and runs DNS - no nameserver step for the customer.
  </Card>

  <Card title="Change forwarding URLs" href="/guides/forwarding">
    Repoint up to 25 domains at a new forwarding URL after the order is live.
  </Card>

  <Card title="How to implement domain swaps" href="/guides/domain-swaps">
    Replace a degrading domain mid-flight without losing warmup.
  </Card>

  <Card title="Best practices" href="/best-practices">
    Errors, paging, idempotent retries, pending actions, webhook hygiene.
  </Card>
</CardGroup>
