---
title: Batch access rules
description: Apply multiple access-rule changes atomically.
doc_version: 0.1.0-preview
last_updated: 2026-09-14
---

# Batch access rules

`POST /namespaces/{namespace}/collections/{collection}/access-rules:batch`

Apply multiple access-rule changes atomically.

**Permission:** Cluster key, or a scoped key with write on this collection.

[API key types and authentication](/engine/reference/authentication.md).

Access rules require an enforced collection. Rule targets use logical paths. API-key actions control writes independently of these reader rules.

Send Idempotency-Key. Retry the same logical request with the same key and body. Idempotency records remain available for replay for 24 hours.

Every operation commits, or none does. Invalid operations fail the whole batch. Error details identify the failed operation.

Reader-only batches return 200. A batch that changes governing rules returns 202 with one job. Affected targets remain hidden until it succeeds.

## Example request

```http
POST /namespaces/acme/collections/incident-logs/access-rules:batch HTTP/1.1
Host: engine.example.com
Authorization: Bearer <GRAPHON_ENGINE_API_KEY>
Content-Type: application/json
Idempotency-Key: 8f3c1e2a-4b5d-6e7f-8091-a2b3c4d5e6f7

{
  "operations": [
    {
      "op": "upsert",
      "folder": "/reports/",
      "readers": [
        "user:ada@acme.example"
      ],
      "sealed": false
    }
  ]
}
```

## Path parameters

### `namespace`

**string · required**

Namespace name or ns_ ID. It must be visible to this key.

### `collection`

**string · required**

Collection name or col_ ID within the namespace. It must be visible to this key.

## Headers

### `Idempotency-Key`

**string · required**

Unique key for this logical operation. Reuse it only with the same request for retries. Retained for 86,400 seconds. A changed body returns idempotency_conflict.

## Request body

### `operations`

**array<object> · required**

Ordered rule operations that commit together. Every operation succeeds, or none does. The 1 MiB JSON body limit bounds the batch.

#### `operations[].add_readers`

**array<string> · optional · nullable**

Principals to add to the existing readers. Existing entries remain unchanged. Do not combine with readers or repeat an entry in remove_readers.

#### `operations[].file`

**string · optional · nullable**

Exact file path, such as /reports/incident.txt. Send exactly one of file and folder. The rule may exist before the file.

#### `operations[].folder`

**string · optional · nullable**

Folder prefix, such as /reports/. Start and end with /. The root / covers the collection. Send exactly one of folder and file.

#### `operations[].op`

**string · required**

Change to apply. upsert creates or replaces and requires readers. update edits an existing rule. delete accepts only its target.

- `upsert`: Create or replace a rule.
- `update`: Change an existing rule.
- `delete`: Delete an existing rule.

#### `operations[].readers`

**array<string> · optional · nullable**

User or group principals allowed by this rule. Principals match exact strings. Maximum 1,024 readers, each at most 256 UTF-8 bytes. An empty list denies retrieval.

#### `operations[].remove_readers`

**array<string> · optional · nullable**

Principals to remove from the existing readers. Missing entries remain unchanged. Removals apply before additions. Do not combine with readers.

#### `operations[].sealed`

**boolean · optional · nullable**

Whether this folder rule overrides every rule below it. Only folder rules can be sealed. The highest sealed ancestor governs a file.

## Responses

### 202 · Batch application accepted

At least one change affects governing rules. One job applies the batch.

Content type: `application/json`.

#### Response headers

##### `x-request-id`

**string · required**

Request correlation ID. Retain it when investigating a failed or unexpected request.

```json
{
  "rules": [
    {
      "id": "acr_01K6Q6N7D8E9F0G1H2J3K4M5N6",
      "folder": "/reports/",
      "readers": [
        "user:ada@acme.example"
      ],
      "sealed": false,
      "created_at": "2026-10-06T10:00:00Z",
      "updated_at": "2026-10-06T10:00:00Z"
    }
  ],
  "job": {
    "id": "job_01K6Q6N7D8E9F0G1H2J3K4M5N6",
    "type": "apply_access_rule",
    "status": "running",
    "namespace": "acme",
    "collection": "incident-logs",
    "error": null,
    "created_at": "2026-10-06T10:00:00Z",
    "updated_at": "2026-10-06T10:00:00Z"
  }
}
```

