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

# Create scrape

> Order a scrape: find businesses live on Google Maps and deliver them as leads.

Takes credits: orders a scrape that searches Google Maps live in the selected areas and delivers up to targetLeads businesses as leads, holding their price up front. Price it first with estimate_scrape, which is free and answers the per-lead rate, the account's floors and how much of the selection is still unswept. The hold is ceil(targetLeads x leadRateMilli / 1000) credits for the leads and, when review scraping is ordered, ceil(targetLeads x maxReviews x review) credits beside it, review being the per-review rate get_pricing names: each part is rounded up to a whole credit on its own, so set maxCredits to the two together. estimate_scrape answers the per-lead rate and never the reviews part, and a quote_exceeded refusal charges nothing and names the whole hold in credits. Whatever the run does not deliver is refunded when it settles, and a business the account already holds, in any niche, is skipped and never charged. The call answers at once and the scrape runs for minutes to hours: watch it with get_scrape_status every 30 to 60 seconds, never faster, until complete is true, then read the leads with list_leads or download_leads. Needs a key with the spend scope. A refusal names its reason in error, for example below_minimum, all_covered, insufficient_credits or quote_exceeded.

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

- Scope: `spend`
- Spends credits: yes
- MCP tool: `create_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.
- `Idempotency-Key`, string, required. Your own key for this one purchase. Send the same key again to retry it safely: the retry replays the first answer instead of charging twice, and carries Idempotent-Replayed: true. The same key on a different request answers idempotency_conflict, and a spend without one answers idempotency_key_required. 16 to 64 characters, matches ^[A-Za-z0-9_-]+$.
- `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.
- `targetLeads`, integer, required. How many leads to deliver at most. At least the minTargetLeads estimate_scrape answers. At most 1,000,000, greater than 0.
- `title`, string, optional. A name for the scrape. Left out, one is derived from its niches and areas. At most 100 characters.
- `maxCredits`, integer, required. The most this call may take, in whole credits. If the price is higher nothing is charged and the call answers quote_exceeded. At most 100,000,000, greater than 0.

## Example request

```bash cURL
curl -X POST "https://api.leadity.io/v1/scrapes" \
  -H "Authorization: Bearer $LEADITY_API_KEY" \
  -H "Idempotency-Key: order-2026-10-04-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "nicheIds": [
      "roofing_contractor"
    ],
    "selection": {
      "states": [
        {
          "country": "US",
          "state": "TX"
        }
      ]
    },
    "filters": {
      "websitePresence": "any",
      "phoneTypeFilter": "mobile"
    },
    "targetLeads": 1000,
    "title": "TX roofers, mobiles",
    "maxCredits": 1000
  }'
```

```javascript Node.js
// Make the key once per purchase and reuse it on every retry.
const idempotencyKey = crypto.randomUUID();

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

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

```python Python
import os
import uuid

import requests

# Make the key once per purchase and reuse it on every retry.
idempotency_key = str(uuid.uuid4())

response = requests.post(
    "https://api.leadity.io/v1/scrapes",
    headers={
        "Authorization": f"Bearer {os.environ['LEADITY_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "nicheIds": ["roofing_contractor"],
        "selection": {
            "states": [
                {"country": "US", "state": "TX"},
            ],
        },
        "filters": {"websitePresence": "any", "phoneTypeFilter": "mobile"},
        "targetLeads": 1000,
        "title": "TX roofers, mobiles",
        "maxCredits": 1000,
    },
)

print(response.json())
```

## Response

- `id`, string, required. The new scrape's id. Pass it to get_scrape_status to watch it.
- `status`, string, required. reserving, queued and running are live; stopping and settling are finishing; completed is final. One of: `reserving`, `queued`, `running`, `stopping`, `settling`, `completed`.
- `newAreas`, integer, required. Distinct codes the plan searches.
- `newWorkItems`, integer, required. Searches the plan runs.
- `credits`, integer, required. The price of the leads, held now.
- `reviewsReservedCredits`, integer, required. The review-scraping hold beside it. 0 when none was ordered.
- `emailFinder`, string, required. The email filter the scrape really runs, after the no-website step-down.
- `replayed`, boolean, required. True when this idempotency key had already placed the order: nothing new was charged, and this is that order.

## Example response

```json 201
{
  "id": "Vb7nW3xQ9k",
  "status": "queued",
  "newAreas": 1523,
  "newWorkItems": 1523,
  "credits": 1000,
  "reviewsReservedCredits": 0,
  "emailFinder": "off",
  "replayed": false
}
```

## 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. | - |
| `insufficient_scope` | 403 | The key does not carry the scope this operation needs. | `requiredScope`: The scope the operation needs: read, write or spend. |
| `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). |
| `package_locked` | 402 | Until its first purchase the account may order only the starter packages. The body names what was ordered and what is allowed. | `ordered`: The package the order's filters read as, custom for any other set. `allowed`: The packages the account may order now. |
| `insufficient_credits` | 402 | The balance does not cover the price. Nothing was charged. | `required`: On create_validation: the credits the list holds. |
| `quote_exceeded` | 409 | The price is above the maxCredits the call allowed. Nothing was charged. | `credits`: What the call would have taken: the whole hold on an order or a list, the price or the hold on an upgrade. `maxCredits`: The ceiling the call sent. `reviewsReservedCredits`: On create_scrape: the part of credits held for review scraping. `count`: On purchase_phone_reveal, when the count was the cause: how many numbers the reveal would name. `maxNumbers`: On purchase_phone_reveal, when the count was the cause: the maxNumbers the call sent. |
| `idempotency_key_required` | 400 | A spending operation must carry an Idempotency-Key header (16 to 64 characters of A-Z, a-z, 0-9, _ and -). | - |
| `invalid_idempotency_key` | 400 | The Idempotency-Key must be 16 to 64 characters of A-Z, a-z, 0-9, _ and -. | - |
| `idempotency_conflict` | 409 | This Idempotency-Key was already used for a different request. | - |
| `below_minimum` | 400 | The quantity is under the smallest one this operation takes: targetLeads (the body carries min), or an upgrade's target (list_scrape_offers names it). | `min`: On create_scrape: the smallest targetLeads the account may order. |
| `all_covered` | 409 | Every area selected is already swept for these niches. Choose other areas. | - |
| `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. |
