# Batch: Scarleta API reference

Page: https://www.scarleta.ai/docs/reference/batch
Base URL: https://api.scarleta.ai. Authentication: `Authorization: Bearer $SCARLETA_API_KEY`.
Operations: 7. Webhooks: 1.

## POST /v1/batch

Operation id: `submit_batch`

**Submit a batch**

Runs one recipe over up to 500 audio files in the background. The call returns `202` at once with a `batch_id`; follow progress with the status route or receive a `batch_completed` webhook when the batch ends. Batches are available on paid accounts only; a free-account key receives 403 `batch_paid_only`.

Each file is charged like a single run of the recipe: 1 token per second of audio at the base rate ($0.10 per minute), and recipes can carry their own per-step rates. Use the estimate route first to see the cost. The account needs a token balance, or auto-recharge turned on, otherwise the call returns 402 `insufficient_balance`.

Send an `Idempotency-Key` header to make retries safe: the same key with the same request returns the original `batch_id` (with `meta.idempotent: true`) and starts nothing new; the same key with a different request returns 409 `idempotency_key_reused`.

Capped at 10 requests per minute per account on top of the hourly account budget.

Access: API key required; paid accounts only. Cost: uses tokens. Charged per file when its run finishes: 1 token per second of audio at the base rate of $0.10 per minute. Recipes can use per-step rates, so check the estimate route first..

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | 1 to 255 visible ASCII characters. Makes the submit safe to retry. |

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `recipe_id` | string | The recipe to run on every file, for example `rec_1005_cat_spectral`. Allowed: at least 1 character. |
| `audio_urls` | array<string> | HTTPS URLs of the audio files to process, 1 to 500 entries. |
| `name` | string | A label for the batch, up to 32000 characters. Returned as `name` by the list route. |
| `message` | string | Older alias of `name`. When both are sent, `name` wins. Allowed: at most 32000 characters. |
| `webhook_url` | string | Where to POST the `batch_completed` event. Must be an https URL that resolves to a public address, at most 4096 bytes. |
| `metadata` | object | Free-form JSON object kept with the batch, at most 8192 bytes when serialized. Keys are limited to 64 characters and string values to 2048. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `batch_id` | string | Identifier of the new batch. |
| `status` | "IN_QUEUE" |   |

### Responses

202: Accepted. The batch is queued. `meta` has no `request_id`; the `X-Request-ID` header carries the request id.

```json
{
  "data": {
    "batch_id": "b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7",
    "status": "IN_QUEUE"
  },
  "meta": {
    "source": "workflow"
  }
}
```

400: Invalid JSON, a missing or empty `audio_urls` or `recipe_id`, an unacceptable audio or webhook URL, a label or metadata that breaks its limits, or a malformed `Idempotency-Key`.

