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

# Batch API

> Run large sets of Agent API requests asynchronously from a JSONL file, then download the results.

## Overview

The Batch API runs many Agent API requests asynchronously. You upload a JSONL file of requests, create a batch from it, poll the batch, and download an output file when it finishes.

Use batch for work that does not need an immediate answer, such as evaluations, classification, enrichment, or offline research over a large dataset. Each request runs the full Agent API loop, so built-in tools such as `web_search` and `fetch_url` work inside a batch. Requests that run inside a batch do not count toward your Agent API [request rate limit](/docs/admin/rate-limits-usage-tiers.md). Token rate limits still apply, and the batch retries a request that hits one.

The Batch and Files endpoints follow the shape of OpenAI's Batch API.

> [!NOTE]
>
> The Batch API is available to every API organization, except organizations with zero data retention. Batch keeps inputs and outputs for 30 days.

> [!NOTE]
>
> The examples on this page read your API key from the `PERPLEXITY_API_KEY` environment variable. The Python and TypeScript examples use `client.files` and `client.batches` in the Perplexity SDKs. The cURL examples use `jq`.

Batch files at `/v1/files` are separate from the files an Agent API response can create, which are covered in [Working with files](/docs/agent-api/working-with-files.md).

## Run a batch

**1. Write the input file**

Create a JSONL file: one JSON object per line, with no surrounding array, no commas between lines, and no blank lines. Each line is one request.

```json batch_input.jsonl theme={null}
{"custom_id": "request-1", "method": "POST", "url": "/v1/responses", "body": {"model": "openai/gpt-6-luna", "input": "What is the capital of France? Answer in one word.", "max_output_tokens": 200}}
{"custom_id": "request-2", "method": "POST", "url": "/v1/responses", "body": {"model": "openai/gpt-6-luna", "input": "What is 12 times 12? Answer with the number only.", "max_output_tokens": 200}}
{"custom_id": "request-3", "method": "POST", "url": "/v1/responses", "body": {"model": "openai/gpt-6-luna", "input": "Name the largest planet in the solar system in one word.", "max_output_tokens": 200}}
```

