# Batches

> POST /v1/batches runs many Soldgraph searches in one call. Poll one batch ID for every result, get a price summary per search, or download all rows as CSV.

`POST https://api.soldgraph.com/v1/batches`

Send many searches in one call and get one batch ID back. Use it to price a whole list of items.

A batch is a list of ordinary searches. Each one is cached, charged and refunded exactly as if you had sent it by itself.

## Create a batch

```bash title="Create a batch"
curl -X POST "https://api.soldgraph.com/v1/batches" \
  -H "Authorization: Bearer $SOLDGRAPH_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: gpu-prices-2026-10-05" \
  -d '{
    "defaults": { "endpoint": "ebay/sold", "country": "uk", "condition": "used", "count": 120 },
    "searches": [
      { "q": "RX 580 4GB" },
      { "q": "GTX 1660 Ti" },
      { "q": "Ryzen 7 5800X", "count": 60 }
    ]
  }'
```

- `searches` is the list. Each search needs an `endpoint`, like `ebay/sold` or `mercari/sold`, plus the same inputs that route takes as a `GET`.
- `defaults` are inputs applied to every search. A search can override any of them. A default must be valid for every search it reaches, so keep route-specific inputs like `count` out of `defaults` when the batch mixes marketplaces.
- Any route works, including the one-listing lookups: `tcgplayer/sales`, `offerup/item`, `facebook/item` and `vinted/item`. Their `q` is an ID, and it comes back in its normalized form.
- IDs and ZIP codes may be sent as JSON numbers or strings. Send a ZIP code with a leading zero as a string.
- `pages` is an eBay shorthand. `{ "q": "RX 580", "pages": 3 }` becomes three searches, for pages 1, 2 and 3. The most is 10. Don't send `page` in the same search. A page past the last one comes back `complete` with no rows and costs nothing. A bigger `count` usually gets the same rows in fewer searches.
- Send `Content-Type: application/json`. The body can be up to 256 KB.

The answer is **202** with the batch, or **200** when every search was already cached. The `Location` header holds the batch's URL.

If any search is invalid, the whole batch is refused with **422** and nothing is charged. `error.index` says which search to fix, and `error.param` says which input:

```json title="One bad search"
{ "error": { "code": "unsupported_or_invalid_query", "index": 2, "param": "count" } }
```

## Get the results

```bash title="Poll a batch"
curl "https://api.soldgraph.com/v1/batches/BATCH_ID?wait=20" \
  -H "Authorization: Bearer $SOLDGRAPH_KEY"
```

`wait` holds the call open for up to 20 seconds and answers as soon as every search has settled. Poll again while `status` is `pending`.

```json title="A finished batch"
{
  "batch_id": "00000000-0000-4000-8000-000000000002",
  "status": "complete",
  "created_at": "2026-10-05T14:02:11+00:00",
  "counts": { "pending": 0, "complete": 2, "failed": 0, "rejected": 1 },
  "credits": 2,
  "items": [
    {
      "index": 0, "endpoint": "ebay/sold", "q": "RX 580 4GB",
      "request_id": "00000000-0000-4000-8000-000000000003",
      "status": "complete", "cached": false, "credits": 1, "rows": 120,
      "summary": { "priced_count": 120, "currency": "GBP", "median": 57.9, "low": 16.15, "high": 249.99,
        "earliest_sold_date": "2026-09-16", "latest_sold_date": "2026-10-04", "best_offer_accepted_count": 20 }
    },
    { "index": 1, "endpoint": "ebay/sold", "q": "GTX 1660 Ti",
      "request_id": "00000000-0000-4000-8000-000000000004",
      "status": "complete", "cached": true, "credits": 1, "rows": 96, "summary": { "priced_count": 96, "currency": "GBP", "median": 74.5, "low": 30, "high": 140, "earliest_sold_date": "2026-09-20", "latest_sold_date": "2026-10-04", "best_offer_accepted_count": 11 } },
    { "index": 2, "endpoint": "ebay/sold", "q": "Ryzen 7 5800X",
      "request_id": null, "status": "rejected", "cached": false, "credits": 0,
      "error": { "code": "too_many_pending" } }
  ]
}
```

Each search has one of four states:

| `status` | Meaning | Cost |
| --- | --- | --- |
| `pending` | Still running. | Holds 1 request until it settles. |
| `complete` | Done. `rows` and `summary` are filled in. Lookups of one listing have no summary. | 1 request |
| `failed` | The search ran and failed. `error.code` is a [job error](https://soldgraph.com/docs/api/errors.md#failed-job-codes). | 0 |
| `rejected` | The search was not accepted, for example because your allowance ran out. `error.code` says why. | 0 |

- By default you get each search's `summary`, not its rows. That keeps a big batch small.
- Add `include=results` to get every search's full `result` in the same answer.
- Each `request_id` is an ordinary job. You can read one search at `/v1/jobs/REQUEST_ID`.
- `index` is the position after `pages` is expanded.
- A search with `error.code` of `not_submitted` was cut off before it was sent. Send the same batch again with the same `Idempotency-Key` and only the missing searches run.

## Download as CSV

```bash title="All rows as one file"
curl "https://api.soldgraph.com/v1/batches/BATCH_ID?format=csv" \
  -H "Authorization: Bearer $SOLDGRAPH_KEY" -o prices.csv
```

- The file has one line per listing from every complete search. The first three columns are `search_index`, `q` and `endpoint`.
- Prices become two columns, like `displayed_price_amount` and `displayed_price_currency`. Both are always present, even when every value is empty.
- A route's columns are the same from file to file. Find a column by its header, not its position. Nested values, like a location, are written as JSON text.
- A batch that mixes routes has every route's columns, with blanks where a row has no such field.
- CSV is ready once nothing is pending. Before that you get `409 result_not_ready`.
- One job works the same way: `/v1/jobs/REQUEST_ID?format=csv`. That file has no `search_index`, `q` or `endpoint` columns.
- Text that starts with `=`, `+`, `-` or `@` gets a leading `'`, so a spreadsheet shows it as text.

## Limits and cost

- **Size.** A batch can hold as many searches as your plan allows pending at once: 10 on Free, 15 on Lite, 25 on Starter, 50 on Pro, 100 on Business and 200 on Scale. `pages` counts as one search per page. A bigger batch returns `422 batch_too_large`.
- **Cost.** One request per search that completes, cache hits and searches with no rows included. Failed and rejected searches are free, and so is a page past the last one.
- **Rate limit.** Creating a batch is one API call toward your 60 per minute, however many searches it holds. Each poll is one call.
- **Pending cap.** Searches in a batch count toward your pending cap like any others. If you already have searches pending, some of the batch can come back `rejected` with `too_many_pending`. Send those again once the rest finish.
- **Retries.** Send an `Idempotency-Key`. A retry with the same key and body returns the same batch. The same key with a different body returns `409 idempotency_conflict`.