#### Response fields

##### `job`

**object · optional · nullable**

Present when the batch changes which rule governs files. Affected files remain hidden until this job succeeds.

###### `job.collection`

**string · optional · nullable**

Collection name when this job belongs to a collection. Otherwise omitted.

###### `job.created_at`

**string (date-time) · required**

Time the resource was created, as an ISO 8601 UTC timestamp.

###### `job.error`

**object · required · nullable**

Failure details, or null when no failure is recorded.

###### `job.error.code`

**string · required**

Machine-readable reason for the failure. Use this value for program logic and message for display or diagnostics.

###### `job.error.message`

**string · required**

Human-readable explanation of the failure. Do not parse this text to decide how to retry.

###### `job.file_id`

**string · optional · nullable**

File ID when this job acts on one file. Otherwise omitted.

###### `job.id`

**string · required**

Immutable job ID with the job_ prefix. Use it to read the current state.

###### `job.namespace`

**string · optional · nullable**

Namespace name when this job belongs to a namespace.

###### `job.status`

**string · required**

Current state of the asynchronous work. succeeded, failed, and canceled are terminal states.

- `queued`: Accepted and waiting to start.
- `running`: Work is in progress.
- `succeeded`: Work completed successfully.
- `failed`: Work ended with an error.
- `canceled`: Work ended after cancellation.

###### `job.type`

**string · required**

Kind of work accepted by the API.

- `index_file`: Index a new or replacement file.
- `delete_file`: Remove a file and clean its derived data.
- `reindex_collection`: Rebuild the collection’s derived data.
- `delete_collection`: Remove a collection and its derived data.
- `delete_namespace`: Remove a namespace and its collections.
- `apply_access_rule`: Apply changed governing rules to affected files.

###### `job.updated_at`

**string (date-time) · required**

Time the resource last changed, as an ISO 8601 UTC timestamp.

##### `rules`

**array<object> · required**

Rules left by upsert and update operations, in operation order. Deleted rules are omitted.

###### `rules[].created_at`

**string (date-time) · required**

Time the resource was created, as an ISO 8601 UTC timestamp.

###### `rules[].file`

**string · optional · nullable**

Exact file path, such as /reports/incident.txt. Send exactly one of file and folder. The rule may exist before the file.

###### `rules[].folder`

**string · optional · nullable**

Folder prefix, such as /reports/. Start and end with /. The root / covers the collection. Send exactly one of folder and file.

###### `rules[].id`

**string · required**

Immutable access-rule ID with the acr_ prefix.

###### `rules[].readers`

**array<string> · required**

User or group principals allowed by this rule. Principals match exact strings. Maximum 1,024 readers, each at most 256 UTF-8 bytes. An empty list denies retrieval.

###### `rules[].sealed`

**boolean · required**

Whether this folder rule overrides every rule below it. Only folder rules can be sealed. The highest sealed ancestor governs a file.

###### `rules[].updated_at`

**string (date-time) · required**

Time the resource last changed, as an ISO 8601 UTC timestamp.

### 200 · Readers updated

A reader-only batch applies on the next request.

#### Request for this response

```http
POST /namespaces/acme/collections/incident-logs/access-rules:batch HTTP/1.1
Host: engine.example.com
Authorization: Bearer <GRAPHON_ENGINE_API_KEY>
Content-Type: application/json
Idempotency-Key: 8f3c1e2a-4b5d-6e7f-8091-a2b3c4d5e6f7

{
  "operations": [
    {
      "op": "update",
      "folder": "/reports/",
      "readers": [
        "user:ada@acme.example",
        "user:grace@acme.example"
      ]
    }
  ]
}
```

Content type: `application/json`.

#### Response headers