| Field | Rule |
| - | - |
| `custom_id` | Your id for the request, 1 to 64 bytes. It must be unique within the file. Use it to match results to requests. |
| `method` | Must be `POST`. |
| `url` | Must be `/v1/responses`. This is the same Agent API endpoint as [`/v1/agent`](/api-reference/agent-post). |
| `body` | A normal Agent API request body. Every line must use the same `model`. See [Supported requests](#supported-requests). |

Save the file as `batch_input.jsonl`.

**2. Upload the file**

Upload the file to `POST /v1/files` with `purpose=batch`, and save the returned file `id`. The API validates every line before it stores the file.

```python Python theme={null}
from pathlib import Path
from perplexity import Perplexity

client = Perplexity()

input_file = client.files.create(
    file=("batch_input.jsonl", Path("batch_input.jsonl").read_bytes()),
    purpose="batch",
)

print(input_file.id)
```

```typescript Typescript theme={null}
import fs from 'fs';
import Perplexity, { toFile } from '@perplexity-ai/perplexity_ai';

const client = new Perplexity();

const inputFile = await client.files.create({
  file: await toFile(fs.createReadStream('batch_input.jsonl')),
  purpose: 'batch',
});

console.log(inputFile.id);
```

```bash cURL theme={null}
FILE_ID=$(curl -s https://api.perplexity.ai/v1/files \
  -H "Authorization: Bearer $PERPLEXITY_API_KEY" \
  -F purpose=batch \
  -F file=@batch_input.jsonl | tee /dev/stderr | jq -r '.id')
```

```json Response theme={null}
{
  "bytes": 599,
  "created_at": 1791822949,
  "expires_at": 1794414949,
  "filename": "batch_input.jsonl",
  "id": "file_01M4RFGV5DQFAZY29N9C2624A6",
  "object": "file",
  "purpose": "batch"
}
```

If a line is invalid, the upload returns `400` with the first invalid line's number and the reason, and nothing is stored:

```json theme={null}
{"error": {"message": "line 2: body.model must match the first line's model", "type": "invalid_request", "code": 400}}
```

A successful upload checks the file format. A request can still fail when it runs, for example if the model id does not exist. Those requests go to the [error file](#error-file-format).

Files are kept for 30 days. Custom expiry is not supported, so sending `expires_after` returns `400`.

**3. Create the batch**

Create a batch from the file id, and save the returned batch `id`. `endpoint` is required and must be `/v1/responses`.

```python Python theme={null}
batch = client.batches.create(
    input_file_id=input_file.id,
    endpoint="/v1/responses",
    completion_window="24h",
    metadata={"job": "nightly-eval"},
)

print(batch.id, batch.status)
```

```typescript Typescript theme={null}
let batch = await client.batches.create({
  input_file_id: inputFile.id,
  endpoint: '/v1/responses',
  completion_window: '24h',
  metadata: { job: 'nightly-eval' },
});

console.log(batch.id, batch.status);
```

```bash cURL theme={null}
BATCH_ID=$(curl -s https://api.perplexity.ai/v1/batches \
  -H "Authorization: Bearer $PERPLEXITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input_file_id": "'"$FILE_ID"'",
    "endpoint": "/v1/responses",
    "completion_window": "24h",
    "metadata": {"job": "nightly-eval"}
  }' | tee /dev/stderr | jq -r '.id')
```

```json Response (abbreviated) theme={null}
{
  "id": "batch_01M4RFGZENVEQTVVJ2222M2JD6",
  "object": "batch",
  "status": "validating",
  "input_file_id": "file_01M4RFGV5DQFAZY29N9C2624A6",
  "created_at": 1791822953,
  "expires_at": 1791995753,
  "output_file_id": null,
  "error_file_id": null,
  "request_counts": {
    "completed": 0,
    "failed": 0,
    "total": 3
  },
  "metadata": {
    "job": "nightly-eval"
  }
}
```

| Field | Rule |
| - | - |
| `input_file_id` | Required. A file uploaded with `purpose=batch`. |
| `endpoint` | Required. Must be `/v1/responses`. |
| `completion_window` | Optional. `24h` is the only value and the default. It is a target, not a deadline. |
| `metadata` | Optional. Up to 16 string pairs. Keys are 1 to 64 characters and values up to 512. |

`output_expires_after` is not supported and returns `400`. The batch's `expires_at` is its deadline, 48 hours after `created_at`. See [Timing and expiry](#timing-and-expiry).

The `Idempotency-Key` header is optional and makes retries safe. In the SDKs, pass it as `idempotency_key` in Python, or as `{ 'Idempotency-Key': key }` in the second argument in TypeScript. Use a new key for each new batch, such as a UUID. If you retry the same create with the same key within 24 hours, the API returns the original batch instead of creating another. Reusing a key with a different `input_file_id`, `endpoint`, or `metadata` returns `400`. Keys are up to 200 bytes.

**4. Poll the batch**

Poll `GET /v1/batches/{batch_id}` until the status is `completed`, `expired`, `cancelled`, or `failed`. Polling every 30 to 60 seconds is enough for most batches.

> [!TIP]
>
> The batch and file endpoints use your organization's request rate limit for its [usage tier](/docs/admin/rate-limits-usage-tiers.md). At tier 0 that is 1 request per second. Do not poll faster than that. If you get `429` while polling, poll less often.

```python Python theme={null}
import time

while batch.status not in ("completed", "expired", "cancelled", "failed"):
    time.sleep(30)
    batch = client.batches.retrieve(batch.id)
    print(batch.status, batch.request_counts)

print(batch.output_file_id, batch.error_file_id, batch.usage)
```

```typescript Typescript theme={null}
const finalStatuses = ['completed', 'expired', 'cancelled', 'failed'];

while (!finalStatuses.includes(batch.status)) {
  await new Promise((resolve) => setTimeout(resolve, 30_000));
  batch = await client.batches.retrieve(batch.id);
  console.log(batch.status, batch.request_counts);
}

console.log(batch.output_file_id, batch.error_file_id, batch.usage);
```

```bash cURL theme={null}
while true; do
  BATCH=$(curl -s "https://api.perplexity.ai/v1/batches/$BATCH_ID" \
    -H "Authorization: Bearer $PERPLEXITY_API_KEY")
  STATUS=$(echo "$BATCH" | jq -r '.status')
  echo "Status: $STATUS $(echo "$BATCH" | jq -c '.request_counts')"
  case "$STATUS" in
    completed|expired|cancelled|failed) break ;;
    null) echo "$BATCH" | jq; break ;;
  esac
  sleep 30
done

echo "$BATCH" | jq '{status, output_file_id, error_file_id, request_counts, usage}'
```

```json Response theme={null}
{
  "status": "completed",
  "output_file_id": "file_01M4RFHCVYMGRJCB05K49XZ5FP",
  "error_file_id": null,
  "request_counts": {
    "completed": 3,
    "failed": 0,
    "total": 3
  },
  "usage": {
    "input_tokens": 113,
    "input_tokens_details": {
      "cached_tokens": 0
    },
    "output_tokens": 59,
    "output_tokens_details": {
      "reasoning_tokens": 37
    },
    "total_tokens": 172
  }
}
```

If the response has no `status`, it is an API error. The loop prints it and stops. `usage` sums the token usage of the requests in the output file only, so it is not your full bill.

**5. Download the results**

A final batch status means processing has ended. It does not mean every request succeeded.

* `output_file_id` is the file of requests that ran to the end. It can point to an empty file.
* `error_file_id` is the file of requests that did not, or `null` when there are none.

Download each file with `GET /v1/files/{file_id}/content`, then check each line's `response.body.status`.

```python Python theme={null}
import json

if batch.output_file_id:
    output = client.files.content(batch.output_file_id).read().decode()
    for line in output.splitlines():
        result = json.loads(line)
        body = result["response"]["body"]
        text = "".join(
            part["text"]
            for item in body["output"]
            if item["type"] == "message"
            for part in item["content"]
            if part["type"] == "output_text"
        )
        print(result["custom_id"], body["status"], text)

if batch.error_file_id:
    errors = client.files.content(batch.error_file_id).read().decode()
    print(errors)
```

```typescript Typescript theme={null}
if (batch.output_file_id) {
  const output = await (await client.files.content(batch.output_file_id)).text();
  for (const line of output.trim().split('\n')) {
    const result = JSON.parse(line);
    const body = result.response.body;
    const text = body.output
      .filter((item: any) => item.type === 'message')
      .flatMap((item: any) => item.content)
      .filter((part: any) => part.type === 'output_text')
      .map((part: any) => part.text)
      .join('');
    console.log(result.custom_id, body.status, text);
  }
}

if (batch.error_file_id) {
  const errors = await (await client.files.content(batch.error_file_id)).text();
  console.log(errors);
}
```

```bash cURL theme={null}
OUTPUT_FILE_ID=$(echo "$BATCH" | jq -r '.output_file_id')
ERROR_FILE_ID=$(echo "$BATCH" | jq -r '.error_file_id')

if [ "$OUTPUT_FILE_ID" != "null" ]; then
  curl -s "https://api.perplexity.ai/v1/files/$OUTPUT_FILE_ID/content" \
    -H "Authorization: Bearer $PERPLEXITY_API_KEY" \
    -o batch_output.jsonl
fi

if [ "$ERROR_FILE_ID" != "null" ]; then
  curl -s "https://api.perplexity.ai/v1/files/$ERROR_FILE_ID/content" \
    -H "Authorization: Bearer $PERPLEXITY_API_KEY" \
    -o batch_errors.jsonl
fi

jq -c '{custom_id, status: .response.body.status, text: [.response.body.output[]? | select(.type == "message") | .content[]? | select(.type == "output_text") | .text] | join("")}' batch_output.jsonl
```

```json Output theme={null}
{"custom_id":"request-1","status":"completed","text":"Paris"}
{"custom_id":"request-2","status":"completed","text":"144"}
{"custom_id":"request-3","status":"completed","text":"Jupiter"}
```

## Supported requests

Batch accepts any model from the Agent API [models list](/docs/agent-api/models.md). Every line of a file must use the same `model`.

Requests that use an `anthropic/*` model must include `max_output_tokens`, as in a normal request. A line without it fails with `400` in the error file.

| Field | Batch rule |
| - | - |
| `model` | Required, and the same on every line. |
| `models` | Not supported. Set `model` instead. |
| `preset` | Allowed only together with `model`. |
| `stream` | `true` is rejected. |
| `background`, `store`, `service_tier` | Ignored. Every batch request runs in the background, is stored, and uses the tier described in [Billing](#billing). |
| `tools` | Built-in tools are allowed. `function`, `connector`, and `advisor` tools are rejected at upload with `400`. `mcp` tools must leave `require_approval` unset or set it to `"never"`. |

Other Agent API fields work as they do in a normal request, including `instructions`, `max_steps`, `max_output_tokens`, `reasoning`, and `response_format`.

The file must be UTF-8. Lines can end in LF or CRLF. Empty lines and duplicate JSON keys are rejected.

## Output file format

Each output line has the request's `custom_id` and the full Response object in `response.body`. Each request is a normal Response, so you can also fetch it with `GET /v1/agent/{id}` using `response.body.id`. The example below is abbreviated.

```json theme={null}
{
  "id": "batch_req_01M4RFGZMBKRRZ4QYPDRCZ0CA0",
  "custom_id": "request-1",
  "response": {
    "status_code": 200,
    "request_id": "01M4RFGZMBKRRZ4QYPDRCZ0CA0",
    "body": {
      "id": "resp_01M4RFGZMBKRRZ4QYPDRCZ0CA0",
      "object": "response",
      "status": "completed",
      "model": "openai/gpt-6-luna",
      "service_tier": "flex",
      "output": [],
      "usage": {}
    }
  },
  "error": null
}
```

* Do not rely on line order. Match results to requests by `custom_id`.
* `response.body.status` can be `completed`, `incomplete` (for example when `max_output_tokens` is reached), or `failed`. Check it before you use the output.

## Error file format

Requests that did not produce a result go to the error file and count as failed. A line has one of two shapes.

A request that the API rejected, or that failed before it produced any output, has `error: null`. `response` holds the status code and error body, in the same error format as a normal request. For example, an input over the model's context window gets `400`.

```json theme={null}
{"id": "batch_req_01M5FP1MGJM704G7XYJTQ735JB", "custom_id": "haiku-no-max", "response": {"status_code": 400, "request_id": "01M5FP1MGJM704G7XYJTQ735JB", "body": {"error": {"message": "validation failed: max_output_tokens is required when using Anthropic models", "type": "invalid_request", "code": 400}}}, "error": null}
```

A request that failed after it produced output stays in the output file, with `response.body.status` set to `failed`.

A request that stopped without an answer has `response: null` and an `error`:

```json theme={null}
{"id": "batch_req_01M4RFJC22KH0N346H7E8XH1DY", "custom_id": "cancel-1", "response": null, "error": {"code": "batch_cancelled", "message": "The batch was cancelled before this request finished."}}
```

| `error.code` | Meaning |
| - | - |
| `batch_expired` | The request had not started, or was waiting for capacity, when the batch reached its 48 hour deadline. |
| `batch_cancelled` | The batch was cancelled before the request finished. |
| `poll_timeout` | One run of the request took more than 30 minutes and was stopped. |
| `response_too_large` | The response was larger than 20 MB and could not be stored. |
| `response_unavailable` | The result of the request could not be recovered. |

## Batch statuses

| Status | Meaning |
| - | - |
| `validating` | The batch was created and its requests are being queued. |
| `in_progress` | Requests are running. |
| `finalizing` | All requests are done and the output files are being written. |
| `completed` | Final. No request expired and the batch was not cancelled. Some requests can still be in the error file. |
| `expired` | Final. At least one request expired at the 48 hour deadline. Requests that finished are in the output file. |
| `cancelling` | A cancel was requested and running requests are stopping. |
| `cancelled` | Final. The batch was cancelled. Requests that finished first are in the output file. |
| `failed` | Final. The batch itself could not be processed. The batch `errors` field can have details, and `output_file_id` can be `null`. |

Once the batch reaches a final status, `request_counts.completed` is the number of lines in the output file. `request_counts.failed` is the number of lines in the error file, including expired and cancelled requests.

## Timing and expiry

Batches target completion within 24 hours, the `completion_window`. That is a target, not a guarantee. The batch's `expires_at` is the deadline, 48 hours after the batch was created.

At the deadline, requests that have not started, or are waiting for capacity, expire with `batch_expired`. Expired requests are not billed. Requests that are already running continue until they finish, and they bill normally. When the last running request finishes, the batch ends `expired` if any request expired. Otherwise it ends `completed`.

## Cancel a batch

Cancel a batch that is `validating` or `in_progress` with `POST /v1/batches/{batch_id}/cancel`.

```python Python theme={null}
batch = client.batches.cancel(batch.id)

print(batch.id, batch.status, batch.request_counts)
```

```typescript Typescript theme={null}
batch = await client.batches.cancel(batch.id);

console.log(batch.id, batch.status, batch.request_counts);
```

```bash cURL theme={null}
curl -X POST "https://api.perplexity.ai/v1/batches/$BATCH_ID/cancel" \
  -H "Authorization: Bearer $PERPLEXITY_API_KEY" | jq '{id, status, request_counts}'
```

```json Response theme={null}
{
  "id": "batch_01M4RFJBRPTFCNZFFNTJ6GA7XG",
  "status": "cancelling",
  "request_counts": {
    "completed": 0,
    "failed": 0,
    "total": 3
  }
}
```

The batch moves to `cancelling`, then to `cancelled` once running requests stop. Poll the batch to see the final status.

* Requests that completed before the cancel stay in the output file and are billed.
* Requests already running can still finish for a short time after the cancel, usually up to about a minute. Those that finish go to the output file and are billed.
* Running requests that are stopped go to the error file with `batch_cancelled`. They can bill for the steps they already ran.
* Requests that had not started go to the error file with `batch_cancelled`. They are not billed.

Cancelling a batch that is already `cancelling` or `cancelled` returns it unchanged. Cancelling a batch that is `finalizing`, `completed`, `expired`, or `failed` returns `409`.

## List batches

`GET /v1/batches` lists your organization's batches, newest first. `limit` is 1 to 100 and defaults to 20. If `has_more` is `true`, pass the page's `last_id` as `after` to get the next page.

```python Python theme={null}
from perplexity import Perplexity

client = Perplexity()

page = client.batches.list(limit=2)

for batch in page.data:
    print(batch.id, batch.status, batch.request_counts)

print(page.has_more, page.last_id)
```

```typescript Typescript theme={null}
import Perplexity from '@perplexity-ai/perplexity_ai';

const client = new Perplexity();

const page = await client.batches.list({ limit: 2 });

for (const batch of page.data) {
  console.log(batch.id, batch.status, batch.request_counts);
}

console.log(page.has_more, page.last_id);
```

```bash cURL theme={null}
curl "https://api.perplexity.ai/v1/batches?limit=2" \
  -H "Authorization: Bearer $PERPLEXITY_API_KEY" \
  | jq '{has_more, last_id, data: [.data[] | {id, status, request_counts}]}'
```

```json Response theme={null}
{
  "has_more": true,
  "last_id": "batch_01M3X4BZ48QKGP30F2FMYDKNY6",
  "data": [
    {
      "id": "batch_01M4RFGZENVEQTVVJ2222M2JD6",
      "status": "completed",
      "request_counts": {"completed": 3, "failed": 0, "total": 3}
    },
    {
      "id": "batch_01M3X4BZ48QKGP30F2FMYDKNY6",
      "status": "completed",
      "request_counts": {"completed": 3, "failed": 0, "total": 3}
    }
  ]
}
```

## Files and retention

Input, output, and error files expire 30 days after they are created, as shown in each file's `expires_at`. Download your results before then.

* `GET /v1/files` lists your organization's files, newest first. It includes your uploads and the output and error files that batches write. Deleted and expired files are not listed.
* `GET /v1/files/{file_id}` returns a file's metadata.
* `GET /v1/files/{file_id}/content` downloads its contents.
* `DELETE /v1/files/{file_id}` deletes it.

Deleting an input file does not affect batches already created from it. Output and error files have `purpose` set to `batch_output`.

`GET /v1/files` takes the same `limit` and `after` paging as the batch list. `limit` is 1 to 100, which is lower than OpenAI's maximum. Set `order` to `asc` for oldest first, and `purpose` to `batch` or `batch_output` to filter.

```bash cURL theme={null}
curl "https://api.perplexity.ai/v1/files?limit=2&purpose=batch" \
  -H "Authorization: Bearer $PERPLEXITY_API_KEY" \
  | jq '{has_more, last_id, data: [.data[] | {id, filename, purpose}]}'
```

```json Response theme={null}
{
  "has_more": true,
  "last_id": "file_01M5FP1KVAMZBDAPV8KXYR8478",
  "data": [
    {
      "id": "file_01M5FPBFJMG9ZPPCBH4YNB90D0",
      "filename": "cancel.jsonl",
      "purpose": "batch"
    },
    {
      "id": "file_01M5FP1KVAMZBDAPV8KXYR8478",
      "filename": "anthropic.jsonl",
      "purpose": "batch"
    }
  ]
}
```

## Limits

| Limit | Value |
| - | - |
| Requests per file | 50,000 |
| File size | 200 MB (209,715,200 bytes) |
| Line size | 4 MB (4,194,304 bytes) |
| Filename | 1 to 255 bytes |
| Output and error files, combined | 8 GB. A batch over this ends `failed` with the `output_too_large` error. |

Your organization can also have a limited number of unfinished batch requests at once, based on its [usage tier](/docs/admin/rate-limits-usage-tiers.md). Each line of an input file is one request. The count drops as requests finish.

| Usage tier | Unfinished batch requests |
| - | - |
| Tier 0 | 50,000 |
| Tier 1 | 100,000 |
| Tier 2 | 200,000 |
| Tier 3 | 300,000 |
| Tier 4 | 500,000 |
| Tier 5 | 500,000 |
| Tier 6 and above | 1,000,000 |

A create that would go over the limit returns `429` with the current count and limit. Wait for earlier batches to finish, or submit a smaller file.

## Billing

There is no separate batch price. OpenAI models run on the `flex` [service tier](/docs/agent-api/models.md#service-tiers) where the model lists it. Other models run on the standard tier at their normal price. Starting 20 to 24 hours after the batch was created, requests that start or resume run on the standard tier so they can finish before the deadline. Each result's `response.body.service_tier` shows the tier that served it. Tokens bill at the tier that served each step. Tool calls bill at their normal price.

Requests that never started or that expired are not billed. A request stopped while it was running can bill for the steps it already ran.

Creating a batch requires API credits. If your credits run out while a batch runs, the batch pauses. Requests refused for credits do not fail. The batch resumes after you add credits. The batch deadline does not change while it is paused. Requests still waiting at the deadline expire with `batch_expired`.

## Errors

| Status | When |
| - | - |
| `400` | Invalid request, an invalid input file line, an unsupported option such as `expires_after` or `output_expires_after`, a missing or expired `input_file_id` on create, or an `Idempotency-Key` reused with a different request. |
| `402` | Your API credits are exhausted. |
| `403` | Your organization has Zero Data Retention. |
| `404` | The batch or file was not found, or belongs to another organization. Expired files also return `404`. |
| `409` | The batch is `finalizing`, `completed`, `expired`, or `failed` and cannot be cancelled. |
| `413` | The file is larger than 200 MB. |
| `429` | Rate limit, unfinished request limit, or too many uploads in progress. Retry later. |
| `503` | The service is temporarily unavailable. Retry the request. |

## Next steps

> **[Background mode](/docs/agent-api/background-mode.md)**

> **[Models](/docs/agent-api/models.md)**

> **[Create batch reference](/api-reference/batches-create-post.md)**

> **[Upload file reference](/api-reference/files-create-post.md)**
