# DataGuard API Integration Guide

This document describes the DataGuard HTTP API so your engineering team can integrate directly — no DataGuard client library required.

All endpoints are JSON over HTTPS. All traffic is authenticated with a per-organization Bearer API key issued through the DataGuard admin portal.

---

## 1. Base URL

```
https://api.sidian.io/org/{your-org-slug}
```

Every example below assumes this as `BASE`. Your org slug is shown on the **API Integration** page of the admin portal; the rest of the docs there wraps this same reference.

---

## 2. Authentication

Every request must include an `Authorization` header with a Bearer token:

```
Authorization: Bearer dg_live_YOUR_KEY
```

Keys begin with `dg_live_`. They are issued to a single organization and must never be exposed in browser code, mobile app bundles, or public repositories. Resolve them server-side from an environment variable or secrets manager.

If the key is missing or invalid you'll get `401 Unauthorized`. If the key is valid but doesn't belong to the org slug in the URL you'll get `403 Forbidden` with `error: "SLUG_MISMATCH"`.

---

## 3. Endpoint reference

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/redact` | Rule-based file redaction. Returns the redacted file. |
| `POST` | `/analyze/review` | Full analysis, returns per-entity recommendations (no mutation). |
| `POST` | `/analyze/auto` | Full analysis + automatic redaction. Returns the redacted file. |
| `POST` | `/scan` | Raw NER scan — every entity, no policy applied. |
| `POST` | `/uploads` | Open a multipart upload session (for files > ~5 MB). |
| `PUT`  | `/uploads/{uploadId}/parts/{partNumber}` | Stream a single part of a multipart upload. |
| `POST` | `/uploads/{uploadId}/complete` | Finalize a multipart upload; returns a reusable `source` handle. |
| `DELETE` | `/uploads/{uploadId}` | Abort an in-flight multipart upload. |

All endpoints are scoped to your org slug — the slug prefix shown in section 1 is implicit in the table above.

---

## 4. The `source` object

Every content-consuming endpoint (`/redact`, `/analyze/*`, `/scan`) takes a top-level `source` object that tells DataGuard where the content lives. It's a discriminated union on `type`.

| `type` | Required fields | When to use |
|---|---|---|
| `text`      | `content: string`, `filename?: string` | Plain text — email bodies, notes, snippets. |
| `url`       | `url: string`, `filename?: string` | A publicly reachable URL — DataGuard fetches it server-side. |
| `content`   | `base64Content: string`, `filename: string`, `mimeType?: string` | Inline base64-encoded file. Keep this path for files under a few MB. |
| `multipart` | `uploadId: string` (UUID from `/uploads/.../complete`) | Files larger than a few MB, or when you need resumable chunked uploads. |

`filename` isn't strictly required for every `type`, but provide it whenever you can — the extension drives processor routing and entity-detection heuristics.

### Supported file types for `/redact` and `/analyze/auto`

| Extension | Processor | Notes |
|---|---|---|
| `.pdf`           | pdf-processor  | Native PDF redaction. |
| `.docx`          | pdf-processor  | Converted to PDF via Gotenberg, then redacted. |
| `.pptx`          | pdf-processor  | Converted to PDF via Gotenberg, then redacted. |
| `.xlsx` / `.xls` | xlsx-processor | Native spreadsheet redaction; sheet structure preserved. |
| `.json`          | lazy-lagoon    | Field-level redact/exclude by JSON dot-path (e.g. `user.email`). |
| `.csv`           | lazy-lagoon    | Column redact/exclude by header name, with optional filter expressions. |

---

## 5. `POST /redact`

Pure rule-based redaction. No NER, no policy evaluation — DataGuard applies exactly the rules you provide and returns the resulting file.

### Request

```jsonc
{
  "source": { /* content | multipart */ },
  "rules": {
    "pdf":  [ /* PdfRule[]        */ ],
    "xlsx": [ /* XlsxRule[]       */ ],
    "json": [ /* StructuredRule[] */ ],
    "csv":  [ /* StructuredRule[] */ ]
  }
}
```

At least one non-empty `rules.*` array is required. Only the rule array matching the file's extension is evaluated — extras are ignored.

### Response — `200 OK`

```jsonc
{
  "base64Content": "JVBERi0xLjQg...",
  "filename":      "contract.pdf",
  "mimeType":      "application/pdf"
}
```

Decode `base64Content` to get the redacted file bytes.

---

## 6. Rule schemas

**Action field case matters — always UPPERCASE.** `REDACT` and `EXCLUDE` are the only accepted values across all rule types. Lowercase is rejected at validation.

### 6.1 PDF / DOCX / PPTX — `rules.pdf[]`

Each rule targets text inside an unstructured document.

| Field | Type | Values |
|---|---|---|
| `action` | string | `"REDACT"` = replace with a black bar. `"EXCLUDE"` = remove content entirely. |
| `target` | string | `"WORDS"` (word/phrase match) · `"PARAGRAPHS"` (entire paragraph) · `"PAGES"` (entire page). |
| `text`   | string | Literal text to match, case-sensitive. Required for `WORDS` and `PARAGRAPHS`. |
| `pages`  | `number[]` (optional) | 1-indexed page filter. Omit or pass `[]` to apply to all pages. |

```jsonc
{
  "source": { "type": "content", "base64Content": "<base64>", "filename": "report.pdf" },
  "rules": {
    "pdf": [
      { "action": "REDACT",  "target": "WORDS",      "text": "ACME Corp",    "pages": [1, 3] },
      { "action": "REDACT",  "target": "PARAGRAPHS", "text": "Confidential", "pages": [] },
      { "action": "EXCLUDE", "target": "PAGES",      "text": "",             "pages": [5] }
    ]
  }
}
```

### 6.2 XLSX / XLS — `rules.xlsx[]`

Each rule targets one sheet via `pageCondition` plus one or more cell actions.

| Field | Type | Values |
|---|---|---|
| `pageCondition.sheetName`          | string  | Exact tab name, case-sensitive. Auto-generated rules default to `"Sheet1"`. |
| `pageCondition.includeFormulas`    | boolean | `false` (default) clears formulas before redaction. `true` keeps them. |
| `pageCondition.nonEmptyValueRedact`| boolean | `true` only redacts non-empty cells. |
| `actions[].actionType`             | string  | `"REDACT"` or `"EXCLUDE"`. |
| `actions[].operation`              | string  | `VALUE` · `RANGE` · `TEXT_COLOR` · `BG_COLOR` · `COLUMN` · `ROW`. |
| `actions[].value`                  | string  | Operand — see matrix below. |

| Operation | Example `value` | Description |
|---|---|---|
| `VALUE`      | `"John Smith"` | Exact cell value match. |
| `RANGE`      | `"C4:D9"`      | Excel-style range. |
| `TEXT_COLOR` | `"0070C0"`     | Font color hex, no `#`. |
| `BG_COLOR`   | `"FF0000"`     | Fill color hex, no `#`. |
| `COLUMN`     | `"C"`          | Entire column by letter — pair with `EXCLUDE`. |
| `ROW`        | `"4"`          | Entire row by number — pair with `EXCLUDE`. |

