# Batches & automations

Run CleanScript from n8n, Make or Zapier, and process many posts from code.

## n8n, Make and Zapier

Each call is one HTTP step. Set it up like this in any tool:

- **Method and URL:** `POST https://api.cleanscript.ai/v1/transcript`, or `/v1/extract` for fields.
- **Header:** `Authorization` with the value `Bearer <key>`. Store it as a credential, not in the step.
- **Body (JSON):** `{"url": "<the post link>"}`, plus `prompt`, `preset` or `schema` for fields.
- **Timeout:** 2 minutes or more. Short posts answer in seconds; long videos take longer.
- **Next steps:** map `text` (transcript) or `data` (fields) from the response.

In **n8n**, use the HTTP Request node with a Header Auth credential and Send Body as JSON; set the timeout under Options. In **Make**, use HTTP, Make a request, with Parse response on. In **Zapier**, use Webhooks by Zapier, Custom Request.

If a step gives up before the answer arrives, run it again: asking again for the same thing within 6 hours is free, and a request that fails is never charged.

## Many posts from code

Your account allows a number of requests a minute (`rate_limit_per_minute` in `GET /v1/account`). Run a few requests at a time; the SDKs wait and retry when you hit the limit (`429`) or a temporary error (`503`), and send an `Idempotency-Key` so a retry is never charged twice.

```python
from concurrent.futures import ThreadPoolExecutor
from cleanscript_ai import CleanScript, CleanScriptError

client = CleanScript(max_retries=4)

def run(url):
    try:
        return url, client.transcript(url).text
    except CleanScriptError as error:
        return url, error  # error.code says why: video_unavailable, no_captions…

with ThreadPoolExecutor(max_workers=4) as pool:
    for url, result in pool.map(run, urls):
        print(url, result)
```

```typescript
import { CleanScript, CleanScriptError } from "cleanscript"

const client = new CleanScript({ maxRetries: 4 })
const queue = [...urls]
const results: { url: string; text?: string; error?: CleanScriptError }[] = []

await Promise.all(
  Array.from({ length: 4 }, async () => {
    for (let url = queue.shift(); url; url = queue.shift()) {
      try {
        results.push({ url, text: (await client.transcript(url)).text })
      } catch (error) {
        if (!(error instanceof CleanScriptError)) throw error
        results.push({ url, error }) // error.code says why
      }
    }
  })
)
```

## Handle failures by code

- **Retry later:** `rate_limited`, `source_unavailable`, `processing_failed` (the SDKs retry these for you; over HTTP, wait for `Retry-After`) and `timeout` (retry once; the result is often ready by then).
- **Skip and record:** `video_unavailable`, `no_captions`, `video_too_long`, `language_unavailable`, `invalid_url`, `unsupported_platform`. Retrying won't change the answer.
- **Stop the batch:** `insufficient_credits` (top up, then resume) and `unauthorized` (check the key).

Over HTTP, send an `Idempotency-Key` header unique to the post, such as `transcript-` plus the post ID (1 to 128 letters, digits, `.`, `_`, `:` or `-`), and reuse it when you retry that post: within 6 hours a retry returns the first successful result instead of running again. Retrying a request that failed, with the same key, runs it again.

## Before a large batch

Check your balance with `GET /v1/account` (free). Every response also carries `X-Credits-Used` and `X-Credits-Remaining`. A 10-minute video costs 1 credit for its transcript, or 3 credits with fields; see [Credits & limits](https://cleanscript.ai/docs/credits.md) for the rates.
