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

# Bulk Async Runs

> Queue up to 100 writer or humanizer runs in one request

Send many texts in one request. Each item becomes its own [async run](https://docs.stealthgpt.ai/api-reference/endpoints/stealthify-runs.md): it takes the same fields, is billed the same way, and has its own `runId`, status URL and webhook.

> [!WARNING]
>
> **`model` is required on every item.** Pass `"super"`, `"standard"`, or `"lite"`. An item without `model` fails the whole request with `400 Bad Request`.

## Flow

**1. Send the items**

Send `POST /api/stealthify/runs/bulk` with up to 100 items. Each item is a [`POST /api/stealthify/runs`](https://docs.stealthgpt.ai/api-reference/endpoints/stealthify-runs) JSON body.

**2. Store the run ids**

The API returns `202 Accepted` with one `runId` and `statusUrl` per item, in the same order as `items`.

**3. Collect the results**

Set `webhookUrl` on the items to get each result as it finishes, or poll each `statusUrl` with [`GET /api/stealthify/runs/{runId}`](https://docs.stealthgpt.ai/api-reference/endpoints/stealthify-runs#poll-run-status).

## Headers

**api-token** `string` *required*

Your Stealth API token from the [Stealth API dashboard](https://www.stealthgpt.ai/api-dashboard).

**Content-Type** `string` *required*

`application/json`. File uploads aren't supported here; send a file to [`POST /api/stealthify/runs`](https://docs.stealthgpt.ai/api-reference/endpoints/stealthify-runs#request-body-file-upload) instead.

## Request body

**items** `object[]` *required*

1-100 runs to queue. Each item takes every JSON field of [`POST /api/stealthify/runs`](https://docs.stealthgpt.ai/api-reference/endpoints/stealthify-runs#request-body): `text` (or `prompt`), `model`, `rephrase`, `tone`, `writingMode`, `qualityMode`, `outputFormat`, `preservation`, `preservationOpen`, `preservationClose`, `preservationMaxLookahead`, `preservationMode`, `webhookUrl` and `webhookSecret`, with the same defaults and limits. Items don't have to match: one can humanize with `preservation` while the next writes from a prompt.

**items\[].idempotencyKey** `string`

Optional key, 1-255 characters, unique within the request. It does what the `idempotency-key` header does on a single run: sending the same item with the same key again returns the run it already started, without queueing or billing it twice. Use your own item id. To redo an item whose run failed, send it with a new key.

## Limits

* **100 items per request.** For more, send several requests.
* **4.5 MB per request body.** A larger body is rejected with `413` before it reaches the API. Long articles may need fewer than 100 items per request.
* **3,000 words per writer prompt** (`rephrase: false`), as on single runs.
* **One batch at a time.** While runs from your previous bulk request are still queued or running, a new bulk request gets `409`, unless it sends every one of those items again with the same `idempotencyKey`s (a resend, for example after a `429` or `500`). Either way an account never has more than 100 bulk runs going at once. A batch still going after an hour stops holding up the next one. Single runs aren't affected.
* **The batch is checked against your balance as a whole.** If your balance or pay-as-you-go can't cover the estimate for every item, nothing is queued and you get `402`. The check is an estimate (input words plus as many output words): each run is billed when it finishes, on what it actually wrote, so a run can still end `failed` with a billing `error.code`, for example a writer run whose article is longer than its prompt.

A request that fails for any of these reasons queues nothing, so you can fix it and send it again.

## Response: 202 Accepted

**runs** `object[]` *required*

One entry per item, in the same order as `items`. Each is the same response a single [`POST /api/stealthify/runs`](https://docs.stealthgpt.ai/api-reference/endpoints/stealthify-runs#response-202-accepted) returns: `runId`, `status` (always `"queued"`) and `statusUrl`.

```json theme={null}
{
  "runs": [
    {
      "runId": "stealthify-api-run:abc123",
      "status": "queued",
      "statusUrl": "/api/stealthify/runs/stealthify-api-run:abc123"
    },
    {
      "runId": "stealthify-api-run:def456",
      "status": "queued",
      "statusUrl": "/api/stealthify/runs/stealthify-api-run:def456"
    }
  ]
}
```

Status, completed and failed responses, and webhook callbacks, are those of a [single run](https://docs.stealthgpt.ai/api-reference/endpoints/stealthify-runs.md#poll-run-status).

## Examples

```bash curl theme={null}
curl --request POST \
  --url 'https://www.stealthgpt.ai/api/stealthify/runs/bulk' \
  --header 'Content-Type: application/json' \
  --header 'api-token: YOUR_API_TOKEN' \
  --data '{
    "items": [
      {
        "idempotencyKey": "post-1041",
        "prompt": "Read [[A1]]our pricing guide[[A2]] before you buy.",
        "model": "super",
        "preservation": true,
        "webhookUrl": "https://example.com/webhooks/stealthify"
      },
      {
        "idempotencyKey": "post-1042",
        "prompt": "Write a short guide to choosing running shoes",
        "rephrase": false,
        "model": "super",
        "webhookUrl": "https://example.com/webhooks/stealthify"
      }
    ]
  }'
```

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

headers = {"api-token": "YOUR_API_TOKEN"}
articles = [
    {"id": "post-1041", "html_text": "Read [[A1]]our pricing guide[[A2]] before you buy."},
]


def create_runs(chunk):
    while True:
        response = requests.post(
            "https://www.stealthgpt.ai/api/stealthify/runs/bulk",
            headers=headers,
            json={
                "items": [
                    {
                        "idempotencyKey": article["id"],
                        "prompt": article["html_text"],
                        "model": "super",
                        "preservation": True,
                    }
                    for article in chunk
                ]
            },
            timeout=60,
        )
        if response.status_code in (409, 429):
            time.sleep(int(response.headers.get("Retry-After", "30")))
            continue
        response.raise_for_status()
        return response.json()["runs"]


# One batch at a time: wait for every run of a batch before sending the next
for start in range(0, len(articles), 100):
    chunk = articles[start:start + 100]
    pending = {run["runId"]: (article, run) for article, run in zip(chunk, create_runs(chunk))}
    while pending:
        time.sleep(10)
        for run_id, (article, run) in list(pending.items()):
            status = requests.get(
                f"https://www.stealthgpt.ai{run['statusUrl']}",
                headers=headers,
                timeout=30,
            ).json()
            if status["status"] in ("completed", "failed", "cancelled"):
                print(article["id"], status["status"])
                del pending[run_id]
```

```javascript nodejs theme={null}
const articles = [
  { id: 'post-1041', htmlText: 'Read [[A1]]our pricing guide[[A2]] before you buy.' },
] // up to 100 per request

const response = await fetch('https://www.stealthgpt.ai/api/stealthify/runs/bulk', {
  method: 'POST',
  headers: {
    'api-token': process.env.STEALTHGPT_API_TOKEN,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    items: articles.map((article) => ({
      idempotencyKey: article.id,
      prompt: article.htmlText,
      model: 'super',
      preservation: true,
      webhookUrl: 'https://example.com/webhooks/stealthify',
    })),
  }),
  signal: AbortSignal.timeout(60_000),
})

if (!response.ok) {
  throw new Error(`bulk create failed: ${response.status} ${await response.text()}`)
}

const { runs } = await response.json()
// runs[i] is the run for articles[i]
```

## Errors

| Status | Meaning |
| - | - |
| **400** | Invalid body. `info` lists each problem with its path, such as `["items", 3, "model"]`. Nothing is queued. |
| **401** | Missing `api-token` header or token not found. |
| **402** | Your balance or pay-as-you-go can't cover the estimate for the whole batch, or no payment method is set up. Nothing is queued. |
| **409** | Your previous batch still has runs queued or running. Send the next batch when it finishes. Nothing is queued. |
| **413** | The request body is over 4.5 MB. Send fewer items per request. |
| **429** | Too many runs are being queued at once. Wait the seconds in the `Retry-After` header and send the same request again. Items without an `idempotencyKey` may already have started, and sending them again queues them again. |
| **500** | Server error. Send the same request again: items with an `idempotencyKey` come back as the runs they already started, while items without one may already have started and are queued again. |

## Usage notes

* With a few hundred items, send them a batch at a time: wait for every run of one batch to finish before sending the next. Set `webhookUrl` so each result arrives as it finishes instead of polling every run.
* Always set `idempotencyKey` on items you might send twice, such as from a queue or a retrying script. Without it, a resent item is a new run, billed again.
* Runs from one request wait in the same queue as every other run, so they finish over minutes, not all at once.


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