```jsonc
{
  "source": { "type": "content", "base64Content": "<base64>", "filename": "data.xlsx" },
  "rules": {
    "xlsx": [
      {
        "pageCondition": { "sheetName": "Employees", "includeFormulas": false, "nonEmptyValueRedact": true },
        "actions": [
          { "actionType": "REDACT",  "operation": "VALUE",      "value": "John Smith" },
          { "actionType": "REDACT",  "operation": "RANGE",      "value": "C4:D9" },
          { "actionType": "EXCLUDE", "operation": "COLUMN",     "value": "F" }
        ]
      }
    ]
  }
}
```

### 6.3 JSON / CSV — `rules.json[]` and `rules.csv[]`

Each rule has an optional `expression` (filter) plus one or more field-level `actions`.

| Field | Type | Values |
|---|---|---|
| `expression`                              | object \| null | `null` = apply to every record. Otherwise a clause group. |
| `expression.logicalOperator`              | string         | `"AND"` or `"OR"`. |
| `expression.expressions[]`                | object[]       | Clauses: `{ fieldName, operator, value }`. |
| `expression.expressions[].fieldName`      | string         | JSON dot-path (e.g. `user.email`, supports `[*]` for arrays) or CSV column header. |
| `expression.expressions[].operator`       | string         | `equals` · `notEquals` · `greaterThan` · `greaterThanOrEqual` · `lessThan` · `lessThanOrEqual` · `contains` · `startsWith` · `endsWith`. |
| `expression.expressions[].value`          | string \| number \| boolean | Compared against the field's value. |
| `actions[].actionType`                    | string         | `"REDACT"` (replaces value with `[REDACTED]`) or `"EXCLUDE"` (removes the field/column). |
| `actions[].fieldName`                     | string         | Field to act on — JSON dot-path or CSV column header. |

