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

# Change forwarding URLs

> Repoint up to 25 domains at a new forwarding URL in one call. Async per-domain job with completed and failed results.

When a customer changes their marketing site (or you need to move forwarding off a stale URL), `POST /domains/forwarding` repoints up to 25 domains at one new URL in a single call. The job runs async; domains already pointed at the URL are returned as completed.

<Note>
  For a fresh order, set `forwarding_url` on `POST /orders` instead - that handles forwarding inline
  as part of provisioning. This guide is for **changing** forwarding on domains that are already
  live.
</Note>

## Steps in detail

<AccordionGroup>
  <Accordion title="1. Submit the bulk change" defaultOpen>
    Each row in `domains[]` can be a `dom_…` ID or a raw domain name - both resolve to the same domain row. Send at most 25 domains per request. The whole request returns quickly with an `fwj_…` job ID; the actual work runs async.

    ```bash cURL theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
    curl -X POST "https://api.peeker.ai/partner/v1/domains/forwarding" \
      -H "Authorization: Bearer pk_test_<your-key>" \
      -H 'Content-Type: application/json' \
      -d '{
        "domains":        ["acme-mail.com", "team-acme.com", "news-acme.com"],
        "forwarding_url": "https://acme.com/inbox-redirect"
      }'
    ```

    ```json 202 Accepted theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
    {
    	"data": {
    		"id": "fwj_01HZX0FW1A2B3C4D5E6F7G8H",
    		"status": "in_progress",
    		"completed_count": 0,
    		"failed_count": 0,
    		"forwarding_url": "https://acme.com/inbox-redirect",
    		"completed": [],
    		"failed": [],
    		"created_at": "2026-05-08T12:00:00Z"
    	}
    }
    ```
  </Accordion>

  <Accordion title="2. Poll until terminal">
    Poll `GET /domains/forwarding/{id}` until the job reaches one of three terminal states:

    * **`completed`** - every submitted domain succeeded.
    * **`partial_success`** - some completed and some failed.
    * **`failed`** - the whole job hit an unrecoverable error.

    Already-correct domains are included in `completed` with newly updated domains.

    ```bash cURL theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
    curl -X GET "https://api.peeker.ai/partner/v1/domains/forwarding/fwj_01HZX0FW…" \
      -H "Authorization: Bearer pk_test_<your-key>"
    ```

    ```json 200 OK · completed theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
    {
    	"data": {
    		"id": "fwj_01HZX0FW1A2B3C4D5E6F7G8H",
    		"status": "completed",
    		"completed_count": 3,
    		"failed_count": 0,
    		"forwarding_url": "https://acme.com/inbox-redirect",
    		"completed": ["acme-mail.com", "team-acme.com", "news-acme.com"],
    		"failed": [],
    		"created_at": "2026-05-08T12:00:00Z",
    		"completed_at": "2026-05-08T12:01:30Z"
    	}
    }
    ```
  </Accordion>

  <Accordion title="3. Listen for the webhooks">
    Forwarding job webhooks use the same `data` shape as `GET /domains/forwarding/{id}`. That makes webhook handling and polling use the same parser.

    <CodeGroup>
      ```json Webhook · domain.forwarding_updated theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
      {
      	"id": "evt_01HZX0EV9A2B3C4D5E6F7G8H",
      	"type": "domain.forwarding_updated",
      	"created_at": "2026-05-08T12:01:30Z",
      	"data": {
      		"id": "fwj_01HZX0FW1A2B3C4D5E6F7G8H",
      		"status": "completed",
      		"completed_count": 3,
      		"failed_count": 0,
      		"forwarding_url": "https://acme.com/inbox-redirect",
      		"completed": ["acme-mail.com", "team-acme.com", "news-acme.com"],
      		"failed": [],
      		"created_at": "2026-05-08T12:00:00Z",
      		"completed_at": "2026-05-08T12:01:30Z"
      	}
      }
      ```

      ```json Webhook · domain.forwarding_failed theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
      {
      	"id": "evt_01HZX0EVAA2B3C4D5E6F7G8H",
      	"type": "domain.forwarding_failed",
      	"created_at": "2026-05-08T12:01:30Z",
      	"data": {
      		"id": "fwj_01HZX0FW1A2B3C4D5E6F7G8H",
      		"status": "partial_success",
      		"completed_count": 2,
      		"failed_count": 1,
      		"forwarding_url": "https://acme.com/inbox-redirect",
      		"completed": ["acme-mail.com", "team-acme.com"],
      		"failed": [{ "domain": "bad-acme.com", "reason": "dns_lookup_failed" }],
      		"created_at": "2026-05-08T12:00:00Z",
      		"completed_at": "2026-05-08T12:01:30Z"
      	}
      }
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="4. Failure reasons & remedies">
    | Reason               | What to do                                                                            |
    | -------------------- | ------------------------------------------------------------------------------------- |
    | `dns_lookup_failed`  | The customer's domain isn't resolving (likely a registrar issue). Re-import or wait.  |
    | `target_unreachable` | The forwarding URL returned 5xx during validation. Check the URL is live and `https`. |
    | `invalid_target`     | URL is malformed or not `http(s)://`. Send a corrected URL on a fresh forwarding job. |

    Already-correct domains are idempotent - safe to re-run the same job; they return in `completed`.
  </Accordion>
</AccordionGroup>

## Things to handle in production

* **Mixing `dom_…` IDs and raw names.** Both resolve. If you have the IDs already (from a previous order response), they're slightly faster - Peeker skips the lookup.
* **Per-domain overrides on a fresh order.** If you're placing a new order and most domains share one forwarding URL but one is different, set `forwarding_url` on `POST /orders` and pass an override object for the odd domain - no separate `POST /domains/forwarding` call needed. Example in [Buying domains & ordering](/guides/buying-domains).
* **Rate budget.** A 25-domain forwarding request burns one rate-limit token (out of 600/min). For 100 domains, send four 25-domain requests - four tokens instead of 100 separate per-domain calls.

## What's next

<CardGroup cols={2}>
  <Card title="Buying domains & ordering" href="/guides/buying-domains">
    Set forwarding inline when you place a fresh order.
  </Card>

  <Card title="Webhooks" href="/webhooks">
    Verify forwarding event signatures and dedupe on `Peeker-Event-Id`.
  </Card>
</CardGroup>
