---
title: Batch Operations
path: api-guides/batch-operations
status: published
---

# Batch Operations

Read or update many secrets in a single request. Useful when an application needs several credentials at startup, or when a config loader wants a whole subtree at once.

**Base path:** `/v1/secrets/batch/`

## Batch read

```bash
curl -X POST https://scaivault.scailabs.ai/v1/secrets/batch \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "paths": [
      "integrations/salesforce/oauth",
      "integrations/scaisend/api-key",
      "integrations/stripe/secret-key"
    ],
    "options": {
      "include_metadata": false,
      "fail_fast": false
    }
  }'
```

```python
paths = [
    "integrations/salesforce/oauth",
    "integrations/scaisend/api-key",
    "integrations/stripe/secret-key",
]
resp = httpx.post(
    "https://scaivault.scailabs.ai/v1/secrets/batch",
    headers={"Authorization": f"Bearer {os.environ['SCAIVAULT_TOKEN']}"},
    json={"paths": paths},
)
result = resp.json()
for path, secret in result["secrets"].items():
    print(path, secret["version"])
```

```typescript
const resp = await fetch("https://scaivault.scailabs.ai/v1/secrets/batch", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAIVAULT_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    paths: [
      "integrations/salesforce/oauth",
      "integrations/scaisend/api-key",
    ],
  }),
});
const result = await resp.json();
```

Response:

```json
{
  "secrets": {
    "integrations/salesforce/oauth": {
      "data": {"client_id": "...", "client_secret": "..."},
      "version": 3,
      "secret_type": "json"
    },
    "integrations/scaisend/api-key": {
      "data": {"key": "sk_send_..."},
      "version": 1,
      "secret_type": "api_key"
    }
  },
  "errors": {
    "integrations/stripe/secret-key": {
      "code": "access_denied",
      "message": "No policy permits read on this path"
    }
  },
  "retrieved": 2,
  "failed": 1
}
```

Partial success is the default behavior — missing or denied paths show up in `errors`, everything else is in `secrets`. Pass `"fail_fast": true` to abort the whole batch on first error.

### Options

| Option | Description |
|--------|-------------|
| `include_metadata` | Include full metadata in each result (default: false) |
| `fail_fast` | Abort on first error (default: false) |
| `version` | Read all paths at a specific version. Useful for coordinated config snapshots. |

### Limits

- Maximum 100 paths per request.
- Each path is evaluated against policies individually — a batch doesn't bypass access control.
- A batch read counts as 1 call against the batch-read rate limit *and* N calls against the read rate limit (where N is the number of paths successfully returned).

## Batch metadata

Like batch read, but returns metadata only — no values. Policies still apply, but `secrets:list` is sufficient; full `secrets:read` is not required.

```bash
curl -X POST https://scaivault.scailabs.ai/v1/secrets/batch/metadata \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "paths": ["integrations/salesforce/*", "integrations/stripe/*"],
    "expand_wildcards": true
  }'
```

With `expand_wildcards: true`, path patterns are expanded server-side to the actual matching paths.

Response:

```json
{
  "metadata": {
    "integrations/salesforce/oauth": {
      "version": 3,
      "updated_at": "2026-04-20T...",
      "tags": ["salesforce", "production"]
    },
    "integrations/salesforce/webhook-secret": {
      "version": 1,
      "updated_at": "2026-02-10T..."
    }
  }
}
```

## Batch write

**Not supported.** Writes are explicitly one-at-a-time. Two reasons:

1. **Atomicity.** Batch writes imply "all or nothing" — ScaiVault has no distributed transaction to make that guarantee cleanly across versioning, policies, and audit.
2. **Auditability.** Per-write audit entries make it obvious who wrote what when. A single batch entry hides what changed.

If you need to seed many secrets, loop over `PUT /v1/secrets/{path}`. The rate limits allow bursts of ~100/min per identity.

## Startup pattern

A service that needs N secrets at startup:

```python
import os, httpx

CREDENTIALS_TO_LOAD = [
    "integrations/salesforce/oauth",
    "integrations/scaisend/api-key",
    "integrations/stripe/secret-key",
    "infra/db/primary/credentials",
    "infra/redis/primary/credentials",
]

def load_all_credentials():
    resp = httpx.post(
        "https://scaivault.scailabs.ai/v1/secrets/batch",
        headers={"Authorization": f"Bearer {os.environ['SCAIVAULT_TOKEN']}"},
        json={"paths": CREDENTIALS_TO_LOAD, "options": {"fail_fast": True}},
        timeout=10.0,
    )
    resp.raise_for_status()
    return resp.json()["secrets"]

secrets = load_all_credentials()
```

With `fail_fast: true`, any missing or denied secret fails the service startup loudly — which is what you want in this situation. A service that starts partially healthy is worse than one that fails to start.

## When to use batch vs individual reads

| Use batch | Use individual |
|-----------|----------------|
| Known fixed set of paths loaded together | Paths discovered at runtime |
| Startup / config loading | Per-request lookups (with cache) |
| Inventory and dashboards (metadata variant) | Long-lived connections with rotation subscription |

Batch isn't always faster — ScaiVault validates each path against policies and fetches each from storage. For a single path, the overhead is indistinguishable from a direct `GET`.

## What's next

- [Managing Secrets](./secrets) — the single-secret API.
- [Your First Integration](../getting-started/your-first-integration) — realistic loading patterns.
- [Python SDK](../sdks/python) — `client.secrets.batch_read(paths)`.
