
# Batch Operations

Batch endpoints write many records in a single request, so you can import, synchronize, or modify bulk data efficiently — and atomically. Each batch runs in a single transaction: if any record fails validation, the whole batch rolls back and no rows are written. Pass `returnRecords: true` to receive the affected records in the response (otherwise only a summary is returned).

| Method & Path                                     | Operation   | Max per batch |
| ------------------------------------------------- | ----------- | ------------- |
| `POST /api/tables/:tableId/records/batch`         | Create      | 1000          |
| `PATCH /api/tables/:tableId/records/batch`        | Update      | 100           |
| `DELETE /api/tables/:tableId/records/batch`       | Soft delete | 100           |
| `POST /api/tables/:tableId/records/batch/restore` | Restore     | 100           |
| `POST /api/tables/:tableId/records/upsert`        | Upsert      | 100           |

All batch bodies require the canonical envelope form (`{ "fields": { ... } }`) — the flat-body shorthand accepted by single-record create is **not** available in batch requests. Every batch must contain at least one record.

## Batch create

Send a `records` array, each item an envelope. Up to 1000 records per call.

```json
POST /api/tables/contacts/records/batch
{
  "records": [
    { "fields": { "email": "alice@example.com", "name": "Alice" } },
    { "fields": { "email": "bob@example.com", "name": "Bob" } }
  ],
  "returnRecords": true
}
```

| Status             | Meaning                                                                                   |
| ------------------ | ----------------------------------------------------------------------------------------- |
| `201 Created`      | All records created                                                                       |
| `400 Bad Request`  | Empty array, over the 1000 limit, or any record fails validation (whole batch rolls back) |
| `401 Unauthorized` | No active session                                                                         |
| `404 Not Found`    | Table not found or not visible                                                            |

## Batch update

Each item names the record `id` (string or number) plus the `fields` to patch. Up to 100 records per call.

```json
PATCH /api/tables/contacts/records/batch
{
  "records": [
    { "id": "1", "fields": { "status": "active" } },
    { "id": 2, "fields": { "status": "archived" } }
  ],
  "returnRecords": true
}
```

Updates are partial per record — only the named fields are written. If any `id` is missing or any value is invalid, the transaction rolls back.

## Batch delete

Send an `ids` array. Soft delete by default; set `permanent: true` to hard-delete already soft-deleted records (admin-only, enforced in the application layer). Up to 100 IDs per call.

```json
DELETE /api/tables/contacts/records/batch
{
  "ids": ["1", "2", 3],
  "permanent": false
}
```

:::callout
**`permanent` can also be a query string.** Route variants that keep the legacy contract accept `?permanent=true`. The JSON-body form is preferred. Permanent batch delete is irreversible and requires the `permanentDelete` permission — see [Soft Delete & Restore](records-soft-delete).
:::

## Batch restore

Recover many soft-deleted records at once. Records that are not currently deleted are skipped; a missing `id` rolls the whole batch back.

```json
POST /api/tables/contacts/records/batch/restore
{
  "ids": ["1", "2", "3"]
}
```

The restore clears each record's `deletedAt`/`deletedBy` and is logged to the record's [change history](record-history).

## Batch upsert

Create-or-update many records matched on one or more unique fields, in a single transaction. Name the merge key(s) with `fieldsToMergeOn` (alias: `matchFields`). Up to 100 records per call.

```json
POST /api/tables/contacts/records/upsert
{
  "records": [
    { "fields": { "email": "alice@example.com", "name": "Alice" } },
    { "fields": { "email": "carol@example.com", "name": "Carol" } }
  ],
  "fieldsToMergeOn": ["email"],
  "returnRecords": true
}
```

Each record is matched on the merge fields: an existing match is patched, otherwise a new row is created. Batch upsert is the canonical path for idempotent synchronization from an external source of truth.

## Limits & semantics summary

| Property        | Behavior                                                                      |
| --------------- | ----------------------------------------------------------------------------- |
| Atomicity       | Each batch is a single transaction — all-or-nothing                           |
| `returnRecords` | `false` by default; `true` returns the affected records                       |
| Minimum size    | At least one record/ID required (`400` otherwise)                             |
| Authorship      | `createdBy`/`updatedBy`/`deletedBy` stamped per record, same as single writes |
| Permissions     | RBAC and field-level permissions enforced per record                          |

## Related pages

- [CRUD & Upsert](records-crud) — single-record operations and the upsert contract
- [Soft Delete & Restore](records-soft-delete) — permanent delete and restore
- [Record History & Comments](record-history) — batch operations are logged
- [Records Overview](records-overview) — envelope and authorship rules
