---
title: Create a batch from N rows — POST /api/v1/workflows/{id}/batches
description: Pre-creates the batch and N queued child executions in a single transaction and returns 202; the rows are then processed asynchronously.
canonical_url: "https://cascades.crscreditapi.com/docs/api/batches/create-batch"
doc_version: "1.0.0"
last_updated: "2026-10-09T18:04:24Z"
source: "public/api-docs/v1/swagger.yaml#createBatch"
---

# Create a batch from N rows — POST /api/v1/workflows/{id}/batches

`POST https://cascades.crscreditapi.com/api/v1/workflows/{id}/batches`

- Operation id: `createBatch`
- Scopes: `executions:write`
- Idempotent: yes (`Idempotency-Key` header required)
- Asynchronous: returns `202 Accepted`; poll `GET /batches/{id}`
- OpenAPI: https://cascades.crscreditapi.com/openapi.json · https://cascades.crscreditapi.com/openapi.yaml (operationId `createBatch`)

Pre-creates the batch and N `queued` child executions in a single
transaction and returns `202`; the rows are then processed
asynchronously. Poll `getBatch` for the counters and
`listBatchExecutions` for per-row status. Cap of 500 rows per
request; every row must be an object. `live` requires production
access (`403 live_mode_forbidden`).

Batches need an **active** cascade, in test mode too. On a draft
(`active: false`) every batch create returns `409 workflow_inactive`
before the rows, the mode or the `Idempotency-Key` are checked. A
single test-mode `triggerWorkflow` on a draft still runs (`202`).
A sandbox account (not approved for production) cannot activate a
cascade, so it gets this 409 on every batch. To run a batch, an
approved account activates the cascade in the app, or with
`PATCH /workflows/{id}` and `active: true` from a key made at
/api-keys (docs keys cannot activate).

`Idempotency-Key` is required (`422 missing_idempotency_key`). The
key is compared against the resolved `execution_mode`,
`external_reference` and a SHA-256 digest of the rows, so reusing a
key with different content returns `409 idempotency_conflict`.
Requires `executions:write`.

## Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Workflow id. Only workflows owned by the token's account resolve; a foreign or deleted id returns 404. |

## Header parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `Idempotency-Key` | string | yes | Unique per-business-event key. Identical resubmissions return the original response; conflicting bodies return 409 idempotency_conflict. Concurrent identical requests are race-safe. A missing or blank header returns 422 missing_idempotency_key. The header is accepted up to 200 characters, but the stored key is namespaced to your client (api_client:<id>:<key>, and for re-evaluations api_client:<id>:reeval:<execution_id>:<key>) and the stored value is capped at 200 characters — so the effective limit on trigger is about 185 characters and lower still on re_evaluations. Use a UUID (36 characters) or another key of at most 128 characters to stay clear of the cap. |

## Request body

`application/json` — `BatchCreateRequest`

| Field | Type | Required | Description |
|---|---|---|---|
| `rows` | array of object | yes | One object per applicant. Every row must be an object. |
| &nbsp;&nbsp;`external_reference` | string | no |  |
| &nbsp;&nbsp;`applicant` | ApplicantPayload | no | Free-form applicant data forwarded to the workflow's first node as `context.applicant`. Field names depend on the workflow definition. |
| `execution_mode` | string | no |  One of: `"test"`, `"live"`. |
| `external_reference` | string | no |  |

```json
{
  "rows": [
    {
      "external_reference": "row-1",
      "applicant": {
        "first_name": "Ava",
        "last_name": "Stone",
        "email": "ava@example.com"
      }
    },
    {
      "external_reference": "row-2",
      "applicant": {
        "first_name": "Ben",
        "last_name": "Park",
        "email": "ben@example.com"
      }
    }
  ],
  "execution_mode": "test",
  "external_reference": "rentvine-batch-001"
}
```

## Responses

### 202 — Batch accepted

`application/json`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | Batch | no |  |
| &nbsp;&nbsp;`id` | integer | no |  |
| &nbsp;&nbsp;`object` | string | no |  |
| &nbsp;&nbsp;`workflow_id` | integer | no |  |
| &nbsp;&nbsp;`status` | string | no | `completed` means every row reached a terminal status, even when some rows failed (see `failed_count`). `failed` means the batch itself stopped on an unexpected error; it is not accepted by `filter[status]` on `listBatches`. One of: `"pending"`, `"running"`, `"interrupted"`, `"completed"`, `"cancelled"`, `"failed"`. |
| &nbsp;&nbsp;`execution_mode` | string | no |  One of: `"test"`, `"live"`. |
| &nbsp;&nbsp;`external_reference` | string, nullable | no |  |
| &nbsp;&nbsp;`total_count` | integer | no |  |
| &nbsp;&nbsp;`completed_count` | integer | no |  |
| &nbsp;&nbsp;`failed_count` | integer | no |  |
| &nbsp;&nbsp;`started_at` | string (date-time), nullable | no |  |
| &nbsp;&nbsp;`completed_at` | string (date-time), nullable | no |  |
| &nbsp;&nbsp;`created_at` | string (date-time) | no |  |
| &nbsp;&nbsp;`updated_at` | string (date-time) | no |  |
| `meta` | Meta | no |  |
| &nbsp;&nbsp;`request_id` | string | yes | Correlation id for this request. Also sent as the `X-Request-Id` response header; quote it when contacting support. |

