> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zerodrift.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Import a Policy

> Turn your own policy document into enforceable rules.

You have a rulebook that is not a regulation: a communications policy, a claims manual, a brand guide, an underwriting policy. You want Anchor to enforce it alongside managed rule packs.

## Prerequisites

* A full-access API key ([Quickstart](https://docs.zerodrift.com/quickstart.md)).
* The policy as PDF, DOCX, or plain text, or in Notion, Linear, Confluence, or Google Drive.

## The flow

```
import → poll extraction → review → activate → enforce (optional: train)
```

Training an adapter is optional and comes after activation. See [Train an Adapter](https://docs.zerodrift.com/guides/train-an-adapter.md).

## Step 1: Import

Three intake paths. Pick one. Every path yields an `import_id`.

### Inline text or small file

`POST /api/policies/import` sends the document in the body (`source: "text"` or base64 `source: "file"`). Payload size is capped; use the presigned flow for large files.

Set `POLICY_TEXT` or encode a small file; use the returned `import_id` for later steps.

```bash cURL theme={null}
curl -X POST "https://api.zerodrift.ai/api/policies/import" \
  -H "x-api-key: $ZERODRIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"source\": \"text\",
    \"content\": \"Client-facing messages must not promise outcomes. Every recommendation must state the basis for the recommendation.\",
    \"filename\": \"comms-policy.txt\"
  }"

# Small file (base64):
# DOC_BASE64=$(base64 -i "$POLICY_FILE")
# curl ... -d "{\"source\":\"file\",\"content\":\"$DOC_BASE64\",\"filename\":\"$(basename "$POLICY_FILE")\"}"
```

```python Python theme={null}
import base64
import os

import requests

API = "https://api.zerodrift.ai"
HEADERS = {
    "x-api-key": os.environ["ZERODRIFT_API_KEY"],
    "Content-Type": "application/json",
}

policy_file = os.environ.get("POLICY_FILE")
if policy_file:
    with open(policy_file, "rb") as f:
        payload = {
            "source": "file",
            "content": base64.b64encode(f.read()).decode(),
            "filename": os.path.basename(policy_file),
        }
else:
    payload = {
        "source": "text",
        "content": os.environ.get(
            "POLICY_TEXT",
            "Client-facing messages must not promise outcomes. "
            "Every recommendation must state the basis for the recommendation.",
        ),
        "filename": "comms-policy.txt",
    }

r = requests.post(f"{API}/api/policies/import", headers=HEADERS, json=payload, timeout=60)
r.raise_for_status()
import_id = r.json()["import_id"]
print("import_id:", import_id)
```

```typescript TypeScript theme={null}
import { readFile } from "node:fs/promises";
import path from "node:path";

const API = "https://api.zerodrift.ai";
const HEADERS = {
  "x-api-key": process.env.ZERODRIFT_API_KEY!,
  "Content-Type": "application/json",
};

let payload: Record<string, string>;
if (process.env.POLICY_FILE) {
  const buf = await readFile(process.env.POLICY_FILE);
  payload = {
    source: "file",
    content: buf.toString("base64"),
    filename: path.basename(process.env.POLICY_FILE),
  };
} else {
  payload = {
    source: "text",
    content:
      process.env.POLICY_TEXT ??
      "Client-facing messages must not promise outcomes. Every recommendation must state the basis for the recommendation.",
    filename: "comms-policy.txt",
  };
}

const r = await fetch(`${API}/api/policies/import`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify(payload),
});
if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
const { import_id: importId } = await r.json();
console.log("import_id:", importId);
```

### Large file (presigned upload)

1. `POST /api/policies/import/presigned_url` → `import_id`, `upload_url`, `required_headers`
2. `PUT` the raw bytes to `upload_url` with **every** header in `required_headers` (omitting them causes `SignatureDoesNotMatch`)
3. `POST /api/policies/import/start` with that `import_id`. If status is `scan_pending`, retry until extraction starts (`processing`)

Max upload size is **1 GB**. Set `POLICY_FILE` to the path on disk.

```bash cURL theme={null}
POLICY_FILE="${POLICY_FILE:?set POLICY_FILE to your PDF path}"

RESP=$(curl -s -X POST "https://api.zerodrift.ai/api/policies/import/presigned_url" \
  -H "x-api-key: $ZERODRIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"filename\": \"$(basename "$POLICY_FILE")\", \"content_type\": \"application/pdf\"}")

IMPORT_ID=$(printf '%s' "$RESP" | jq -r '.import_id')
UPLOAD_URL=$(printf '%s' "$RESP" | jq -r '.upload_url')

CURL_HEADERS=()
while IFS=$'\t' read -r key value; do
  CURL_HEADERS+=(-H "$key: $value")
done < <(printf '%s' "$RESP" | jq -r '.required_headers | to_entries[] | "\(.key)\t\(.value)"')

curl -X PUT "$UPLOAD_URL" "${CURL_HEADERS[@]}" --data-binary @"$POLICY_FILE"

# Retry while malware scan is still pending
while true; do
  START=$(curl -s -X POST "https://api.zerodrift.ai/api/policies/import/start" \
    -H "x-api-key: $ZERODRIFT_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"import_id\": \"$IMPORT_ID\"}")
  STATUS=$(printf '%s' "$START" | jq -r '.status // empty')
  if [ "$STATUS" != "scan_pending" ]; then
    echo "$START"
    break
  fi
  sleep 3
done

echo "IMPORT_ID=$IMPORT_ID"
```

```python Python theme={null}
import os
import time

import requests

API = "https://api.zerodrift.ai"
API_KEY = os.environ["ZERODRIFT_API_KEY"]
HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"}
policy_file = os.environ["POLICY_FILE"]

presign = requests.post(
    f"{API}/api/policies/import/presigned_url",
    headers=HEADERS,
    json={
        "filename": os.path.basename(policy_file),
        "content_type": "application/pdf",
    },
    timeout=30,
).json()

import_id = presign["import_id"]
with open(policy_file, "rb") as f:
    requests.put(
        presign["upload_url"],
        headers=presign["required_headers"],
        data=f,
        timeout=120,
    ).raise_for_status()

while True:
    started = requests.post(
        f"{API}/api/policies/import/start",
        headers=HEADERS,
        json={"import_id": import_id},
        timeout=30,
    ).json()
    if started.get("status") != "scan_pending":
        break
    time.sleep(3)

if "status" not in started:
    raise SystemExit(f"Start failed: {started.get('error', started)}")
print("import_id:", import_id, "status:", started["status"])
```

```typescript TypeScript theme={null}
import { readFile } from "node:fs/promises";
import path from "node:path";

const API = "https://api.zerodrift.ai";
const API_KEY = process.env.ZERODRIFT_API_KEY!;
const policyFile = process.env.POLICY_FILE!;

const presignRes = await fetch(`${API}/api/policies/import/presigned_url`, {
  method: "POST",
  headers: {
    "x-api-key": API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    filename: path.basename(policyFile),
    content_type: "application/pdf",
  }),
});
if (!presignRes.ok) throw new Error(`presign ${presignRes.status}`);
const presign = await presignRes.json();

const fileBytes = await readFile(policyFile);
const put = await fetch(presign.upload_url, {
  method: "PUT",
  headers: presign.required_headers as Record<string, string>,
  body: fileBytes,
});
if (!put.ok) throw new Error(`upload ${put.status}`);

let started: { status?: string; error?: string };
for (;;) {
  const startRes = await fetch(`${API}/api/policies/import/start`, {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ import_id: presign.import_id }),
  });
  started = await startRes.json();
  if (started.status !== "scan_pending") break;
  await new Promise((r) => setTimeout(r, 3000));
}
if (!started.status) {
  throw new Error(`Start failed: ${started.error ?? JSON.stringify(started)}`);
}
console.log("import_id:", presign.import_id, "status:", started.status);
```

### Connected source

In Command, open **Settings → Sources**, connect Notion, Linear, Confluence, or Google Drive, select a document, and sync. The sync creates the same `import_id` as an upload. Details: [MCP Connectors](https://docs.zerodrift.com/connecting-mcp.md).

## Step 2: Poll extraction

Poll `GET /api/policies/import/{import_id}` until `status` is no longer `processing`. Continue when it is `pending_review`. `no_rules_found` and `failed` are not activatable — fix the document and import again.

Use `IMPORT_ID` from the previous step (or your environment).

```bash cURL theme={null}
: "${IMPORT_ID:?set IMPORT_ID from the previous step}"

while true; do
  DETAILS=$(curl -s "https://api.zerodrift.ai/api/policies/import/$IMPORT_ID" \
    -H "x-api-key: $ZERODRIFT_API_KEY")
  STATUS=$(printf '%s' "$DETAILS" | jq -r '.status')
  if [ "$STATUS" != "processing" ]; then
    break
  fi
  sleep 3
done

echo "$STATUS $(printf '%s' "$DETAILS" | jq -r '.rule_count // empty')"
if [ "$STATUS" != "pending_review" ]; then
  echo "import is not activatable: $STATUS" >&2
  exit 1
fi
```

```python Python theme={null}
import os
import time

import requests

API = "https://api.zerodrift.ai"
HEADERS = {"x-api-key": os.environ["ZERODRIFT_API_KEY"]}
import_id = os.environ["IMPORT_ID"]

while True:
    details = requests.get(
        f"{API}/api/policies/import/{import_id}",
        headers=HEADERS,
        timeout=30,
    ).json()
    if details.get("status") != "processing":
        break
    time.sleep(3)

print(details["status"], details.get("rule_count"))
if details.get("status") != "pending_review":
    raise SystemExit(f"import is not activatable: {details.get('status')}")
```

```typescript TypeScript theme={null}
const API = "https://api.zerodrift.ai";
const importId = process.env.IMPORT_ID!;

let details: { status: string; rule_count?: number };
for (;;) {
  const r = await fetch(`${API}/api/policies/import/${importId}`, {
    headers: { "x-api-key": process.env.ZERODRIFT_API_KEY! },
  });
  if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
  details = await r.json();
  if (details.status !== "processing") break;
  await new Promise((res) => setTimeout(res, 3000));
}
console.log(details.status, details.rule_count);
if (details.status !== "pending_review") {
  throw new Error(`import is not activatable: ${details.status}`);
}
```

## Step 3: Review

Inspect extracted rules in the poll response or in Command. Before activation you can PATCH a pending rule's `prompt`, `fix_note`, or `confidence` ([Edit Imported Rule](https://docs.zerodrift.com/api-reference/custom-policies/edit-imported-rule.md)). Pass `null` to clear `prompt` or `fix_note`.

## Step 4: Activate

`POST /api/policies/import/{import_id}/activate` activates every extracted rule (or a `rule_ids` subset). Set `overwrite: true` to replace rules from an earlier import when ids collide.

Activated rules run on enforcement for your API key. Use `validation_scope.imports` when you need to narrow to this import (see [Per-Tenant Rulepacks](https://docs.zerodrift.com/guides/per-tenant-rulepacks.md)).

```bash cURL theme={null}
curl -X POST "https://api.zerodrift.ai/api/policies/import/$IMPORT_ID/activate" \
  -H "x-api-key: $ZERODRIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```python Python theme={null}
import os

import requests

API = "https://api.zerodrift.ai"
HEADERS = {
    "x-api-key": os.environ["ZERODRIFT_API_KEY"],
    "Content-Type": "application/json",
}
import_id = os.environ["IMPORT_ID"]

r = requests.post(
    f"{API}/api/policies/import/{import_id}/activate",
    headers=HEADERS,
    json={},
    timeout=30,
)
r.raise_for_status()
print(r.json())
```

```typescript TypeScript theme={null}
const API = "https://api.zerodrift.ai";
const importId = process.env.IMPORT_ID!;

const r = await fetch(`${API}/api/policies/import/${importId}/activate`, {
  method: "POST",
  headers: {
    "x-api-key": process.env.ZERODRIFT_API_KEY!,
    "Content-Type": "application/json",
  },
  body: "{}",
});
if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
console.log(await r.json());
```

## Step 5: Enforce against it

After activation, untrained custom rules run on Max. Scope with
`validation_scope.imports` and `model_engine: "anchor_3_0_max"`. After you
[train an adapter](https://docs.zerodrift.com/guides/train-an-adapter.md), Flagship (`anchor_3_0`) can check
that import.

```bash cURL theme={null}
curl -X POST "https://api.zerodrift.ai/api/v3/content/validate" \
  -H "x-api-key: $ZERODRIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"content\": \"You will definitely be approved and your rate will never change.\",
    \"model_engine\": \"anchor_3_0_max\",
    \"mode\": \"sync\",
    \"validation_scope\": {
      \"imports\": [\"$IMPORT_ID\"]
    }
  }"
```

```python Python theme={null}
import os

import requests

API = "https://api.zerodrift.ai"
HEADERS = {
    "x-api-key": os.environ["ZERODRIFT_API_KEY"],
    "Content-Type": "application/json",
}
import_id = os.environ["IMPORT_ID"]

r = requests.post(
    f"{API}/api/v3/content/validate",
    headers=HEADERS,
    json={
        "content": "You will definitely be approved and your rate will never change.",
        "model_engine": "anchor_3_0_max",
        "mode": "sync",
        "validation_scope": {"imports": [import_id]},
    },
    timeout=30,
)
r.raise_for_status()
print(r.json())
```

```typescript TypeScript theme={null}
const API = "https://api.zerodrift.ai";
const importId = process.env.IMPORT_ID!;

const r = await fetch(`${API}/api/v3/content/validate`, {
  method: "POST",
  headers: {
    "x-api-key": process.env.ZERODRIFT_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    content: "You will definitely be approved and your rate will never change.",
    model_engine: "anchor_3_0_max",
    mode: "sync",
    validation_scope: { imports: [importId] },
  }),
});
if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
console.log(await r.json());
```

## Gotchas

* **Retention.** The original document is kept **7 days** by default ([Data Retention](https://docs.zerodrift.com/data-retention.md)). Training after that returns `410`; re-import.
* **Engine.** Untrained custom rules run on `anchor_3_0_max`. Flagship (`anchor_3_0`) can check the import after you train an adapter. See [Enforce Content](https://docs.zerodrift.com/api-reference/validate/validate-content.md).
* **Re-sync replaces.** Syncing a connected document again updates the import; it does not create a duplicate.
* **Read-only connections.** ZeroDrift does not write back to Notion, Linear, Confluence, or Google Drive.
* **Presigned headers.** Always send `required_headers` on the S3 `PUT`. Retry `scan_pending` on start.

## Next

> **[Train an Adapter](https://docs.zerodrift.com/guides/train-an-adapter.md)**
>
> Teach Anchor your policy's judgment, not just its extracted rules.

> **[Per-Tenant Rulepacks](https://docs.zerodrift.com/guides/per-tenant-rulepacks.md)**
>
> Give each customer their own packs and imports.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.