```jsonc
// JSON — redact one field for every record
{
  "source": { "type": "content", "base64Content": "<base64>", "filename": "users.json" },
  "rules": {
    "json": [
      {
        "expression": null,
        "actions": [ { "actionType": "REDACT", "fieldName": "user.email" } ]
      }
    ]
  }
}

// CSV — drop the "ssn" column only for US rows
{
  "source": { "type": "content", "base64Content": "<base64>", "filename": "customers.csv" },
  "rules": {
    "csv": [
      {
        "expression": {
          "logicalOperator": "AND",
          "expressions": [ { "fieldName": "country", "operator": "equals", "value": "US" } ]
        },
        "actions": [ { "actionType": "EXCLUDE", "fieldName": "ssn" } ]
      }
    ]
  }
}
```

---

## 7. `POST /analyze/review`

Runs the full analysis pipeline (NER + policy evaluation) and returns a per-entity review. No file is produced. Accepts every `source.type`.

### Request

```json
{
  "source": {
    "type": "text",
    "content": "John Smith SSN 123-45-6789 was treated at Mayo Clinic.",
    "filename": "note.txt"
  }
}
```

### Response — `200 OK`

```jsonc
{
  "review": [
    {
      "category": "SSN",
      "action": "REDACT",
      "reference": "HIPAA § 164.514(b)(2)(i)",
      "items": ["123-45-6789"],
      "recommendation": "Redact before sharing — restricted under policy."
    },
    {
      "category": "PERSON_NAME",
      "action": "REVIEW",
      "reference": "HIPAA § 164.514(b)",
      "items": ["John Smith"],
      "recommendation": "Review sharing context — may require consent."
    }
  ],
  "summary": "2 restricted entities found — SSN, PERSON_NAME.",
  "stats": { "entitiesFound": 2, "processingTimeMs": 912 }
}
```

Entities whose policy action is `RETAIN` are omitted from `review[]`. `entities`, `summary`, and `stats` are included per your API key's response-format configuration.

---

## 8. `POST /analyze/auto`

Analysis **and** redaction in one call. DataGuard runs NER + policy evaluation, auto-generates the appropriate rules from every `REDACT`-action entity it finds, dispatches the file to the right processor, and returns the redacted bytes plus metadata. Accepts `source.type` of `content` or `multipart`.

### Request

```json
{
  "source": {
    "type": "content",
    "base64Content": "<base64>",
    "filename": "patient-records.pdf"
  }
}
```

Or with a multipart upload:

```json
{
  "source": { "type": "multipart", "uploadId": "23ab52be-2b7e-4fe6-956d-ae813aedf8c3" }
}
```

### Response — `200 OK`

```jsonc
{
  "base64Content": "JVBERi0xLjQ...",
  "filename": "patient-records.pdf",
  "mimeType": "application/pdf",
  "entities": {
    "SSN": { "action": "REDACT", "reference": "HIPAA § 164.514(b)(2)(i)", "items": ["123-45-6789"] }
  },
  "summary": "1 restricted entry found — SSN.",
  "stats": { "entitiesFound": 1, "processingTimeMs": 2341 }
}
```

Auto-generated rules are:

- `WORDS` for PDF / DOCX / PPTX
- `VALUE` for XLSX (targeting `Sheet1`)
- Path-based `REDACT` actions for JSON / CSV

Use `/redact` with your own `rules[]` when you need to target named XLSX sheets, use advanced operations (`RANGE`, `BG_COLOR`, expressions), or skip the analysis step entirely.

---

## 9. `POST /scan`

Raw NER — returns every detected entity regardless of policy. Useful for diagnostics and for building custom rule generators.

### Request

```json
{
  "source": {
    "type": "text",
    "content": "Contact Jane at jane.doe@acme.com or +1 415 555 0100.",
    "filename": "email.txt"
  }
}
```

### Response — `200 OK`

