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

# Estimate scrape

> Quote an order for free: what is still new, the per-lead rate and the floors.

Prices a scrape order without placing it and takes no credits. It answers how much of the selection is still unswept for each niche, an estimate of the leads it can yield, the exact per-lead rate (the price of any lead count is ceil(leads x leadRateMilli / 1000) credits), and the account's floors: an order needs at least minWorkItems new searches, at least minTargetLeads leads, and leads costing at least minOrderCredits. It also echoes the filters as the order would really run them: websitePresence none steps emailFinder down to off, and findOwnerBlockedBy names any niche that refuses the owner lookup. Call it before create_scrape and set maxCredits from it.

`POST https://api.leadity.io/v1/scrapes/estimate`

- Scope: `read`
- Spends credits: no
- MCP tool: `estimate_scrape`

## Headers

- `Authorization`, string, required. Bearer, a space, then an API key: Bearer lk_api_... An MCP key is refused here with wrong_key_kind.
- `Content-Type`, string, required. application/json

## Body parameters

- `nicheIds`, string[], required. One to ten niche ids from list_niches. The plan draws across them at random. 1 to 10 items, each 1 to 100 characters.
- `selection`, object, required. Where to search: whole regions, single ZIP or postal codes, or both. A code already swept for a niche is skipped for that niche.
  - `states`, object[], optional. Whole regions (a state, province, territory or UK region), every area in each. list_territories names them. At most 200 items.
    - `country`, string, required. The two-letter country code, e.g. US. list_territories names the countries on sale. Matches ^[A-Z]{2}$.
    - `state`, string, required. The region code as list_territories names it, e.g. TX. 1 to 8 characters.
  - `zips`, object[], optional. Single ZIP or postal codes. list_areas and search_areas name them. At most 60,000 items.
    - `country`, string, required. The two-letter country code, e.g. US. list_territories names the countries on sale. Matches ^[A-Z]{2}$.
    - `state`, string, optional. The region the code sits in, when known. 1 to 8 characters.
    - `code`, string, required. One ZIP or postal code exactly as list_areas names it, e.g. 78701. 1 to 16 characters.
