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

# Submit tasks in bulk: batches and location grids

> Choose between the 500-task batch, the 10,000-task batch object and the location grid, then submit and read results.

Three endpoints create many tasks in one request. Each one creates ordinary tasks: polling, webhooks, credits and plan limits are the same as for a single task.

## Compare the three endpoints

| | `POST /v1/async/task/batch` | `POST /v1/batches` | `POST /v1/async/task/grid` |
| - | - | - | - |
| What you send | A JSON array of task requests | `{ items, webhook?, metadata?, idempotencyKey? }` | One task request plus `grid` |
| Max tasks | 500 | 10,000 | 81 (a 9 × 9 grid) |
| Max body | 8 MiB | 32 MiB | 1 MiB |
| Success status | `200` | `202` created, `200` for a repeated `idempotencyKey` | `200` |
| Validation | Each item alone; bad items fail in place | All items first; one bad item rejects the whole batch with `400` and nothing is stored | The center point first; if it is refused, the whole grid is refused |
| Over your plan limit | Extra items fail with `PLAN_CONCURRENCY_LIMIT`; you resubmit them | Items wait on the server and are released as room frees up | Extra points fail in place |
| Progress | Poll each task | `GET /v1/batches/{id}` counts | `GET /v1/async/task/grid/{id}` |
| Completion signal | One webhook per task | One webhook per task, then `batch.completed` | One webhook per task |

## Which one to use

* **Up to 500 tasks, and you handle retries:** `POST /v1/async/task/batch`. You get every task id in the response.
* **More tasks than your plan's concurrency, or a daily job of thousands:** `POST /v1/batches`. The server holds the items and feeds them in as your [plan limit](/concepts/credits-and-limits.md#plan-concurrency) and each engine's queue allow, so you do not retry `429`s or `503`s yourself.
* **One prompt asked from many points around a place:** `POST /v1/async/task/grid`.

## `POST /v1/async/task/batch`

Send an array of up to 500 task requests ([reference](/api-reference/create-task-batch.md)). Engines can be mixed.

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/async/task/batch \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    { "taskType": "CHATGPT", "idempotencyKey": "job-42:p1:chatgpt", "payload": { "prompt": "best CRM for a small team", "country": "US" } },
    { "taskType": "GEMINI", "idempotencyKey": "job-42:p1:gemini", "payload": { "prompt": "best CRM for a small team", "country": "US" } }
  ]'
```

A well-formed request returns `200`. `results[i].index` matches the position in your array, and each result has either `task` or `error`:

```json theme={"theme":"css-variables"}
{
  "success": true,
  "summary": { "total": 2, "succeeded": 1, "failed": 1 },
  "results": [
    { "success": true, "index": 0, "task": { "id": "8f2c1e40-...", "status": "QUEUED", "...": "..." }, "credits": { "creditsToCharge": 0, "creditsCharged": 0 } },
    {
      "success": false,
      "index": 1,
      "error": {
        "code": "PLAN_CONCURRENCY_LIMIT",
        "message": "free plan allows 2 concurrent tasks; 2 already active",
        "details": { "plan": "free", "limit": 2, "source": "plan", "active": 2, "retryAfterSec": 5 },
        "timestamp": "2026-10-19T04:12:00.000Z"
      }
    }
  ]
}
```

The whole request is refused, with no task created, in these cases:

| Status | Code | When |
| - | - | - |
| `400` | `VALIDATION_ERROR` | Not valid JSON, not an array, empty, or over 500 items |
| `413` | `PAYLOAD_TOO_LARGE` | Body over 8 MiB |
| `503` | `SERVER_OVERLOADED` | Admission is saturated. Retry after `Retry-After`. |
| `503` | `SERVER_OVERLOADED` | The global queue is full. Retry after `Retry-After`. |

A repeated `idempotencyKey` returns the existing task as a success. See [Idempotency](/concepts/idempotency.md).

## `POST /v1/batches`

Send up to 10,000 items ([reference](/api-reference/create-batch.md)). Each item takes the task fields `taskType`, `payload`, `priority` and `idempotencyKey`, plus an optional `customId` (up to 128 characters) that is echoed in results. Set `webhook.url` on the batch, not on items.

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/batches \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotencyKey": "daily-2026-10-19",
    "webhook": { "url": "https://hooks.example.com/querying" },
    "metadata": { "run": "daily" },
    "items": [
      { "customId": "crm-chatgpt-us", "taskType": "CHATGPT", "payload": { "prompt": "best CRM for a small team", "country": "US" } },
      { "customId": "crm-gemini-us", "taskType": "GEMINI", "payload": { "prompt": "best CRM for a small team", "country": "US" } }
    ]
  }'
```