```jsonc
{
  "entities": {
    "PERSON_NAME": { "items": ["Jane"] },
    "EMAIL":       { "items": ["jane.doe@acme.com"] },
    "PHONE":       { "items": ["+1 415 555 0100"] }
  },
  "stats": { "entitiesFound": 3, "processingTimeMs": 412 }
}
```

No policy action, no recommendation — just what NER saw.

---

## 10. Multipart uploads

Use this flow whenever your file is larger than a few MB. DataGuard stores parts on an attested-encrypted scratch volume and hands you back an `uploadId` you can pass into `/redact`, `/analyze/auto`, `/analyze/review`, or `/scan` as `source: { type: "multipart", uploadId }`.

### 10.1 Lifecycle

| Step | Method | Path | Purpose |
|---|---|---|---|
| 1 | `POST`   | `/uploads`                                | Open a session. Returns `uploadId`, `partSize`, `expiresAt`. |
| 2 | `PUT`    | `/uploads/{uploadId}/parts/{partNumber}`  | Stream a single part (raw bytes). Returns that part's SHA-256 `etag`. |
| 3 | `POST`   | `/uploads/{uploadId}/complete`            | Finalize with the full list of `{ partNumber, etag }`. |
| 4 | `POST`   | `/redact`, `/analyze/*`, `/scan`          | Consume with `source: { type: "multipart", uploadId }`. |
| — | `DELETE` | `/uploads/{uploadId}`                     | Abort and release scratch storage. |

### 10.2 Constraints

| Limit | Value |
|---|---|
| Minimum part size (non-final parts) | 5 MiB |
| Maximum part size                   | 5 GiB |
| Default part size                   | 10 MiB |
| Maximum parts per upload            | 10,000 |
| Maximum assembled file size         | 5 GiB |
| Max bytes materialized for analysis | 250 MiB (larger files work with `/redact` but not with analysis endpoints) |
| Concurrent uploads per org          | 50 |
| Initiate rate limit                 | 60 / hour / org |
| Session TTL (initiated → ready)     | 24 h |
| Ready TTL (ready → consumed)        | 1 h |
| Part inactivity TTL                 | 15 min |

### 10.3 `POST /uploads` — initiate

Request body:

```jsonc
{
  "filename": "quarterly-report.pdf",
  "mimeType": "application/pdf",   // optional, verified at complete
  "size":     12582912,            // optional; enforced ≤ 5 GiB if provided
  "partSize": 10485760             // optional; min 5 MiB, max 5 GiB, default 10 MiB
}
```

Response `201 Created`:

```jsonc
{
  "uploadId":    "23ab52be-2b7e-4fe6-956d-ae813aedf8c3",
  "partSize":    10485760,
  "minPartSize": 5242880,
  "maxParts":    10000,
  "expiresAt":   "2026-04-23T20:08:57.395Z"
}
```

### 10.4 `PUT /uploads/{uploadId}/parts/{partNumber}` — upload a part

- `partNumber` is 1-indexed, up to `10000`.
- Body is **raw binary bytes** — not JSON, not base64.
- Set `Content-Type: application/octet-stream`.
- Every part except the **last** must be ≥ `minPartSize` returned at initiate.

Response `200 OK`:

```json
{
  "partNumber": 1,
  "etag":       "f50dcfc76f8e2be83cee21e6d25e9bf15c92761b2318847858c46d380539b69f",
  "size":       5242880
}
```

Save every `etag` — you have to pass them back at complete.

### 10.5 `POST /uploads/{uploadId}/complete` — finalize

Request:

```json
{
  "parts": [
    { "partNumber": 1, "etag": "..." },
    { "partNumber": 2, "etag": "..." },
    { "partNumber": 3, "etag": "..." }
  ]
}
```

Parts can be supplied in any order but must form a contiguous `1..N` sequence. The server assembles, verifies each SHA-256, sniffs the assembled MIME against the declared filename, and marks the session ready.

Response `200 OK`:

```json
{
  "uploadId": "23ab52be-2b7e-4fe6-956d-ae813aedf8c3",
  "filename": "quarterly-report.pdf",
  "size":     12582912,
  "mimeType": "application/pdf",
  "source":   { "type": "multipart", "uploadId": "23ab52be-2b7e-4fe6-956d-ae813aedf8c3" }
}
```

