> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Extract Endpoints

> Reference for the Zenrows Extract endpoints: read which domains Extract serves today, ask for a new domain to be prepared, and track a preparation through to ready.

Extract's own endpoints live under `https://api.zenrows.com/v1/extract/`. They answer two questions that `extract=auto` cannot: **which domains work today**, and **how to get one that doesn't yet**.

None of them fetches a page, so none of them costs credits.

Authenticate the same way as any Zenrows request, with either the `apikey` query parameter or the `X-API-Key` header. Both work on every endpoint below; the examples alternate to show each.

## Which domains Extract serves

### `GET /v1/extract/domains`

Every domain Extract can handle right now. This is the list behind [Domain coverage](/extract/setup#domain-coverage), read live instead of from a page, so an integration can check coverage before sending a request rather than handling an error after one.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl "https://api.zenrows.com/v1/extract/domains?apikey=YOUR_ZENROWS_API_KEY"
  ```

  ```python Python theme={"dark"}
  # pip install requests
  import requests

  response = requests.get(
      'https://api.zenrows.com/v1/extract/domains',
      headers={'X-API-Key': 'YOUR_ZENROWS_API_KEY'},
  )
  supported = {entry['domain'] for entry in response.json()['domains']}
  print('amazon.com' in supported)
  ```

  ```javascript Node.js theme={"dark"}
  const response = await fetch('https://api.zenrows.com/v1/extract/domains', {
      headers: { 'X-API-Key': 'YOUR_ZENROWS_API_KEY' },
  });
  const { domains } = await response.json();
  const supported = new Set(domains.map((entry) => entry.domain));
  console.log(supported.has('amazon.com'));
  ```
</CodeGroup>

```json Response theme={"dark"}
{
  "domains": [
    {
      "domain": "amazon.com",
      "verticals": [
        "Automotive/Auto Parts",
        "Books and Literature/Fiction",
        "Shopping",
        "Technology & Computing/Consumer Electronics"
      ],
      "warm": true
    },
    {
      "domain": "americanas.com.br",
      "verticals": ["Shopping", "Technology & Computing/Consumer Electronics"],
      "warm": true
    }
  ]
}
```

| Field       | Description                                                                                                                                                                                                  |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `domain`    | The registrable domain. It covers every subdomain: `amazon.com` also means `www.amazon.com`.                                                                                                                 |
| `verticals` | The kinds of page Extract has prepared for this domain. A domain serving several verticals is one entry, not several. Roughly a quarter of entries have none, which does not mean the domain works any less. |
| `warm`      | Always `true` today. The field exists for a future "declared but not prepared yet" state.                                                                                                                    |

A domain in this list is a domain you can send `extract=auto` to. A domain missing from it returns [`REQS007`](/api-error-codes#REQS007), naming the domain and pointing at the endpoint below.

## Getting a domain prepared

Extract works on a domain once Zenrows has prepared it: learned its page shapes and verified that extraction actually returns the right data. You can ask for a domain to be prepared yourself, within a monthly allowance.

<Note>
  Preparing a domain is not instant. A submission is accepted immediately and then works through discovery, preparation and verification, typically minutes rather than seconds. Poll the status endpoint rather than blocking on the submit call.
</Note>

### `POST /v1/extract/prepared-domains`

Ask for a domain to be prepared. The body takes the domain and, optionally, example URLs.

**Send example URLs when you can.** They are authoritative: Zenrows prepares exactly the page kinds you point at, and skips the discovery step that guesses them. One URL per page kind you care about is enough: a product page, a search results page.

| Field          | Required | Description                                                                   |
| -------------- | -------- | ----------------------------------------------------------------------------- |
| `domain`       | yes      | The domain to prepare, without scheme.                                        |
| `example_urls` | no       | URLs on that domain, one per page kind. When present, they replace discovery. |

Send an `Idempotency-Key` header to make a retry safe: the same key returns the original submission instead of spending a second allowance slot.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl -X POST "https://api.zenrows.com/v1/extract/prepared-domains?apikey=YOUR_ZENROWS_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: 8f14e45f-ea2c-4f2b-9c1a-7d3b6a0e5c91" \
    -d '{
      "domain": "scrapingcourse.com",
      "example_urls": [
        "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie",
        "https://www.scrapingcourse.com/ecommerce/"
      ]
    }'
  ```

  ```python Python theme={"dark"}
  # pip install requests
  import requests

  response = requests.post(
      'https://api.zenrows.com/v1/extract/prepared-domains',
      headers={
          'X-API-Key': 'YOUR_ZENROWS_API_KEY',
          'Idempotency-Key': '8f14e45f-ea2c-4f2b-9c1a-7d3b6a0e5c91',
      },
      json={
          'domain': 'scrapingcourse.com',
          'example_urls': [
              'https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie',
              'https://www.scrapingcourse.com/ecommerce/',
          ],
      },
  )
  print(response.status_code, response.json()['state'])
  ```
</CodeGroup>

A submission returns **`202 Accepted`** and its starting state, in the same shape the status endpoint returns:

```json Response theme={"dark"}
{
  "domain": "scrapingcourse.com",
  "status": "preparing",
  "state": "received",
  "submitted_at": "2026-09-14T08:02:38.686126Z",
  "updated_at": "2026-09-14T08:02:38.686126Z"
}
```

Submitting a domain that is already prepared returns `202` with its current record rather than starting again, so a retry is never wasted.

### `GET /v1/extract/prepared-domains/{domain}`

How far a submission has got.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl "https://api.zenrows.com/v1/extract/prepared-domains/scrapingcourse.com?apikey=YOUR_ZENROWS_API_KEY"
  ```

  ```python Python theme={"dark"}
  # pip install requests
  import requests, time

  while True:
      response = requests.get(
          'https://api.zenrows.com/v1/extract/prepared-domains/scrapingcourse.com',
          headers={'X-API-Key': 'YOUR_ZENROWS_API_KEY'},
      )
      body = response.json()
      if body['status'] != 'preparing':
          break
      time.sleep(30)

  print(body['status'])
  ```
</CodeGroup>

A domain that was never submitted returns `404`:

```json Response theme={"dark"}
{
  "code": "not_found",
  "detail": "This domain has never been submitted.",
  "status": 404,
  "title": "Not found (not_found)",
  "type": "https://docs.zenrows.com/api-error-codes#not_found"
}
```

#### Reading the response

Two fields describe progress, at two levels of detail. **Branch on `status`; show `state` if you are drawing a progress bar.**

`status` is the answer to "can I use this domain yet":

| `status`    | Meaning                                        |
| ----------- | ---------------------------------------------- |
| `preparing` | Still working. Keep polling.                   |
| `ready`     | Done. `extract=auto` works on this domain now. |
| `failed`    | Stopped. See `failure`.                        |

`state` is the current step, and will gain steps over time:

| `state`            | Meaning                                                                |
| ------------------ | ---------------------------------------------------------------------- |
| `received`         | Accepted, not started.                                                 |
| `discovering`      | Finding the domain's page kinds. Skipped when you sent `example_urls`. |
| `preparing`        | Building the extraction for each page kind.                            |
| `verifying`        | Checking that extraction returns the right data.                       |
| `ready` / `failed` | Terminal, matching `status`.                                           |

`roles` breaks the same progress down per page kind, each with its own `status`, the `url` it is working from, and a `detail` sentence if it stopped. It is only present for the account that submitted the domain, because example URLs disclose what you scrape and are not shown to other accounts polling it.

#### When a preparation fails

A `failed` submission carries a `failure` block with a `code`, a `detail` sentence, and `slot_consumed`:

```json Response theme={"dark"}
{
  "domain": "scrapingcourse.com",
  "status": "failed",
  "state": "failed",
  "roles": [
    {
      "role": "",
      "status": "failed",
      "url": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie",
      "detail": "the target could not be fetched; the gateway rejected this request"
    }
  ],
  "failure": {
    "code": "unpreparable",
    "detail": "we could not prepare this domain: 1 of 1 pages could not be prepared: https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie (the target could not be fetched; the gateway rejected this request)",
    "slot_consumed": false
  },
  "submitted_at": "2026-09-14T08:02:38.686126Z",
  "updated_at": "2026-09-14T08:02:39.927245Z"
}
```

`slot_consumed` tells you whether the attempt cost you a slot, but **it does not tell you what to do next** on its own. Two failures hand the slot back and still need different responses: one is ours to fix and a plain retry works, the other will fail again with the same URLs.

Read the code, and use `slot_consumed` for the billing half of the sentence:

| `code`                  | What happened                                                                                  | What to do                                                                    | `slot_consumed` |
| ----------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------- |
| `interrupted`           | Processing stopped on our side.                                                                | Resubmit exactly as you sent it.                                              | `false`         |
| `preparation_exhausted` | We could not produce a verifiable extraction in two independent attempts. Ours, not yours.     | Resubmit exactly as you sent it.                                              | `false`         |
| `discovery_failed`      | We could not work out the domain's page kinds on our own.                                      | Resubmit with `example_urls`.                                                 | `false`         |
| `unpreparable`          | None of the pages you sent gave us usable HTML: blocked, a challenge page, or not HTML at all. | Try different example URLs from the same site. The same ones will fail again. | `false`         |
| `preparation_failed`    | Extraction could not be built for a page kind.                                                 | Try other example URLs, or contact support.                                   | `true`          |
| `verification_failed`   | Extraction was built but did not meet the quality bar.                                         | Contact support.                                                              | `true`          |
| `unsupported`           | The domain cannot be handled.                                                                  | Contact support before spending more slots on it.                             | `true`          |

### `GET /v1/extract/prepared-domains`

Every domain your account has submitted, with your allowance alongside it.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl "https://api.zenrows.com/v1/extract/prepared-domains?apikey=YOUR_ZENROWS_API_KEY"
  ```
</CodeGroup>

```json Response theme={"dark"}
{
  "allowance": {
    "used": 0,
    "limit": 10,
    "remaining": 10,
    "month": "2026-09"
  },
  "domains": []
}
```

This lists what **you** submitted, which is a different question from [which domains Extract serves](#which-domains-extract-serves). That one lists every prepared domain, however it got there.

### `GET /v1/extract/prepared-domains/allowance`

How many domains you can still ask for this month, on its own.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl "https://api.zenrows.com/v1/extract/prepared-domains/allowance?apikey=YOUR_ZENROWS_API_KEY"
  ```
</CodeGroup>

```json Response theme={"dark"}
{
  "used": 0,
  "limit": 10,
  "remaining": 10,
  "month": "2026-09"
}
```

`month` is the UTC calendar month the count applies to; the allowance resets when it rolls over. A failure that returns your slot (`slot_consumed: false`) is not counted in `used`.

## Errors

Alongside the [standard Zenrows error codes](/api-error-codes), these endpoints return:

| Code                   | Status | Meaning                                                                                                                             |
| ---------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_argument`     | `400`  | The request body is missing or malformed. `detail` says what, and `reason` carries a machine-readable tag such as `invalid_domain`. |
| `not_found`            | `404`  | This account has never submitted that domain.                                                                                       |
| `in_progress`          | `409`  | That domain already has a preparation running, possibly someone else's. Poll its status instead of resubmitting.                    |
| `idempotency_conflict` | `409`  | The same `Idempotency-Key` was reused with a different body.                                                                        |
| `out_of_allowance`     | `429`  | No preparation slots left this month. Check `GET /v1/extract/prepared-domains/allowance`.                                           |

An invalid submission is rejected before it costs anything:

```json Response theme={"dark"}
{
  "code": "invalid_argument",
  "detail": "Domain is required.",
  "reason": "invalid_domain",
  "status": 400,
  "title": "Invalid argument (invalid_argument)",
  "type": "https://docs.zenrows.com/api-error-codes#invalid_argument"
}
```