The response is `202` with the batch. The first items are already submitted:

```json theme={"theme":"css-variables"}
{
  "success": true,
  "batch": {
    "id": "3f6b2a10-...",
    "status": "OPEN",
    "total": 2,
    "counts": { "pending": 0, "queued": 2, "processing": 0, "completed": 0, "failed": 0, "rejected": 0 },
    "createdAt": "2026-10-19T09:00:00.000Z",
    "completedAt": null,
    "idempotencyKey": "daily-2026-10-19",
    "webhook": { "url": "https://hooks.example.com/querying" },
    "metadata": { "run": "daily" }
  }
}
```

### Envelope rules

| Field | Rule |
| - | - |
| `items` | 1 to 10,000 objects |
| `webhook.url` | Same [URL rules](/concepts/webhooks.md#url-rules) as a task. Applies to every item. |
| `metadata` | An object of at most 8 KB as JSON, returned unchanged |
| `idempotencyKey` | 1 to 256 characters. A repeat returns the first batch with `200`. |
| Item `idempotencyKey` | Must be unique inside the batch |
| Item `webhook` | Rejected. Use the batch's `webhook.url`. |

### All-or-nothing validation

Every item runs the same checks as a single task before anything is stored: fields, key scope, private engines, dedicated endpoints, region and `payload.location`. If any item fails, the response is `400 VALIDATION_ERROR`, nothing is stored, and `details` lists up to 50 failing items:

```json theme={"theme":"css-variables"}
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "1 of 2 items are invalid; the batch was not stored",
    "details": {
      "invalidItems": 1,
      "items": [
        { "index": 1, "code": "VALIDATION_ERROR", "message": "Request validation failed", "details": [{ "field": "payload.prompt", "message": "payload.prompt or payload.query is required" }] }
      ]
    },
    "timestamp": "2026-10-19T09:00:00.000Z"
  }
}
```

### How items run

The batch releases items as tasks when your plan limit and the engine's queue have room. Accounts take turns, so a large batch does not hold up a small one from another account. Each released item goes through the same admission as a single task: it places a credit hold and counts against the key's daily credit limit.

When an item is refused, the code decides what happens:

| Code | Item |
| - | - |
| `PLAN_CONCURRENCY_LIMIT`, `KEY_DAILY_CREDIT_LIMIT`, `SERVER_OVERLOADED`, `DB_UNAVAILABLE`, `ENQUEUE_ERROR` | Stays `PENDING` and is tried again later |
| `QUEUE_BACKPRESSURE`, `ENGINE_UNAVAILABLE` | Stays `PENDING` and is tried again later |
| `API_KEY_INACTIVE` | `REJECTED`: the batch's key was revoked or expired, or its sub-account was suspended |
| Any other code, including `INSUFFICIENT_CREDITS` | `REJECTED` |

The batch becomes `COMPLETED` when no item is pending and every task has finished. With `webhook.url`, the server then posts the [`batch.completed` event](/concepts/webhooks#batch-completion-event).

### Read progress and results

`GET /v1/batches/{id}` returns the batch with `counts` by item status: `pending`, `queued`, `processing`, `completed`, `failed`, `rejected`. Polling it also moves pending items along.

```bash theme={"theme":"css-variables"}
curl https://api.querying.ai/v1/batches/3f6b2a10-... \
  -H "Authorization: Bearer $QUERYING_API_KEY"
```

`GET /v1/batches/{id}/items` lists items in submission order:

```bash theme={"theme":"css-variables"}
curl "https://api.querying.ai/v1/batches/3f6b2a10-.../items?status=completed&include=response&limit=20" \
  -H "Authorization: Bearer $QUERYING_API_KEY"
```

| Query | Description |
| - | - |
| `status` | One of `pending`, `queued`, `processing`, `completed`, `failed`, `rejected` |
| `include=response` | Adds each finished task's `response` or `error` |
| `limit` | Default 100, max 1,000. With `include=response`: default 20, max 100. |
| `cursor` | The `nextCursor` of the previous page |

```json theme={"theme":"css-variables"}
{
  "success": true,
  "items": [
    {
      "index": 0,
      "customId": "crm-chatgpt-us",
      "taskType": "CHATGPT",
      "status": "COMPLETED",
      "taskId": "8f2c1e40-...",
      "completedAt": "2026-10-19T09:01:40.000Z",
      "response": { "text": "...", "sources": [] }
    }
  ],
  "nextCursor": null
}
```

A `REJECTED` item carries `error: { code, message }`. Result bodies are included only within the 24-hour polling window of [Async tasks](/concepts/async-tasks.md#retention). See also [Get a batch](/api-reference/get-batch.md) and [List batch items](/api-reference/list-batch-items.md).

## `POST /v1/async/task/grid`

Ask one prompt from every point of a square grid around a center ([reference](/api-reference/create-task-grid.md)). Each point becomes a task with that point as `payload.location`, so the engine must take a location: `CHATGPT`, `GEMINI`, `GOOGLE`, `AIMODE` or `GOOGLE_SERP`. Do not send `payload.location` yourself.

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/async/task/grid \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "taskType": "CHATGPT",
    "idempotencyKey": "soho-coffee-2026-10-19",
    "payload": { "prompt": "best coffee shops near me for working on a laptop", "country": "US" },
    "grid": { "center": { "latitude": 40.72288, "longitude": -73.99875 }, "size": 3, "radiusMeters": 1500 }
  }'
```

| Field | Rule |
| - | - |
| `grid.size` | `3`, `5`, `7` or `9` points per side (9 to 81 tasks) |
| `grid.radiusMeters` | 100 to 50,000: the distance from the center to the middle of each edge. Points are `2 × radius ÷ (size − 1)` apart. |
| `grid.center` | Latitude and longitude in decimal degrees. The grid must stay below 89 degrees of latitude. |
| `idempotencyKey` | Optional, 1 to 200 characters. It becomes the grid id. |
| `priority`, `webhook` | Applied to every point |

Row 0 is the north edge and column 0 the west edge. The response returns `grid` (with `id` and `spacingMeters`), a `summary`, and `points` in row order, each with `row`, `col`, `location` and either `task` or `error`.

The center point is admitted first, alone. If it is refused (for example `422 LOCATION_UNSUPPORTED`, `403 KEY_SCOPE_DENIED`, `402 INSUFFICIENT_CREDITS` or a full plan), that error is the response for the whole grid and no task is created. Points after the center that are refused for capacity fail in place with `success: false`.

Read a grid back with [`GET /v1/async/task/grid/{id}`](/api-reference/get-task-grid). Add `?include=response` for finished tasks' results. Points that were never admitted are absent, and another account's grid id returns `404`. Resending the same grid under the same id admits only the missing points; see [Idempotency](/concepts/idempotency.md#location-grids).

## Next steps

> **[Idempotency](/concepts/idempotency.md)**
>
> How each endpoint treats a repeated key.

> **[Credits and limits](/concepts/credits-and-limits.md)**
>
> Plan concurrency and pacing.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.