# Batch jobs

[Bulk link data](/tools/batch-stats/) reads up to 2,000 links to posts or account pages as one batch job. You send the links once; the job reads them 50 at a time in the background, and you read the rows with its handle. Each row is charged as one call on its platform; dead links and rows that never ran cost nothing. Batches are for accounts that have bought credits.

- Page: https://socialtoai.com/docs/batches/
- Last updated: 2026-10-25

- POST: `https://api.socialtoai.com/v1/tools/batch_stats`

The inputs and the credits of each platform are on [Bulk link data in the documentation](https://socialtoai.com/docs/tools/batch-stats.md). This page is about the job.

## Submit a batch

- Send `confirm: false` first to get the quote: the most the job is charged, free. A batch runs in the background, so a request without `confirm: true` is answered with the quote too.
- Send it again with `confirm: true`. The answer is HTTP 202 with a `handle`; nothing is charged yet. The same batch sent again under the same `Idempotency-Key` returns the same handle.
- For an account that has bought credits, with a key that has the permission to start batches; otherwise HTTP 403. A link whose platform cannot be told refuses the whole request with `invalid_params`, and nothing runs.

**cURL**

```
curl https://api.socialtoai.com/v1/tools/batch_stats \
  -H "Authorization: Bearer $SOCIALTOAI_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["https://x.com/NASA"],
    "confirm": true
  }'
```

**202 Accepted** (202, 0 credits)

```
{
  "request_id": "req_00000000-0000-4000-8000-000000000003",
  "schema_version": "v1",
  "status": "running",
  "handle": "res_00000000000040008000000000000003",
  "retry_after_seconds": 5,
  "billing": {
    "cost_credits": 0,
    "balance_credits": 100,
    "today_spend_credits": 0,
    "daily_limit_credits": null,
    "daily_remaining_credits": null,
    "display": "This call: 0 credits. Balance: 100 credits (≈ $0.33)."
  }
}
```

## Read the rows

Read the job at `GET /v1/results/{handle}` with its handle, as often as you like, for free. While it runs the answer is HTTP 202 with the rows that are in so far and `retry_after_seconds`; once it has ended, HTTP 200. Add `?format=csv` for a table. The result is kept for 24 hours after the job ended.

**cURL**

```
curl https://api.socialtoai.com/v1/results/res_00000000000040008000000000000003 \
  -H "Authorization: Bearer $SOCIALTOAI_API_KEY"
```

| Field | Type | What it is |
| --- | --- | --- |
| `request_id` | string | The id of the request that started the job. |
| `schema_version` | "v1" | The version of the response format. |
| `tool` | "batch_stats" | The tool the job runs. |
| `status` | "running" \| "done" \| "stopped" \| "cancelled" \| "expired" \| "refused" | `running`, then one of the end statuses below. |
| `stop_reason` | string \| null | Why it ended early: `too_many_failed_rows`, `deadline`, or the error code a wave was refused with. |
| `handle` | string | The handle the job is read with. |
| `retry_after_seconds` | integer | While it runs: when to read again. (may be absent) |
| `progress.rows` | integer | The links sent. |
| `progress.rows_done` | integer | The rows run so far. |
| `progress.waves` | integer | The waves the job is cut into. |
| `progress.waves_done` | integer | The waves run so far. |
| `charge.quote_credits` | integer | The quote: the most the job is charged. |
| `charge.charged_credits` | integer | What the rows that ran were charged so far. |
| `main_number.id` | string | The job's main number: `engagement_total`. |
| `main_number.value` | number | The engagement of the links that work, added up. |
| `result.ok` | integer | Rows that came back. |
| `result.dead` | integer | Links that lead nowhere: deleted, private or gone. |
| `result.failed` | integer | Rows that did not come back. |
| `result.unrecognized` | integer | Links whose post or account could not be read from the link. |
| `result.not_run` | integer | Rows the job never reached. |
| `result.dead_links` | string[] | The dead links themselves. |
| `rows` | array | One row per link, in the order sent: see below. |
| `expires_at` | string \| null | Until when the result can be read; `null` while it runs. |

Each row:

| Field | Type | What it is |
| --- | --- | --- |
| `row` | integer | Its place in `urls`, from 0. |
| `url` | string | The link as sent. |
| `platform` | string | The platform the link is on. |
| `kind` | "post" \| "account" \| null | `post` or `account`; `null` when the link does not say. |
| `status` | "ok" \| "dead" \| "failed" \| "unrecognized" \| "not_run" | How the row ended. Only `ok` is charged. |
| `credits` | integer | What the row was charged. |
| `item` | object \| any \| null | The post or the account as it is now, in the [response format](https://socialtoai.com/docs/api/response.md). |
| `extra` | array | Items of a second call some platforms need for a post's numbers. |

## How a job ends

The job is cut into waves of 50 rows, run one after another. Whatever the end, the rows that ran stay charged and the rest cost nothing.

| status | What it means |
| --- | --- |
| `running` | Waves are still being run. Reading answers HTTP 202 with the rows that are in so far. |
| `done` | Every row was run. |
| `stopped` | More than 30% of the rows run so far failed, so no new wave was opened: `stop_reason` is `too_many_failed_rows`. |
| `cancelled` | You cancelled it in the console. |
| `expired` | Its 6 hours ran out: `stop_reason` is `deadline`. |
| `refused` | A wave could not be started, for example because the balance ran out: `stop_reason` is the error code. |

## What is charged

- Each row is one call on its platform, at that platform's price for a post link or an account link. A row that came back is charged, numbers of zero and all.
- A dead link, a link that could not be read and a row that never ran cost nothing. `charge.charged_credits` is never above the quote.
- A batch cannot be paused. It can be cancelled on the console's Tasks page, also when a key started it: no new wave opens, the wave in flight runs to its end and is charged, and the rows not run cost nothing. Cancelling cannot be taken back.
- When the job ends, a `batch.finished` event goes to your [webhooks](https://socialtoai.com/docs/webhooks.md).

## Over MCP

Your AI runs it with the `research` tool, `tool` set to `batch_stats`, and reads the result by calling the same tool again with the handle. [The tool list](https://socialtoai.com/docs/mcp/tools.md) has the description it is given.
