---
title: Bulk Operations
path: tutorials/bulk-operations
status: published
---

# Bulk Operations

Create, update, or delete many records in a single request. Atomic by default — all succeed or nothing changes.

**Base path:** `/api/v1/domains/{domain_id}/records/bulk*`
**Required permission:** `records:create`, `records:delete`

## Bulk create

```bash
curl -X POST https://scaidns.scailabs.ai/api/v1/domains/$DOMAIN_ID/records/bulk \
  -H "X-API-Key: $SCAIDNS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {"name": "app1", "type": "A", "content": "192.0.2.10", "ttl": 300},
      {"name": "app2", "type": "A", "content": "192.0.2.11", "ttl": 300},
      {"name": "app3", "type": "A", "content": "192.0.2.12", "ttl": 300}
    ],
    "continue_on_error": false
  }'
```

```python
records = [
    {"name": f"app{i}", "type": "A", "content": f"192.0.2.{10+i}", "ttl": 300}
    for i in range(100)
]

resp = httpx.post(
    f"https://scaidns.scailabs.ai/api/v1/domains/{DOMAIN_ID}/records/bulk",
    headers={"X-API-Key": os.environ["SCAIDNS_API_KEY"]},
    json={"records": records, "continue_on_error": False},
)
result = resp.json()
print(f"Created: {result['total_created']}, errors: {result['total_errors']}")
```

```typescript
const records = Array.from({ length: 100 }, (_, i) => ({
  name: `app${i}`,
  type: "A",
  content: `192.0.2.${10 + i}`,
  ttl: 300,
}));

const resp = await fetch(
  `https://scaidns.scailabs.ai/api/v1/domains/${domainId}/records/bulk`,
  {
    method: "POST",
    headers: {
      "X-API-Key": process.env.SCAIDNS_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ records, continue_on_error: false }),
  },
);
const result = await resp.json();
```

### Request

| Field | Type | Required | Notes |
|-------|------|---------|-------|
| `records` | array | Yes | Same shape as single-record create; up to 500 records per request |
| `continue_on_error` | boolean | No | Default false (atomic) |

### Response

```json
{
  "created": [
    {"id": "r_1", "name": "app1.example.com", "type": "A", "content": "192.0.2.10", "ttl": 300}
  ],
  "errors": [
    {"index": 2, "name": "app3", "type": "A", "error": "IP conflict with record r_existing"}
  ],
  "total_created": 99,
  "total_errors": 1
}
```

When `continue_on_error: false` (default), any error fails the whole batch and `created` is empty. When `true`, partial success is possible.

## Bulk delete

```bash
curl -X POST https://scaidns.scailabs.ai/api/v1/domains/$DOMAIN_ID/records/bulk-delete \
  -H "X-API-Key: $SCAIDNS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "record_ids": ["r_abc", "r_def", "r_ghi"]
  }'
```

Response:

```json
{
  "deleted": 3,
  "errors": [],
  "total_deleted": 3,
  "total_errors": 0
}
```

## Atomicity

When `continue_on_error: false` (default):

- Validation runs first against every record.
- If anything fails validation (invalid content, conflicts, permission), the request returns `4xx` and nothing is written.
- Otherwise, all records are committed in a single transaction.

When `continue_on_error: true`:

- Each record is processed independently.
- Successes are returned in `created`, failures in `errors`.
- Partial success is possible — the request always returns `200`.

Use atomic mode for configuration changes where partial application would leave the zone in an inconsistent state. Use `continue_on_error: true` for opportunistic imports where you want to see what got in.

## Limits

- **Max records per request:** 500. For larger imports, chunk into batches.
- **Payload size:** 10 MB default. Lower if behind a proxy with smaller limits.
- **Rate limits:** Bulk calls count as a single request against your API key's rate limit. The underlying record count isn't multiplied.

## Patterns

### Split across multiple zones

The bulk endpoint operates on a single zone. If you need to provision records across many zones, loop at the application layer:

```python
for zone_id, records in by_zone.items():
    resp = httpx.post(
        f"https://scaidns.scailabs.ai/api/v1/domains/{zone_id}/records/bulk",
        headers={"X-API-Key": os.environ["SCAIDNS_API_KEY"]},
        json={"records": records},
    )
    resp.raise_for_status()
```

### Full zone replacement via bulk-delete + bulk-create

```python
# Get current records
resp = httpx.get(
    f"https://scaidns.scailabs.ai/api/v1/domains/{DOMAIN_ID}/records",
    headers=HEADERS,
)
existing_ids = [r["id"] for r in resp.json()["data"]]

# Delete them all
if existing_ids:
    httpx.post(
        f"https://scaidns.scailabs.ai/api/v1/domains/{DOMAIN_ID}/records/bulk-delete",
        headers=HEADERS,
        json={"record_ids": existing_ids},
    ).raise_for_status()

# Create the new set
httpx.post(
    f"https://scaidns.scailabs.ai/api/v1/domains/{DOMAIN_ID}/records/bulk",
    headers=HEADERS,
    json={"records": new_records, "continue_on_error": False},
).raise_for_status()
```

For a proper atomic replace, prefer import with `mode: replace` — see [Import and Export](./import-export.md).

### Idempotent bulk creates

Bulk create rejects duplicates that match `(name, type, content)`. To make repeated calls safe:

- Pre-check with `GET /records?type=A&name=app1`.
- Or set `continue_on_error: true` and filter errors for "already exists" messages.
- Or use import with `mode: skip_existing` for larger batches.

## Errors

| Status | Cause |
|--------|-------|
| `400` | Empty `records` array, or per-record validation failed in atomic mode |
| `403` | Caller lacks `records:create` / `records:delete` on this domain |
| `409` | Conflict with existing record (atomic mode) |
| `413` | Payload too large |
| `429` | Rate limit exceeded |

The response body details which record failed when relevant.

## What's next

- [Managing Records](./managing-records.md) — single-record operations.
- [Import and Export](./import-export.md) — whole-zone imports.
- [Records reference](../reference/records.md) — endpoint details.