The `source` object in the response is exactly what you pass into `/redact`, `/analyze/*`, or `/scan`.

### 10.6 `DELETE /uploads/{uploadId}` — abort

No body. Returns `204 No Content`. Idempotent as long as the session isn't currently being consumed.

### 10.7 Single-consume semantics

An `uploadId` may only be consumed **once** — after `/redact`, `/analyze/*`, or `/scan` reads it, its scratch file is released. If you need to run multiple operations on the same bytes, create a separate upload for each, or consume once with `/analyze/auto` (which analyzes and redacts in a single call).

### 10.8 End-to-end example — bash

```bash
BASE="https://api.sidian.io/org/${ORG_SLUG}"
KEY="$DATAGUARD_API_KEY"

# 1. Initiate
INIT=$(curl -s -X POST "$BASE/uploads" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename":"quarterly-report.pdf","mimeType":"application/pdf"}')

UPLOAD_ID=$(echo "$INIT" | jq -r .uploadId)

# 2. Upload each part — raw bytes
ETAG1=$(curl -s -X PUT "$BASE/uploads/$UPLOAD_ID/parts/1" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @chunk_001.bin | jq -r .etag)
# ... repeat for parts 2..N ...

# 3. Complete
curl -s -X POST "$BASE/uploads/$UPLOAD_ID/complete" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d "{\"parts\":[{\"partNumber\":1,\"etag\":\"$ETAG1\"}]}"

# 4. Consume the uploadId
curl -s -X POST "$BASE/redact" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"source\": { \"type\": \"multipart\", \"uploadId\": \"$UPLOAD_ID\" },
    \"rules\":  { \"pdf\": [ { \"action\": \"REDACT\", \"target\": \"WORDS\", \"text\": \"Confidential\" } ] }
  }"
```

### 10.9 End-to-end example — TypeScript

```ts
import fetch from "node-fetch";
import { readFile } from "fs/promises";

const KEY  = process.env.DATAGUARD_API_KEY!;
const BASE = `https://api.sidian.io/org/${process.env.ORG_SLUG}`;

async function uploadFile(path: string, filename: string, mimeType: string) {
  const bytes = await readFile(path);

  // 1. Initiate
  const init = await fetch(`${BASE}/uploads`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ filename, mimeType, size: bytes.length }),
  }).then((r) => r.json()) as { uploadId: string; partSize: number };

  // 2. PUT parts in order
  const parts: { partNumber: number; etag: string }[] = [];
  for (let offset = 0, n = 1; offset < bytes.length; offset += init.partSize, n++) {
    const chunk = bytes.subarray(offset, offset + init.partSize);
    const res = await fetch(`${BASE}/uploads/${init.uploadId}/parts/${n}`, {
      method: "PUT",
      headers: {
        "Authorization": `Bearer ${KEY}`,
        "Content-Type": "application/octet-stream",
      },
      body: chunk,
    }).then((r) => r.json()) as { etag: string };
    parts.push({ partNumber: n, etag: res.etag });
  }

  // 3. Complete
  await fetch(`${BASE}/uploads/${init.uploadId}/complete`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ parts }),
  });

  return init.uploadId;
}