- `filters`, object, required. The delivery filters, each independent of the others. app-api validates this object, so a bad value answers invalid_body. Today: websitePresence (required: any, has or none); phoneTypeFilter (off, with_phone, mobile, landline or voip); findOwner (true delivers only businesses with a confirmed owner name); emailFinder (off, require or require_verified, which deliver only businesses whose own site publishes an address, verified for the second; stepped down to off when websitePresence is none); nicheMatch (true delivers only businesses whose own Google main category is an ordered niche); minGoogleReviews (0 or one of the floors get_pricing lists); reviews ({ minReviews, maxReviews }: walk each business's Google reviews, charged per review). Absent fields are off. The full and current list, with prices, is on the filters page of the Leadity docs; get_scrape_defaults answers the account's own usual set.

## Example request

```bash cURL
curl -X POST "https://api.leadity.io/v1/scrapes/estimate" \
  -H "Authorization: Bearer $LEADITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nicheIds": [
      "roofing_contractor"
    ],
    "selection": {
      "states": [
        {
          "country": "US",
          "state": "TX"
        }
      ]
    },
    "filters": {
      "websitePresence": "any",
      "phoneTypeFilter": "mobile"
    }
  }'
```

```javascript Node.js
const response = await fetch("https://api.leadity.io/v1/scrapes/estimate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LEADITY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "nicheIds": [
      "roofing_contractor"
    ],
    "selection": {
      "states": [
        {
          "country": "US",
          "state": "TX"
        }
      ]
    },
    "filters": {
      "websitePresence": "any",
      "phoneTypeFilter": "mobile"
    }
  }),
});

const data = await response.json();
console.log(data);
```

```python Python
import os

import requests

response = requests.post(
    "https://api.leadity.io/v1/scrapes/estimate",
    headers={
        "Authorization": f"Bearer {os.environ['LEADITY_API_KEY']}",
    },
    json={
        "nicheIds": ["roofing_contractor"],
        "selection": {
            "states": [
                {"country": "US", "state": "TX"},
            ],
        },
        "filters": {"websitePresence": "any", "phoneTypeFilter": "mobile"},
    },
)

print(response.json())
```

## Response

- `totalAreas`, integer, required. ZIP or postal codes the selection names.
- `newAreas`, integer, required. Codes still unswept for at least one niche.
- `coveredAreas`, integer, required. Codes already swept for every niche. Swept ground is never swept again.
- `inProgressAreas`, integer, required. Codes held by another live scrape of this account. They free up when it ends.
- `newWorkItems`, integer, required. Searches the order would run: each niche times each code still new for it.
- `minWorkItems`, integer, required. The fewest searches an order may run.
- `maxWorkItems`, integer, required. The most searches one order may run.
- `minTargetLeads`, integer, required. The smallest targetLeads this account may order.
- `minOrderCredits`, integer, required. The least the leads of an order may cost, in credits.
- `niches`, integer, required. How many niches were priced.
- `estimatedLeads`, integer, required. A rough estimate of the leads the new searches can yield. Not a promise.
- `estimatedCredits`, integer, required. What estimatedLeads would cost.
- `leadRateMilli`, integer, required. The price of one new lead in thousandths of a credit, at these filters and the dearest niche selected.
- `phoneTypeFilter`, string, required. The phone filter priced.
- `findOwner`, boolean, required. Whether the owner lookup was priced.
- `websitePresence`, string, required. The website filter the order would run.
- `websiteLockedBy`, object[], required. Niches that forced a different website filter. Empty today.
  - `id`, string, required.
  - `name`, string, required.
- `findOwnerBlockedBy`, object[], required. Niches that refuse the owner lookup. With findOwner on, an order naming any of them is refused.
  - `id`, string, required.
  - `name`, string, required.
- `emailFinder`, string, required. The email filter the order would run, after the no-website step-down.

## Example response

```json 200
{
  "totalAreas": 1935,
  "newAreas": 1523,
  "coveredAreas": 412,
  "inProgressAreas": 0,
  "newWorkItems": 1523,
  "minWorkItems": 20,
  "maxWorkItems": 200000,
  "minTargetLeads": 20,
  "minOrderCredits": 20,
  "niches": 1,
  "estimatedLeads": 9138,
  "estimatedCredits": 9138,
  "leadRateMilli": 1000,
  "phoneTypeFilter": "mobile",
  "findOwner": false,
  "websitePresence": "any",
  "websiteLockedBy": [],
  "findOwnerBlockedBy": [],
  "emailFinder": "off"
}
```

## Errors

Every refusal answers `{ error, message }` with the status below. The full list is on the [Errors](https://docs.leadity.io/errors) page.

| Code | Status | Meaning | Extra fields |
| --- | --- | --- | --- |
| `unauthorized` | 401 | The API key is missing, malformed, revoked or expired. | - |
| `wrong_key_kind` | 401 | This key opens the other surface: an API key works on the HTTP API, an MCP key on the MCP server. | - |
| `account_suspended` | 403 | The account is suspended. | - |
| `email_not_verified` | 403 | The account's email address is not verified yet. | - |
| `invalid_body` | 400 | The request does not match the operation's schema. | `issues`: The problems found, each with path (the field as a dotted path, led by the part of the request it travels in: path, query or body, such as body.items.1) and message (what is wrong there). |
| `rate_limited` | 429 | Too many requests. Wait for the Retry-After seconds and try again. | `retryAfterSeconds`: The seconds to wait, the same number as the Retry-After header. |
| `try_again` | 503 | Leadity could not answer just now. Retry in a moment, with the same Idempotency-Key on a spending call. | `retryAfterSeconds`: When a request with this Idempotency-Key is still being processed: the seconds to wait before retrying with the same key, as the Retry-After header says. |
| `internal_error` | 500 | Something failed on our side. Nothing was charged twice. | `body`: On the MCP server, when Leadity answered in a shape the tool does not publish: that answer as it came. |
