# Batches

`POST https://api.serpkite.com/v1/batches` · Credits: Half the realtime price; polling is free

Queue up to 100 requests for one endpoint at half price, then poll `GET /v1/batches/{id}` or get a signed webhook.

Send one `endpoint` and 1–100 `requests` (the same bodies the realtime endpoint takes). The answer is `202 Accepted` with `batches`: one entry per request, in order, each either a batch job or an error object (for example a request that failed validation). Each job is billed at 0.5× the realtime price, and failed jobs are refunded.

Poll `GET /v1/batches/{id}` (free) with a job `id`, or follow its `poll_url`, until `status` is `done` or `failed`. Or pass `webhook_url` (or set an account webhook) and we POST each finished job, signed with `X-SerpKite-Signature`. See [Batch requests](https://serpkite.com/docs/batch) and [Webhooks](https://serpkite.com/docs/webhooks).

## Request body

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `endpoint` **required** | string |  | Endpoint every request runs against. One of: `search`, `images`, `videos`, `news`, `maps`, `places`, `reviews`, `shopping`, `scholar`, `patents`, `autocomplete`, `lens`, `webpage`. |
| `requests` **required** | array |  | 1–100 request objects with the same fields as the realtime endpoint (e.g. `{"q":"…","country":"de"}`). |
| `webhook_url` | string |  | Where each finished job is POSTed (event `batch.completed`). Defaults to the account webhook set in the dashboard. |

## Example request

cURL:

```bash
curl https://api.serpkite.com/v1/batches \
  -H "Authorization: Bearer $SERPKITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endpoint":"search","requests":[{"q":"best espresso machine","country":"us"},{"q":"best espresso machine","country":"de","language":"de"}],"webhook_url":"https://example.com/hooks/serpkite"}'

# then poll a job (free)
curl https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90 \
  -H "Authorization: Bearer $SERPKITE_API_KEY"
```

TypeScript:

```ts
import { SerpKite } from "serpkite";

const sk = new SerpKite();
const { batches } = await sk.batches.create({
  endpoint: "search",
  requests: [
    { q: "best espresso machine", country: "us" },
    { q: "best espresso machine", country: "de", language: "de" },
  ],
});
const done = await sk.batches.wait(batches[0].id); // polls until done or failed
console.log(done.status, done.result?.results[0].title);
```

Python:

```python
from serpkite import SerpKite

sk = SerpKite()
job = sk.batches.create(
    endpoint="search",
    requests=[
        {"q": "best espresso machine", "country": "us"},
        {"q": "best espresso machine", "country": "de", "language": "de"},
    ],
)
done = sk.batches.wait(job.batches[0].id)  # polls until done or failed
print(done.status, done.credits_used)
```

## Response fields

| Field | Type | Description |
| --- | --- | --- |
| `batches[]` | array | `202` response: one entry per request, in order. Each is a job (fields below) or `{"error":{"code","message"}}`. |
| `id` | string | Job ID (UUID). |
| `status` | string | `queued`, `running`, `done` or `failed`. |
| `endpoint` | string | The endpoint the job runs, e.g. `/v1/search`. |
| `created_at` | string | ISO 8601 timestamp. |
| `completed_at` | string \| null | When the job finished. |
| `credits_used` | number | Credits charged (0.5× the realtime price; 0 if failed). |
| `poll_url` | string | `https://api.serpkite.com/v1/batches/<id>`. |
| `webhook_url` | string \| null | Where the result is POSTed. |
| `webhook_status` | string \| null | `pending`, `delivered` or `failed`. |
| `error` | object \| null | `code` and `message` when `status` is `failed`. |
| `result` | object \| null | The same body the realtime endpoint returns (`request`, `results`, …, `meta`). Present when done; kept for 24 hours. |

## Example response

```json
{
  "id": "0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90",
  "status": "done",
  "endpoint": "/v1/search",
  "created_at": "2026-09-29T08:00:00Z",
  "completed_at": "2026-09-29T08:03:12Z",
  "credits_used": 0.5,
  "poll_url": "https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90",
  "webhook_url": null,
  "webhook_status": null,
  "error": null,
  "result": {
    "request": {
      "endpoint": "search",
      "engine": "google",
      "q": "best espresso machine",
      "country": "us",
      "language": "en"
    },
    "results": [
      "…"
    ],
    "related_searches": [],
    "meta": {
      "request_id": "req_01J8ZK4M6Q2V7",
      "credits_used": 0.5,
      "cached": false,
      "engine": "google",
      "latency_ms": 1034,
      "parse_quality": "ok",
      "resolved_urls": true
    }
  }
}
```

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | A parameter is missing or invalid. |
| 401 | `unauthorized` | The API key is missing, invalid or revoked. |
| 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. |
| 404 | `not_found` | The batch job does not exist, belongs to another account, or its result has expired. |
| 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. |
| 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. |

All errors: https://serpkite.com/docs/errors

## Notes

- `GET /v1/batches/{id}` is free. Poll every few seconds at most; jobs target completion within 15 minutes.
- Realtime endpoints reject JSON arrays; send many queries through this endpoint instead.
- The official SDKs wrap both calls: `sk.batches.create(…)` and `sk.batches.wait(id)`.

## Related

- [Batch requests](https://serpkite.com/docs/batch)
- [Webhooks](https://serpkite.com/docs/webhooks)
- [Official SDKs](https://serpkite.com/docs/sdks)