```json
{
  "data": {
    "id": 77,
    "object": "batch",
    "workflow_id": 27,
    "status": "pending",
    "execution_mode": "test",
    "external_reference": "rentvine-batch-001",
    "total_count": 2,
    "completed_count": 0,
    "failed_count": 0,
    "started_at": null,
    "completed_at": null,
    "created_at": "2026-09-09T14:20:00Z",
    "updated_at": "2026-09-09T14:20:00Z"
  },
  "meta": {
    "request_id": "7d4c2f48-9a1b-4c3d-8e2f-1a2b3c4d5e6f"
  }
}
```

### 400 — `malformed_body`

`application/json` — `ErrorEnvelope`

```json
{
  "error": {
    "code": "validation_failed",
    "message": "The request could not be processed.",
    "details": [
      {
        "field": "execution_mode",
        "message": "must be test or live"
      }
    ],
    "request_id": "7d4c2f48-9a1b-4c3d-8e2f-1a2b3c4d5e6f",
    "doc_url": "https://cascades.crscreditapi.com/docs/api/errors/validation_failed"
  }
}
```

### 401 — Missing, malformed, expired or revoked bearer token. The `WWW-Authenticate` header carries the RFC 6750 challenge.

`application/json` — `ErrorEnvelope`

```json
{
  "error": {
    "code": "unauthorized",
    "message": "A valid bearer token is required.",
    "details": [],
    "request_id": "7d4c2f48-9a1b-4c3d-8e2f-1a2b3c4d5e6f",
    "doc_url": "https://cascades.crscreditapi.com/docs/api/errors/unauthorized"
  }
}
```

### 403 — `insufficient_scope` or `live_mode_forbidden`

`application/json` — `ErrorEnvelope`

```json
{
  "error": {
    "code": "live_mode_forbidden",
    "message": "Live mode requires production access.",
    "details": [],
    "request_id": "7d4c2f48-9a1b-4c3d-8e2f-1a2b3c4d5e6f",
    "doc_url": "https://cascades.crscreditapi.com/docs/api/errors/live_mode_forbidden"
  }
}
```

### 404 — Resource does not exist or belongs to another tenant / api_client

`application/json` — `ErrorEnvelope`

```json
{
  "error": {
    "code": "not_found",
    "message": "The requested resource was not found.",
    "details": [],
    "request_id": "7d4c2f48-9a1b-4c3d-8e2f-1a2b3c4d5e6f",
    "doc_url": "https://cascades.crscreditapi.com/docs/api/errors/not_found"
  }
}
```

### 409 — `workflow_inactive` (the cascade is a draft, in any mode) or `idempotency_conflict`

`application/json` — `ErrorEnvelope`

```json
{
  "error": {
    "code": "workflow_inactive",
    "message": "Only active workflows can run batches through the public API.",
    "details": [],
    "request_id": "7d4c2f48-9a1b-4c3d-8e2f-1a2b3c4d5e6f",
    "doc_url": "https://cascades.crscreditapi.com/docs/api/errors/workflow_inactive"
  }
}
```

### 422 — `missing_idempotency_key` or `validation_failed` (empty `rows`, more than 500 rows, a non-object row, or a bad `execution_mode`)

`application/json` — `ErrorEnvelope`

```json
{
  "error": {
    "code": "missing_idempotency_key",
    "message": "Idempotency-Key header is required.",
    "details": [],
    "request_id": "7d4c2f48-9a1b-4c3d-8e2f-1a2b3c4d5e6f",
    "doc_url": "https://cascades.crscreditapi.com/docs/api/errors/missing_idempotency_key"
  }
}
```

### 429 — Rate limit exceeded (60 req/min per IP on `/api/v1`, 600 req/hour
per token prefix, or 300 req/5 min per IP globally). The body is the
standard error envelope and `Retry-After` gives the seconds until
the window resets.