##### `x-request-id`

**string · required**

Request correlation ID. Retain it when investigating a failed or unexpected request.

```json
{
  "rules": [
    {
      "id": "acr_01K6Q6N7D8E9F0G1H2J3K4M5N6",
      "folder": "/reports/",
      "readers": [
        "user:ada@acme.example",
        "user:grace@acme.example"
      ],
      "sealed": false,
      "created_at": "2026-10-06T10:00:00Z",
      "updated_at": "2026-10-06T10:00:00Z"
    }
  ]
}
```

#### Response fields

##### `job`

**object · optional · nullable**

Present when the batch changes which rule governs files. Affected files remain hidden until this job succeeds.

###### `job.collection`

**string · optional · nullable**

Collection name when this job belongs to a collection. Otherwise omitted.

###### `job.created_at`

**string (date-time) · required**

Time the resource was created, as an ISO 8601 UTC timestamp.

###### `job.error`

**object · required · nullable**

Failure details, or null when no failure is recorded.

###### `job.error.code`

**string · required**

Machine-readable reason for the failure. Use this value for program logic and message for display or diagnostics.

###### `job.error.message`

**string · required**

Human-readable explanation of the failure. Do not parse this text to decide how to retry.

###### `job.file_id`

**string · optional · nullable**

File ID when this job acts on one file. Otherwise omitted.

###### `job.id`

**string · required**

Immutable job ID with the job_ prefix. Use it to read the current state.

###### `job.namespace`

**string · optional · nullable**

Namespace name when this job belongs to a namespace.

###### `job.status`

**string · required**

Current state of the asynchronous work. succeeded, failed, and canceled are terminal states.

- `queued`: Accepted and waiting to start.
- `running`: Work is in progress.
- `succeeded`: Work completed successfully.
- `failed`: Work ended with an error.
- `canceled`: Work ended after cancellation.

###### `job.type`

**string · required**

Kind of work accepted by the API.

- `index_file`: Index a new or replacement file.
- `delete_file`: Remove a file and clean its derived data.
- `reindex_collection`: Rebuild the collection’s derived data.
- `delete_collection`: Remove a collection and its derived data.
- `delete_namespace`: Remove a namespace and its collections.
- `apply_access_rule`: Apply changed governing rules to affected files.

###### `job.updated_at`

**string (date-time) · required**

Time the resource last changed, as an ISO 8601 UTC timestamp.

##### `rules`

**array<object> · required**

Rules left by upsert and update operations, in operation order. Deleted rules are omitted.

###### `rules[].created_at`

**string (date-time) · required**

Time the resource was created, as an ISO 8601 UTC timestamp.

###### `rules[].file`

**string · optional · nullable**

Exact file path, such as /reports/incident.txt. Send exactly one of file and folder. The rule may exist before the file.

###### `rules[].folder`

**string · optional · nullable**

Folder prefix, such as /reports/. Start and end with /. The root / covers the collection. Send exactly one of folder and file.

###### `rules[].id`

**string · required**

Immutable access-rule ID with the acr_ prefix.

###### `rules[].readers`

**array<string> · required**

User or group principals allowed by this rule. Principals match exact strings. Maximum 1,024 readers, each at most 256 UTF-8 bytes. An empty list denies retrieval.

###### `rules[].sealed`

**boolean · required**

Whether this folder rule overrides every rule below it. Only folder rules can be sealed. The highest sealed ancestor governs a file.

###### `rules[].updated_at`

**string (date-time) · required**

Time the resource last changed, as an ISO 8601 UTC timestamp.

## Errors

[Error format and retry guidance](/engine/reference/errors.md).

### 400 · `invalid_request`

A field, path, parameter, or conditional request rule is invalid.

Correct the request. Unknown fields are rejected.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "A field, path, parameter, or conditional request rule is invalid.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 401 · `unauthenticated`

The API key is missing, invalid, or revoked.

Send an active key in Authorization: Bearer or X-API-Key.