// 4. Pass uploadId into /redact, /analyze/auto, /analyze/review, or /scan.
const uploadId = await uploadFile("./report.pdf", "report.pdf", "application/pdf");
await fetch(`${BASE}/redact`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    source: { type: "multipart", uploadId },
    rules: { pdf: [{ action: "REDACT", target: "WORDS", text: "Confidential" }] },
  }),
});
```

---

## 11. Error responses

All errors return a JSON body:

```json
{ "error": "ERROR_CODE", "message": "Human-readable explanation", "details": { } }
```

### 11.1 Common HTTP status codes

| Status | When it happens |
|---|---|
| `200 OK`                      | Request succeeded. |
| `201 Created`                 | Resource created (e.g. multipart session at `POST /uploads`). |
| `204 No Content`              | Success with no body (`DELETE /uploads/{uploadId}`). |
| `400 Bad Request`             | Malformed body, schema validation failure, bad part number / etag, MIME mismatch on complete. |
| `401 Unauthorized`            | API key missing or invalid. |
| `403 Forbidden`               | Key valid but wrong org / wrong `uploadId`. |
| `404 Not Found`               | Unknown `uploadId` or resource. |
| `409 Conflict`                | Session state doesn't permit the operation — `UPLOAD_NOT_READY`, `UPLOAD_ALREADY_COMPLETED`. |
| `410 Gone`                    | Multipart session expired, aborted, or already released after consumption. |
| `413 Payload Too Large`       | Declared or assembled file exceeds the 5 GiB cap. |
| `422 Unprocessable Entity`    | Unsupported file extension, or content received but can't be processed. |
| `429 Too Many Requests`       | Concurrent-upload cap (50/org) or initiate rate (60/hr/org) exceeded. Back off and retry. |
| `500 Internal Server Error`   | Server-side failure — contact `support@sidian.io` with the timestamp if this persists. |
| `503 Service Unavailable`     | Temporary — usually `SCRATCH_FULL`. Retry with exponential backoff. |

### 11.2 Multipart-specific error codes

| HTTP | `error` | When |
|---|---|---|
| 400 | `INVALID_PART_NUMBER`      | `partNumber` outside `1..10000`. |
| 400 | `INVALID_PART_SIZE`        | Non-final part below min, or any part over max. |
| 400 | `ETAG_MISMATCH`            | Client etag doesn't match server-computed SHA-256. |
| 400 | `MISSING_PARTS`            | `/complete` parts array is non-contiguous or missing an uploaded part. |
| 400 | `MIME_MISMATCH`            | Declared MIME / filename extension disagrees with sniffed bytes at complete. |
| 403 | `UPLOAD_FORBIDDEN`         | `uploadId` belongs to a different organization. |
| 404 | `UPLOAD_NOT_FOUND`         | Unknown `uploadId`. |
| 409 | `UPLOAD_ALREADY_COMPLETED` | Session already completed — no more parts accepted. |
| 409 | `UPLOAD_NOT_READY`         | Consumer hit an `uploadId` that hasn't finished `/complete` yet. |
| 410 | `UPLOAD_EXPIRED`           | Session aborted or exceeded its TTL. |
| 413 | `TOO_LARGE`                | Declared / assembled size exceeds the 5 GiB cap. |
| 422 | `UNSUPPORTED_FILE_TYPE`    | Filename extension not in the supported list (see §4). |
| 429 | `TOO_MANY_PENDING_UPLOADS` | Org at 50 concurrent sessions, or 60 initiates/hour exceeded. |
| 503 | `SCRATCH_FULL`             | Service scratch disk temporarily full — retry with backoff. |

### 11.3 Auth / org errors

| `error` | Status | Meaning |
|---|---|---|
| `MISSING_API_KEY`         | 401 | `Authorization` header not supplied. |
| `INVALID_API_KEY_FORMAT`  | 401 | Key doesn't start with `dg_live_`. |
| `INVALID_API_KEY`         | 401 | Key not found / revoked. |
| `AUTH_ERROR`              | 401 | Server-side failure validating the key. |
| `SLUG_MISMATCH`           | 403 | Key valid but URL org slug doesn't match key's org. |
| `SUBSCRIPTION_INACTIVE`   | 403 | Org subscription not active. Contact `support@sidian.io`. |
| `PLAN_UPGRADE_REQUIRED`   | 403 | API integration requires the Unlimited plan. |

---

## 12. Rate limits

| Limit | Value |
|---|---|
| Multipart initiate, per org         | 60 / hour |
| Concurrent multipart uploads, per org | 50 sessions |
| Max assembled file via multipart    | 5 GiB |

The content endpoints (`/redact`, `/analyze/*`, `/scan`) are not currently rate-limited server-side, but please back off on any `429` or `503` with exponential delay.

---

## 13. Choosing the right endpoint

| Situation | Use |
|---|---|
| You already know exactly what to redact. | `POST /redact` with explicit `rules`. |
| You want DataGuard to figure it out and redact in one step. | `POST /analyze/auto`. |
| You want to show users a review before mutating anything. | `POST /analyze/review`. |
| You need raw entity detection without any policy opinion. | `POST /scan`. |
| Your file is larger than a few MB or you want resumable uploads. | `POST /uploads` (multipart flow) and pass `uploadId` to any of the above. |

---

## 14. Support

- Admin portal: `https://admin.sidian.io` (manage keys, review audit log).
- Integration issues or key provisioning: `support@sidian.io`.
- Status page: `https://status.sidian.io`.
