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

# Purchase residuals

> Buy one cell of a finished scrape's held-back businesses.

Takes credits: buys every business in one cell of a finished scrape's held-back inventory (businesses it found and did not deliver) and delivers them as a new completed scrape. Price it first with list_scrape_offers, which is free and lists every cell with its count and its price in credits. Name the cell exactly as the offer does. The cell is recounted when you buy, and maxCredits must cover its price then, or nothing is charged and the call answers quote_exceeded. A cell already bought answers already_purchased, and one that holds nothing any more answers no_offer. Needs a key with the spend scope.

`POST https://api.leadity.io/v1/scrapes/{id}/residuals`

- Scope: `spend`
- Spends credits: yes
- MCP tool: `purchase_residuals`

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

## Path parameters

- `id`, string, required. The scrape's id, as create_scrape and list_scrapes answer it. 1 to 64 characters, matches ^[A-Za-z0-9]{1,64}$.

## Body parameters

- `phoneType`, string, required. The phone bucket: a line type, with_phone, no_phone or any. One of: `any`, `mobile`, `landline`, `voip`, `toll_free`, `with_phone`, `no_phone`.
- `website`, string, required. Whether the business has a website: has, none or any. One of: `any`, `has`, `none`.
- `email`, string, required. The email bucket: none, unverified, verified or any. One of: `any`, `none`, `unverified`, `verified`.
- `niche`, string, required. The Google category class: match (its main category is an ordered niche), other, unknown, any, or category for one named category beside it. One of: `any`, `match`, `other`, `unknown`, `category`.
- `category`, string, optional. The key of one named category from the offer's categories. Present exactly when niche is category. 1 to 200 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/Vb7nW3xQ9k/residuals" \
  -H "Authorization: Bearer $LEADITY_API_KEY" \
  -H "Idempotency-Key: order-2026-10-04-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "phoneType": "landline",
    "website": "has",
    "email": "any",
    "niche": "match",
    "maxCredits": 449
  }'
```

```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/Vb7nW3xQ9k/residuals", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LEADITY_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "phoneType": "landline",
    "website": "has",
    "email": "any",
    "niche": "match",
    "maxCredits": 449
  }),
});

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/Vb7nW3xQ9k/residuals",
    headers={
        "Authorization": f"Bearer {os.environ['LEADITY_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "phoneType": "landline",
        "website": "has",
        "email": "any",
        "niche": "match",
        "maxCredits": 449,
    },
)

print(response.json())
```

## Response

- `scrapeId`, string, required. The new completed scrape holding the businesses bought. Read it with list_leads.
- `count`, integer, required. Businesses bought.
- `credits`, integer, required. Credits charged.

## Example response

```json 201
{
  "scrapeId": "Hn4kR9wT2x",
  "count": 642,
  "credits": 449
}
```

## 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). |
| `not_found` | 404 | No such resource on this account. | `id`: On export_scrapes: the first id that is not one of the account's scrapes. |
| `paywall_required` | 402 | This operation needs a purchase on the account first. | - |
| `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. | - |
| `no_offer` | 409 | The scrape has nothing of that kind to sell, or it is still running. Read list_scrape_offers. | - |
| `already_purchased` | 409 | This upgrade was already bought on this scrape. | - |
| `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. |