```json
{
  "error": {
    "code": "missing_audio_urls",
    "message": "audio_urls must be a non-empty array",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#missing_audio_urls"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

402: No token balance and no auto-recharge.

```json
{
  "error": {
    "code": "insufficient_balance",
    "message": "Your team has no token balance and auto-recharge is not active. Top up to submit batches.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#insufficient_balance"
  }
}
```

403: The key belongs to a free account, or a file is not owned by your account.

```json
{
  "error": {
    "code": "batch_paid_only",
    "message": "Batch processing is available on paid plans only.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_paid_only"
  }
}
```

404: The recipe does not exist or is not visible to your account.

```json
{
  "error": {
    "code": "not_found",
    "message": "Recipe 'rec_9999_unknown' not found.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#not_found"
  }
}
```

409: The `Idempotency-Key` was used before with a different request.

```json
{
  "error": {
    "code": "idempotency_key_reused",
    "message": "This Idempotency-Key was already used with a different batch request.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#idempotency_key_reused"
  }
}
```

413: The metadata is over 16384 bytes, or the `webhook_url` is over 4096 bytes.

```json
{
  "error": {
    "code": "metadata_too_large",
    "message": "metadata serialized JSON exceeds 16384 bytes",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#metadata_too_large"
  }
}
```

422: More than 500 files.

```json
{
  "error": {
    "code": "batch_too_large",
    "message": "audio_urls must contain at most 500 entries (got 501)",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_too_large"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

500: The batch could not be started. Retry with the same `Idempotency-Key`.

```json
{
  "error": {
    "code": "workflow_create_failed",
    "message": "Batch could not be started; please retry.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#workflow_create_failed"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`batch_paid_only`](https://www.scarleta.ai/docs/errors#batch_paid_only), [`batch_too_large`](https://www.scarleta.ai/docs/errors#batch_too_large), [`idempotency_key_reused`](https://www.scarleta.ai/docs/errors#idempotency_key_reused), [`insufficient_balance`](https://www.scarleta.ai/docs/errors#insufficient_balance), [`invalid_audio_url`](https://www.scarleta.ai/docs/errors#invalid_audio_url), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_idempotency_key`](https://www.scarleta.ai/docs/errors#invalid_idempotency_key), [`invalid_json`](https://www.scarleta.ai/docs/errors#invalid_json), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`invalid_message`](https://www.scarleta.ai/docs/errors#invalid_message), [`invalid_metadata`](https://www.scarleta.ai/docs/errors#invalid_metadata), [`invalid_name`](https://www.scarleta.ai/docs/errors#invalid_name), [`invalid_recipe_id`](https://www.scarleta.ai/docs/errors#invalid_recipe_id), [`invalid_webhook_url`](https://www.scarleta.ai/docs/errors#invalid_webhook_url), [`media_not_owned`](https://www.scarleta.ai/docs/errors#media_not_owned), [`message_too_long`](https://www.scarleta.ai/docs/errors#message_too_long), [`metadata_too_large`](https://www.scarleta.ai/docs/errors#metadata_too_large), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_audio_urls`](https://www.scarleta.ai/docs/errors#missing_audio_urls), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`missing_recipe_id`](https://www.scarleta.ai/docs/errors#missing_recipe_id), [`name_too_long`](https://www.scarleta.ai/docs/errors#name_too_long), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized), [`webhook_url_too_large`](https://www.scarleta.ai/docs/errors#webhook_url_too_large), [`webhook_validation_unavailable`](https://www.scarleta.ai/docs/errors#webhook_validation_unavailable), [`workflow_create_failed`](https://www.scarleta.ai/docs/errors#workflow_create_failed)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/batch" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Idempotency-Key: a-unique-value-per-request" \
  -H "Content-Type: application/json" \
  -d '{
    "recipe_id": "rec_1005_cat_spectral",
    "audio_urls": [
      "https://example.com/track-1.wav",
      "https://example.com/track-2.wav"
    ],
    "name": "Morning session",
    "webhook_url": "https://example.com/hooks/scarleta"
  }'
```

## GET /v1/batch/{batch_id}

Operation id: `get_batch`

**Get a batch**

Returns the live state of one batch: its status, per-state item counters and progress. A batch is final when `status` is `completed`, `failed`, `partial` or `cancelled`. Only batches created by your own account can be read; any other id returns 404. Capped at 600 requests per minute per account on top of the hourly account budget.

Access: API key required; paid accounts only. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `batch_id` | path | string | yes | The `batch_id` returned when the batch was submitted. Batches submitted today get a UUID; an older batch can carry an id of the form `batch_<digits>_<hex>`, which is accepted too. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `message` | string | Human-readable progress line. |
| `batch_id` | string | Identifier of the batch. |
| `status` | "processing" \| "completed" \| "failed" \| "partial" \| "cancelled" \| null | Current state. `processing` until every item is final. |
| `error` | string \| null | Summary of why the batch failed, when it did. |
| `total` | integer | Items in the batch. Allowed: -9007199254740991 to 9007199254740991. |
| `completed` | integer | Items that finished successfully. Allowed: -9007199254740991 to 9007199254740991. |
| `failed` | integer | Items that failed. Allowed: -9007199254740991 to 9007199254740991. |
| `cancelled` | integer | Items cancelled before they finished. Allowed: -9007199254740991 to 9007199254740991. |
| `pending` | integer | Items waiting to start. Allowed: -9007199254740991 to 9007199254740991. |
| `processing` | integer | Items running now. Allowed: -9007199254740991 to 9007199254740991. |
| `percent_complete` | number | `completed` divided by `total`, as a percentage with one decimal. |
| `actual_cost` | number | Tokens charged so far for the batch. |
| `estimated_cost` | number \| null | Estimate recorded at submit, when there was one. |
| `created_at` | number \| null | Milliseconds since the Unix epoch. |
| `completed_at` | number \| null | Milliseconds since the Unix epoch. |
| `webhook_delivery_status` | string \| null | State of the `batch_completed` delivery: `in_progress`, `delivered`, `failed`, `skipped` (no `webhook_url`, or the batch was cancelled, which sends no webhook) or `unconfirmed`; null before the batch ends. It describes only the per-request `webhook_url`: addresses registered with `POST /v1/webhooks` are retried the same way, but their outcome is not shown in this field. |
| `webhook_last_attempt_at` | number \| null | Milliseconds since the Unix epoch. |

### Responses

200: Success

```json
{
  "data": {
    "message": "Batch b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7: 100.0% complete (2/2)",
    "batch_id": "b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7",
    "status": "completed",
    "error": null,
    "total": 2,
    "completed": 2,
    "failed": 0,
    "cancelled": 0,
    "pending": 0,
    "processing": 0,
    "percent_complete": 100,
    "actual_cost": 40,
    "estimated_cost": null,
    "created_at": 1791333870854,
    "completed_at": 1791333895009,
    "webhook_delivery_status": "delivered",
    "webhook_last_attempt_at": 1791333895229
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: `batch_id` is neither a UUID nor a legacy `batch_<digits>_<hex>` id.

```json
{
  "error": {
    "code": "invalid_batch_id",
    "message": "batch_id must be a valid batch id",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_batch_id"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: No batch with this id exists for your account.

```json
{
  "error": {
    "code": "batch_not_found",
    "message": "Batch b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7 not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_not_found"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`batch_not_found`](https://www.scarleta.ai/docs/errors#batch_not_found), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_batch_id`](https://www.scarleta.ai/docs/errors#invalid_batch_id), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
BATCH_ID="YOUR_BATCH_ID"
curl "https://api.scarleta.ai/v1/batch/$BATCH_ID" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## DELETE /v1/batch/{batch_id}

Operation id: `cancel_batch`

**Cancel a batch**

Stops a batch that is still running: items that have not finished are cancelled and their work is stopped. Items that already finished keep their results and their charges. A batch that has already reached a final state returns 409 `batch_already_final`. Capped at 10 requests per minute per account on top of the hourly account budget.

Access: API key required; paid accounts only. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `batch_id` | path | string | yes | The `batch_id` returned when the batch was submitted. Batches submitted today get a UUID; an older batch can carry an id of the form `batch_<digits>_<hex>`, which is accepted too. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `message` | string |   |
| `batch_id` | string | Identifier of the batch. |
| `cancelled` | integer | Items this call cancelled. Allowed: -9007199254740991 to 9007199254740991. |

### Responses

200: Success

```json
{
  "data": {
    "message": "Batch b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7 cancellation requested. 2 items cancelled.",
    "batch_id": "b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7",
    "cancelled": 2
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: `batch_id` is neither a UUID nor a legacy `batch_<digits>_<hex>` id.

```json
{
  "error": {
    "code": "invalid_batch_id",
    "message": "batch_id must be a valid batch id",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_batch_id"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: No batch with this id exists for your account.

```json
{
  "error": {
    "code": "batch_not_found",
    "message": "Batch b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7 not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_not_found"
  }
}
```

409: The batch had already reached a final state. `error.details.status` holds that state.

```json
{
  "error": {
    "code": "batch_already_final",
    "message": "Batch b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7 already completed",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_already_final"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`batch_already_final`](https://www.scarleta.ai/docs/errors#batch_already_final), [`batch_not_found`](https://www.scarleta.ai/docs/errors#batch_not_found), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_batch_id`](https://www.scarleta.ai/docs/errors#invalid_batch_id), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
BATCH_ID="YOUR_BATCH_ID"
curl -X DELETE "https://api.scarleta.ai/v1/batch/$BATCH_ID" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## GET /v1/batch/{batch_id}/results

Operation id: `get_batch_results`

**Get batch results**

Returns the per-file results of a batch, 50 items per page by default. Page with `offset` and `limit` while `has_more` is true. The keys inside each `raw_results` depend on the recipe that ran. Capped at 600 requests per minute per account on top of the hourly account budget.

Access: API key required; paid accounts only. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `batch_id` | path | string | yes | The `batch_id` returned when the batch was submitted. Batches submitted today get a UUID; an older batch can carry an id of the form `batch_<digits>_<hex>`, which is accepted too. |
| `offset` | query | integer | no | Items to skip. Defaults to 0. Allowed: 0 to 9007199254740991. |
| `limit` | query | integer | no | Items per page. Defaults to 50; values above 100 are clamped to 100. Allowed: 1 to 100. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `message` | string |   |
| `batch_id` | string | Identifier of the batch. |
| `items` | array<object> |   |
| `items[].item_id` | string | Identifier of this item attempt. |
| `items[].item_index` | integer \| null | Position of the file in `audio_urls`, starting at 0. Allowed: -9007199254740991 to 9007199254740991. |
| `items[].audio_url` | string | The file this item processed. |
| `items[].display_name` | string \| null | The file's library name, when it is in your library; null otherwise. |
| `items[].reference_url` | string \| null |   |
| `items[].status` | "pending" \| "processing" \| "completed" \| "failed" \| "cancelled" |   |
| `items[].raw_results` | object \| null | Output of the recipe for this file. The keys depend on the recipe that ran. |
| `items[].error` | string \| null | Why the item failed, when it did. |
| `items[].cost` | number \| null | Tokens charged for this item. |
| `items[].duration_ms` | number \| null | How long the item took, in milliseconds. |
| `items[].retry_count` | integer \| null | How many times this item has been retried. Allowed: -9007199254740991 to 9007199254740991. |
| `total` | integer | Items in the batch. Allowed: -9007199254740991 to 9007199254740991. |
| `offset` | integer | Allowed: -9007199254740991 to 9007199254740991. |
| `limit` | integer | Allowed: -9007199254740991 to 9007199254740991. |
| `has_more` | boolean | True when another page follows. |
| `filters_applied` | object |   |
| `filters_applied.status_filter` | string |   |

### Responses

200: Success

```json
{
  "data": {
    "message": "Returning 1 items (offset 0)",
    "batch_id": "b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7",
    "items": [
      {
        "item_id": "3f2a9c1e-7b64-4c0d-8e21-5a6b7c8d9e0f",
        "item_index": 0,
        "audio_url": "https://example.com/track-1.wav",
        "reference_url": null,
        "status": "completed",
        "raw_results": {
          "recipe_id": "rec_1005_cat_spectral",
          "status": "COMPLETED"
        },
        "error": null,
        "cost": 20,
        "duration_ms": 8120,
        "retry_count": 0
      }
    ],
    "total": 2,
    "offset": 0,
    "limit": 50,
    "has_more": true,
    "filters_applied": {
      "status_filter": "all"
    }
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: `batch_id` is neither a UUID nor a legacy `batch_<digits>_<hex>` id.

```json
{
  "error": {
    "code": "invalid_batch_id",
    "message": "batch_id must be a valid batch id",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_batch_id"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: No batch with this id exists for your account.

```json
{
  "error": {
    "code": "batch_not_found",
    "message": "Batch b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7 not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_not_found"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`batch_not_found`](https://www.scarleta.ai/docs/errors#batch_not_found), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_batch_id`](https://www.scarleta.ai/docs/errors#invalid_batch_id), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
BATCH_ID="YOUR_BATCH_ID"
curl "https://api.scarleta.ai/v1/batch/$BATCH_ID/results" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## POST /v1/batch/{batch_id}/retry

Operation id: `retry_batch`

**Retry failed items of a batch**

Re-queues the items of a finished batch that failed, as new attempts, and reopens the batch. Only a batch whose status is `failed`, `partial` or `cancelled` can be retried; any other status returns 409 `batch_not_retryable`. An item that has already been retried `max_retries` times is left as failed and counted in `permanently_failed`. The body is optional. Capped at 10 requests per minute per account on top of the hourly account budget.

Access: API key required; paid accounts only. Cost: uses tokens. Re-queued items are charged again when they finish, at the same per-file rate as the original run..

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `batch_id` | path | string | yes | The `batch_id` returned when the batch was submitted. Batches submitted today get a UUID; an older batch can carry an id of the form `batch_<digits>_<hex>`, which is accepted too. |

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `max_retries` | integer | Highest number of times one item may be retried, 1 to 10. Defaults to 2. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `message` | string |   |
| `batch_id` | string | Identifier of the batch. |
| `requeued` | integer | Items put back in the queue. Allowed: -9007199254740991 to 9007199254740991. |
| `permanently_failed` | integer | Failed items that were not re-queued because they reached `max_retries`. Allowed: -9007199254740991 to 9007199254740991. |
| `superseded` | boolean | True when a cancel or another retry changed the batch while this call ran, so nothing was started. |

### Responses

200: Success

```json
{
  "data": {
    "message": "Retry results: 1 items requeued, 0 permanently failed (already at 2 retries)",
    "batch_id": "b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7",
    "requeued": 1,
    "permanently_failed": 0
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: The body is not a JSON object, `max_retries` is not an integer in range, or `batch_id` is neither a UUID nor a legacy `batch_<digits>_<hex>` id.

```json
{
  "error": {
    "code": "invalid_max_retries",
    "message": "max_retries must be an integer between 1 and 10",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_max_retries"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: No batch with this id exists for your account. If the batch exists but its recipe was deleted, the code is `not_found` instead (past results are kept).

```json
{
  "error": {
    "code": "batch_not_found",
    "message": "Batch b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7 not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_not_found"
  }
}
```

409: The batch is still running or already completed.

```json
{
  "error": {
    "code": "batch_not_retryable",
    "message": "Batch is not in a retryable terminal state (current: completed)",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_not_retryable"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

500: The retry could not be started.

```json
{
  "error": {
    "code": "workflow_create_failed",
    "message": "Retry could not be started; please retry.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#workflow_create_failed"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`batch_not_found`](https://www.scarleta.ai/docs/errors#batch_not_found), [`batch_not_retryable`](https://www.scarleta.ai/docs/errors#batch_not_retryable), [`batch_recipe_missing`](https://www.scarleta.ai/docs/errors#batch_recipe_missing), [`batch_tenant_missing`](https://www.scarleta.ai/docs/errors#batch_tenant_missing), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_batch_id`](https://www.scarleta.ai/docs/errors#invalid_batch_id), [`invalid_json`](https://www.scarleta.ai/docs/errors#invalid_json), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`invalid_max_retries`](https://www.scarleta.ai/docs/errors#invalid_max_retries), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`not_found`](https://www.scarleta.ai/docs/errors#not_found), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized), [`workflow_create_failed`](https://www.scarleta.ai/docs/errors#workflow_create_failed)

### Example

```bash
BATCH_ID="YOUR_BATCH_ID"
curl -X POST "https://api.scarleta.ai/v1/batch/$BATCH_ID/retry" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "max_retries": 2
  }'
```

## POST /v1/batch/estimate

Operation id: `estimate_batch`

**Estimate the cost of a batch**

Returns the token cost of running a recipe over a list of files, per file and in total, without starting anything and without charging. Batches are available on paid accounts only; a free-account key receives 403 `batch_paid_only`. Accepts the same `recipe_id` and `audio_urls` as submit, at most 500 files. A file whose length cannot be read contributes 0 and reports `duration_seconds: null`. Capped at 600 requests per minute per account on top of the hourly account budget.

Access: API key required; paid accounts only. Cost: no token cost.

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `recipe_id` | string | The recipe to run on every file, for example `rec_1005_cat_spectral`. Allowed: at least 1 character. |
| `audio_urls` | array<string> | HTTPS URLs of the audio files to process, 1 to 500 entries. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `message` | string |   |
| `recipe_id` | string |   |
| `total_cost` | number | Tokens the whole batch would cost. |
| `file_count` | integer | Allowed: -9007199254740991 to 9007199254740991. |
| `per_file` | array<object> |   |
| `per_file[].audio_url` | string |   |
| `per_file[].duration_seconds` | number \| null |   |
| `per_file[].cost` | number |   |

### Responses

200: Success

```json
{
  "data": {
    "message": "Estimated cost for 2 file(s) with recipe rec_1005_cat_spectral",
    "recipe_id": "rec_1005_cat_spectral",
    "total_cost": 40,
    "file_count": 2,
    "per_file": [
      {
        "audio_url": "https://example.com/track-1.wav",
        "duration_seconds": 4,
        "cost": 20
      },
      {
        "audio_url": "https://example.com/track-2.wav",
        "duration_seconds": 3.2,
        "cost": 20
      }
    ]
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: The body is not valid JSON, `recipe_id` or `audio_urls` is missing, or a URL is not an acceptable public audio URL.

```json
{
  "error": {
    "code": "missing_audio_urls",
    "message": "audio_urls must be a non-empty array",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#missing_audio_urls"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

403: The key belongs to a free account.

```json
{
  "error": {
    "code": "batch_paid_only",
    "message": "Batch processing is available on paid plans only.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_paid_only"
  }
}
```

404: The recipe does not exist or is not visible to your account.

```json
{
  "error": {
    "code": "recipe_not_found",
    "message": "Recipe rec_9999_unknown not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#recipe_not_found"
  }
}
```

422: More than 500 files.

```json
{
  "error": {
    "code": "batch_too_large",
    "message": "audio_urls must contain at most 500 entries (got 501)",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#batch_too_large"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`batch_paid_only`](https://www.scarleta.ai/docs/errors#batch_paid_only), [`batch_too_large`](https://www.scarleta.ai/docs/errors#batch_too_large), [`invalid_audio_url`](https://www.scarleta.ai/docs/errors#invalid_audio_url), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_json`](https://www.scarleta.ai/docs/errors#invalid_json), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_audio_urls`](https://www.scarleta.ai/docs/errors#missing_audio_urls), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`missing_recipe_id`](https://www.scarleta.ai/docs/errors#missing_recipe_id), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`recipe_not_found`](https://www.scarleta.ai/docs/errors#recipe_not_found), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/batch/estimate" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipe_id": "rec_1005_cat_spectral",
    "audio_urls": [
      "https://example.com/track-1.wav",
      "https://example.com/track-2.wav"
    ]
  }'
```

## GET /v1/batches

Operation id: `list_batches`

**List batches**

Returns your most recent batches, newest first, each with its item counters and the tokens charged so far. Page past the first `limit` batches with `offset`; `total` is how many batches you have in all. Returns an empty list for a free account, which cannot create batches. Capped at 600 requests per minute per account on top of the hourly account budget.

Access: API key required; free and paid accounts. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | Rows to return. Defaults to 50; values above 100 are clamped to 100. Allowed: 1 to 100. |
| `offset` | query | integer | no | Batches to skip, newest first. Defaults to 0; a negative or non-numeric value is read as 0. Allowed: 0 to 9007199254740991. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `message` | string |   |
| `batches` | array<object> |   |
| `batches[].batch_id` | string | Identifier of the batch. Batches submitted today get a UUID. An older batch can carry an id of the form `batch_<digits>_<hex>`: it is listed here and every batch route accepts it. |
| `batches[].status` | "processing" \| "completed" \| "failed" \| "partial" \| "cancelled" \| null | Current state of the batch. |
| `batches[].recipe_id` | string \| null |   |
| `batches[].name` | string \| null | The label sent as `name` (or `message`) at submit, if any. |
| `batches[].total_items` | integer \| null | Allowed: -9007199254740991 to 9007199254740991. |
| `batches[].completed_items` | integer | Items that finished successfully. Allowed: -9007199254740991 to 9007199254740991. |
| `batches[].failed_items` | integer | Items that failed. Allowed: -9007199254740991 to 9007199254740991. |
| `batches[].cancelled_items` | integer | Items cancelled before they finished. Allowed: -9007199254740991 to 9007199254740991. |
| `batches[].created_at` | number \| null | When the batch was submitted, in milliseconds since the Unix epoch. |
| `batches[].completed_at` | number \| null | When the batch reached a final state, in milliseconds since the Unix epoch. |
| `batches[].actual_cost` | integer | Tokens charged so far for this batch's items; 0 when nothing was charged. Allowed: -9007199254740991 to 9007199254740991. |
| `count` | integer | Allowed: -9007199254740991 to 9007199254740991. |
| `total` | integer | How many batches you have in all, which can be more than the page returned. On a page past the end it is the offset you asked for. Allowed: -9007199254740991 to 9007199254740991. |
| `offset` | integer | The offset this page starts at. Allowed: -9007199254740991 to 9007199254740991. |

### Responses

200: Success

```json
{
  "data": {
    "message": "Returning 1 batches",
    "batches": [
      {
        "batch_id": "b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7",
        "status": "completed",
        "recipe_id": "rec_1005_cat_spectral",
        "name": "Morning session",
        "total_items": 2,
        "completed_items": 2,
        "failed_items": 0,
        "cancelled_items": 0,
        "created_at": 1791333870854,
        "completed_at": 1791333895009,
        "actual_cost": 120
      }
    ],
    "count": 1,
    "total": 1,
    "offset": 0
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl "https://api.scarleta.ai/v1/batches" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## Webhook batch_completed

**Batch completed**

Sent when a batch ends as `completed`, `failed` or `partial`: to the `webhook_url` of the batch, if it has one, and to every address registered with `POST /v1/webhooks`. The body is a JSON object with exactly the fields below. Each delivery carries `X-Scarleta-Timestamp` (Unix seconds) and `X-Scarleta-Signature` (`v1=` followed by the lowercase hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with your signing secret from `GET /v1/webhooks/signing-secret`). Reject deliveries whose timestamp is more than 300 seconds from your clock. For one day after a rotation a delivery also carries `X-Scarleta-Signature-Previous`, signed with the previous secret; a receiver that has not switched to the new secret yet can verify that header with its old secret. Addresses registered with `POST /v1/webhooks` receive this event too, signed the same way; an address identical to the `webhook_url` is delivered once.

Your endpoint must answer with a 2xx status within 10 seconds. Scarleta makes up to 3 attempts, waiting 2 s and 4 s before the second and third; redirects are not followed. Targets that are not public https endpoints are never called. A batch you cancel sends no `batch_completed`: its `webhook_delivery_status` is `skipped`, and the cancel call itself is the answer. If you retry a batch, the webhook is sent when the retry ends. Each completion starts one delivery sequence to the `webhook_url` of the batch and never a second one, but inside that sequence an attempt that timed out or got a non-2xx answer is sent again with the same body, and the earlier attempt may already have reached you: de-duplicate by `batch_id` and `completed_at`. Registered addresses are delivered at least once, on a best-effort basis: a failed delivery to a registered address can also be sent again when the same completion is replayed, so de-duplicate by `batch_id` and `completed_at` there too. `webhook_delivery_status` on the batch shows the outcome of the delivery to its `webhook_url` only; registered addresses are retried the same way, but their outcome is not shown in that field.

Set by the `webhook_url` field of: `submit_batch`.

### Payload

| Field | Type | Description |
| --- | --- | --- |
| `event` | "batch_completed" |   |
| `batch_id` | string | The batch that ended. |
| `status` | "completed" \| "failed" \| "partial" | How the batch ended. |
| `total_items` | integer | Items in the batch. Allowed: -9007199254740991 to 9007199254740991. |
| `completed_items` | integer | Allowed: -9007199254740991 to 9007199254740991. |
| `failed_items` | integer | Allowed: -9007199254740991 to 9007199254740991. |
| `cancelled_items` | integer | Allowed: -9007199254740991 to 9007199254740991. |
| `actual_total_cost` | number | Tokens charged for the whole batch. |
| `completed_at` | number | When the batch ended, in milliseconds since the Unix epoch. |

### Example payload

```json
{
  "event": "batch_completed",
  "batch_id": "b1a2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7",
  "status": "completed",
  "total_items": 2,
  "completed_items": 2,
  "failed_items": 0,
  "cancelled_items": 0,
  "actual_total_cost": 40,
  "completed_at": 1791333895009
}
```