```json
{
  "error": {
    "code": "unauthenticated",
    "message": "The API key is missing, invalid, or revoked.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 403 · `forbidden`

The key can see the resource but lacks permission for this operation.

Use a key with the required action or scope kind.

```json
{
  "error": {
    "code": "forbidden",
    "message": "The key can see the resource but lacks permission for this operation.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 404 · `not_found`

The resource is absent, deleted, or outside this key’s scope.

Check the identifier and the key’s namespace and collection scope.

```json
{
  "error": {
    "code": "not_found",
    "message": "The resource is absent, deleted, or outside this key’s scope.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 409 · `idempotency_conflict`

This idempotency key was already used with a different request body.

Reuse the original body for a retry, or choose a new key for a new operation.

```json
{
  "error": {
    "code": "idempotency_conflict",
    "message": "This idempotency key was already used with a different request body.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 409 · `operation_in_progress`

An unfinished operation conflicts with this request.

Inspect the existing job and wait for its completion event before retrying.

```json
{
  "error": {
    "code": "operation_in_progress",
    "message": "An unfinished operation conflicts with this request.",
    "request_id": "req_example",
    "retryable": true,
    "details": {}
  }
}
```

### 409 · `limit_exceeded`

The operation would exceed a documented size or count limit.

Reduce the input or resource count. This is not a tenant billing limit.

```json
{
  "error": {
    "code": "limit_exceeded",
    "message": "The operation would exceed a documented size or count limit.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 413 · `request_too_large`

The JSON request body exceeds 1,048,576 bytes.

Reduce the JSON body. Use upload or a URI source for larger file content.

```json
{
  "error": {
    "code": "request_too_large",
    "message": "The JSON request body exceeds 1,048,576 bytes.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 503 · `temporarily_unavailable`

A dependency, cluster readiness condition, or capacity limit prevents this operation.

Inspect retryable and details. Retry only when permitted, preserving the idempotency key for the same write.

```json
{
  "error": {
    "code": "temporarily_unavailable",
    "message": "A dependency, cluster readiness condition, or capacity limit prevents this operation.",
    "request_id": "req_example",
    "retryable": true,
    "details": {}
  }
}
```

### 500 · `internal_error`

An unexpected server failure prevented this request from completing.

Retain x-request-id for investigation. Follow retryable before retrying a write.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An unexpected server failure prevented this request from completing.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

## Other request languages

### cURL

```bash
curl -sS -X POST "$GRAPHON_ENGINE_URL/namespaces/acme/collections/incident-logs/access-rules:batch" \
  -H "Authorization: Bearer $GRAPHON_ENGINE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 8f3c1e2a-4b5d-6e7f-8091-a2b3c4d5e6f7' \
  --data-raw '{
  "operations": [
    {
      "op": "upsert",
      "folder": "/reports/",
      "readers": [
        "user:ada@acme.example"
      ],
      "sealed": false
    }
  ]
}'
```

### Python

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    os.environ["GRAPHON_ENGINE_URL"] + "/namespaces/acme/collections/incident-logs/access-rules:batch",
    data=json.dumps({"operations": [{"op": "upsert", "folder": "/reports/", "readers": ["user:ada@acme.example"], "sealed": False}]}).encode("utf-8"),
    headers={
        "Authorization": "Bearer " + os.environ["GRAPHON_ENGINE_API_KEY"],
        "Content-Type": "application/json",
        "Idempotency-Key": "8f3c1e2a-4b5d-6e7f-8091-a2b3c4d5e6f7",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.read().decode())
```

### JavaScript

```javascript
const response = await fetch(process.env.GRAPHON_ENGINE_URL + "/namespaces/acme/collections/incident-logs/access-rules:batch", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GRAPHON_ENGINE_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "8f3c1e2a-4b5d-6e7f-8091-a2b3c4d5e6f7",
  },
  body: JSON.stringify({"operations":[{"op":"upsert","folder":"/reports/","readers":["user:ada@acme.example"],"sealed":false}]}),
});
console.log(response.status, await response.text());
```

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