`application/json` — `ErrorEnvelope`

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry later.",
    "details": [],
    "request_id": "7d4c2f48-9a1b-4c3d-8e2f-1a2b3c4d5e6f",
    "doc_url": "https://cascades.crscreditapi.com/docs/api/errors/rate_limited"
  }
}
```

## Code samples

### cURL

```shell
curl -X POST "https://cascades.crscreditapi.com/api/v1/workflows/27/batches" \
  -H "Authorization: Bearer {{API_KEY}}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3fa85f64-5717-4562-b3fc-2c963f66afa6" \
  -d '{
    "rows": [
      {
        "external_reference": "row-1",
        "applicant": {
          "first_name": "Ava",
          "last_name": "Stone",
          "email": "ava@example.com"
        }
      },
      {
        "external_reference": "row-2",
        "applicant": {
          "first_name": "Ben",
          "last_name": "Park",
          "email": "ben@example.com"
        }
      }
    ],
    "execution_mode": "test",
    "external_reference": "rentvine-batch-001"
  }'
```

### Node.js

```javascript
const response = await fetch("https://cascades.crscreditapi.com/api/v1/workflows/27/batches", {
  method: "POST",
  headers: {
    "Authorization": "Bearer {{API_KEY}}",
    "Content-Type": "application/json",
    "Idempotency-Key": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  },
  body: JSON.stringify({
    "rows": [
      {
        "external_reference": "row-1",
        "applicant": {
          "first_name": "Ava",
          "last_name": "Stone",
          "email": "ava@example.com"
        }
      },
      {
        "external_reference": "row-2",
        "applicant": {
          "first_name": "Ben",
          "last_name": "Park",
          "email": "ben@example.com"
        }
      }
    ],
    "execution_mode": "test",
    "external_reference": "rentvine-batch-001"
  })
});

const { data, meta } = await response.json();
console.log(response.status, data, meta.request_id);
```

### Python

```python
import requests

response = requests.post(
    "https://cascades.crscreditapi.com/api/v1/workflows/27/batches",
    headers={
        "Authorization": "Bearer {{API_KEY}}",
        "Idempotency-Key": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    },
    json={
      "rows": [
        {
          "external_reference": "row-1",
          "applicant": {
            "first_name": "Ava",
            "last_name": "Stone",
            "email": "ava@example.com"
          }
        },
        {
          "external_reference": "row-2",
          "applicant": {
            "first_name": "Ben",
            "last_name": "Park",
            "email": "ben@example.com"
          }
        }
      ],
      "execution_mode": "test",
      "external_reference": "rentvine-batch-001"
    },
)
response.raise_for_status()
payload = response.json()
print(payload["data"], payload["meta"]["request_id"])
```

### Ruby

```ruby
require "net/http"
require "json"

uri = URI("https://cascades.crscreditapi.com/api/v1/workflows/27/batches")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer {{API_KEY}}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
request.body = <<~JSON
  {
    "rows": [
      {
        "external_reference": "row-1",
        "applicant": {
          "first_name": "Ava",
          "last_name": "Stone",
          "email": "ava@example.com"
        }
      },
      {
        "external_reference": "row-2",
        "applicant": {
          "first_name": "Ben",
          "last_name": "Park",
          "email": "ben@example.com"
        }
      }
    ],
    "execution_mode": "test",
    "external_reference": "rentvine-batch-001"
  }
JSON

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
payload = JSON.parse(response.body)
puts payload["data"], payload.dig("meta", "request_id")
```

### Go

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
)

func main() {
	body := []byte(`{
	  "rows": [
	    {
	      "external_reference": "row-1",
	      "applicant": {
	        "first_name": "Ava",
	        "last_name": "Stone",
	        "email": "ava@example.com"
	      }
	    },
	    {
	      "external_reference": "row-2",
	      "applicant": {
	        "first_name": "Ben",
	        "last_name": "Park",
	        "email": "ben@example.com"
	      }
	    }
	  ],
	  "execution_mode": "test",
	  "external_reference": "rentvine-batch-001"
	}`)
	req, err := http.NewRequest("POST", "https://cascades.crscreditapi.com/api/v1/workflows/27/batches", bytes.NewBuffer(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer {{API_KEY}}")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", "3fa85f64-5717-4562-b3fc-2c963f66afa6")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```

### Agent

```shell
claude mcp add cascades \
  -e CASCADES_API_KEY={{API_KEY}} \
  -e CASCADES_PROD_BASE_URL=https://cascades.crscreditapi.com \
  -- npx -y @crscreditapi/cascades-mcp-server

# Then ask your assistant:
#   "Create a batch from n rows 27 using the Cascades API."
# No MCP tool covers this endpoint yet; the assistant can call POST /workflows/{id}/batches with curl.
```

## Related

- [Batches](https://cascades.crscreditapi.com/docs/concepts/batches)
- [Idempotency](https://cascades.crscreditapi.com/docs/concepts/idempotency)
- [Sandbox vs live](https://cascades.crscreditapi.com/docs/concepts/sandbox-vs-live)

## Sitemap

See the full [sitemap](https://cascades.crscreditapi.com/sitemap.md) for all pages.
