> ## 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](/quickstart)).
* 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](/guides/train-an-adapter).

## 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.

<CodeGroup>
  ```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);
  ```
</CodeGroup>

### 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.

<CodeGroup>
  ```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);
  ```
</CodeGroup>

### 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](/connecting-mcp).

## 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).

<CodeGroup>
  ```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}`);
  }
  ```
</CodeGroup>

## 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](/api-reference/custom-policies/edit-imported-rule)). 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](/guides/per-tenant-rulepacks)).

<CodeGroup>
  ```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());
  ```
</CodeGroup>

## Step 5: Enforce against it

After activation, scope enforcement with `validation_scope.imports` when you want this import (alone or with managed packs):

<CodeGroup>
  ```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\",
      \"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",
          "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",
      mode: "sync",
      validation_scope: { imports: [importId] },
    }),
  });
  if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
  console.log(await r.json());
  ```
</CodeGroup>

## Gotchas

* **Retention.** The original document is kept **7 days** by default ([Data Retention](/data-retention)). Training after that returns `410`; re-import.
* **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

<CardGroup cols={2}>
  <Card title="Train an Adapter" icon="graduation-cap" href="/guides/train-an-adapter">
    Teach Anchor your policy's judgment, not just its extracted rules.
  </Card>

  <Card title="Per-Tenant Rulepacks" icon="building" href="/guides/per-tenant-rulepacks">
    Give each customer their own packs and imports.
  </Card>
</CardGroup>
