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

# Per-Tenant Rulepacks

> Serve many customers with different rules from one integration.

You run a platform. Tenant A needs US Securities. Tenant B needs a different managed pack plus their own imported policy. Every call must run that tenant's rules and nothing else.

## Key allow-list vs `validation_scope`

The API key's **Rulepacks** setting is the outer boundary. Create keys in Command (**Settings → API → Keys**) and set **Rulepacks** on each key. That allow-list caps which managed packs the key may use.

`validation_scope` only **narrows** live enforcement to a subset of already-active rules, rule packs, and imports. It cannot broaden key permissions. If a request scopes to a pack the key does not allow, that pack is outside the key boundary.

Discover pack ids with [Rule Packs](/api-reference/rulepacks) (`GET /api/rulepacks/`). Do not hard-code pack ids you have not confirmed for your account.

## Two patterns

**Pattern A — one API key per tenant.** Mint a key per tenant and set **Rulepacks** on the key. Every call with that key stays inside that allow-list. Simplest isolation when tenants are few and stable.

**Pattern B — one key, scope per request.** Keep a tenant table and pass `validation_scope` on every call. Best when tenants are many or change often. The shared key's allow-list remains the outer boundary; per-request scope only narrows further.

Activated custom rules from [Import a Policy](/guides/import-a-policy) run for the account by default. For Pattern B, always pass `validation_scope` so a tenant does not pick up another tenant's active imports.

## Pattern A

Use the tenant's key. Always pass `validation_scope.imports` on Pattern A too: the key Rulepacks allow-list does not isolate custom imports. Use that tenant's import ids, or `[]` if the tenant has none.

<CodeGroup>
  ```bash cURL theme={null}
  : "${TENANT_IMPORT_ID:=}"  # empty = this tenant has no custom import
  if [ -n "$TENANT_IMPORT_ID" ]; then
    IMPORTS="[\"$TENANT_IMPORT_ID\"]"
  else
    IMPORTS="[]"
  fi

  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\": \"Our fund guarantees 20% returns with zero risk!\",
      \"model_engine\": \"anchor_3_0\",
      \"mode\": \"sync\",
      \"validation_scope\": {
        \"imports\": $IMPORTS
      }
    }"
  ```

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

  import requests

  API = "https://api.zerodrift.ai"
  # Pattern A: key Rulepacks allow-list + explicit imports (or [])
  HEADERS = {"x-api-key": os.environ["ZERODRIFT_API_KEY"]}
  tenant_import = os.environ.get("TENANT_IMPORT_ID")
  imports = [tenant_import] if tenant_import else []

  r = requests.post(
      f"{API}/api/v3/content/validate",
      headers=HEADERS,
      json={
          "content": "Our fund guarantees 20% returns with zero risk!",
          "model_engine": "anchor_3_0",
          "mode": "sync",
          "document_category": "scenario_retail_investor_letter",
          "validation_scope": {"imports": imports},
      },
      timeout=30,
  )
  r.raise_for_status()
  print(r.json())
  ```

  ```typescript TypeScript theme={null}
  const API = "https://api.zerodrift.ai";
  // Pattern A: key Rulepacks allow-list + explicit imports (or [])
  const HEADERS = {
    "x-api-key": process.env.ZERODRIFT_API_KEY!,
    "Content-Type": "application/json",
  };
  const tenantImport = process.env.TENANT_IMPORT_ID;
  const imports = tenantImport ? [tenantImport] : [];

  const r = await fetch(`${API}/api/v3/content/validate`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      content: "Our fund guarantees 20% returns with zero risk!",
      model_engine: "anchor_3_0",
      mode: "sync",
      document_category: "scenario_retail_investor_letter",
      validation_scope: { imports },
    }),
  });
  if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
  console.log(await r.json());
  ```
</CodeGroup>

## Pattern B

One shared key. Look up the tenant's activated `import_id` and pass `validation_scope.imports` on every call. Import ids come from [Import a Policy](/guides/import-a-policy), not invented fixtures. Set `TENANT_IMPORT_ID`.

Live `validation_scope.rulepacks` currently fails closed (`fallback_reason: rulepack_scope_unsupported`). Use Pattern A (per-tenant keys) to isolate managed packs; use Pattern B for tenant-specific imported policies.

Optional `metadata` is merged into the job ([Enforce Content](/api-reference/validate/validate-content)). Activity list filters are the documented query params on [List Activity](/api-reference/activities/list-activities) (`from`, `to`, `status`, `agent_id`, `source`, `rule_id`, `search`, `cursor`, `limit`) — do not assume arbitrary metadata keys are Activity filters.

<CodeGroup>
  ```bash cURL theme={null}
  : "${TENANT_IMPORT_ID:?set TENANT_IMPORT_ID to an activated import}"

  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 at this rate.\",
      \"model_engine\": \"anchor_3_0\",
      \"mode\": \"sync\",
      \"validation_scope\": {
        \"imports\": [\"$TENANT_IMPORT_ID\"]
      },
      \"metadata\": {
        \"tenant_id\": \"acme-broker\"
      }
    }"
  ```

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

  import requests

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

  TENANTS = {
      "acme-broker": {"imports": [os.environ["TENANT_IMPORT_ID"]]},
  }


  def enforce_for_tenant(tenant_id: str, content: str) -> dict:
      t = TENANTS[tenant_id]
      r = requests.post(
          f"{API}/api/v3/content/validate",
          headers=HEADERS,
          json={
              "content": content,
              "model_engine": "anchor_3_0",
              "mode": "sync",
              "validation_scope": {"imports": t["imports"]},
              "metadata": {"tenant_id": tenant_id},
          },
          timeout=30,
      )
      r.raise_for_status()
      return r.json()


  print(
      enforce_for_tenant(
          "acme-broker",
          "You will definitely be approved at this rate.",
      )
  )
  ```

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

  type TenantScope = { imports: string[] };

  const TENANTS: Record<string, TenantScope> = {
    "acme-broker": { imports: [process.env.TENANT_IMPORT_ID!] },
  };

  export async function enforceForTenant(tenantId: string, content: string) {
    const t = TENANTS[tenantId];
    const r = await fetch(`${API}/api/v3/content/validate`, {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify({
        content,
        model_engine: "anchor_3_0",
        mode: "sync",
        validation_scope: { imports: t.imports },
        metadata: { tenant_id: tenantId },
      }),
    });
    if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
    return r.json();
  }

  console.log(
    await enforceForTenant(
      "acme-broker",
      "You will definitely be approved at this rate.",
    ),
  );
  ```
</CodeGroup>

## Tenant-specific policies

A tenant's own rulebook becomes an `import_id` through [Import a Policy](/guides/import-a-policy). Put that id in the tenant's `imports` list. For either pattern, pass `validation_scope.imports` explicitly when multiple tenants share one ZeroDrift account. Calls then run the regulatory pack and tenant policy together when both are in scope.

## Gotchas

* **Missing `validation_scope.imports`** evaluates every active import on the account. Pass the tenant's import ids (or `[]`) on Pattern A and Pattern B.
* **`validation_scope` cannot widen the key.** It only narrows within the key allow-list and already-active imports/rules.
* **Managed-pack request scope is not live.** `validation_scope.rulepacks` currently returns `status: "failed"` with `fallback_reason: rulepack_scope_unsupported`. Isolate managed packs with Pattern A until that path is supported.
* **Resolve ids.** List packs with `GET /api/rulepacks/` (for example `zd-us-securities`). Set `TENANT_IMPORT_ID` for Pattern B.

## Next

<CardGroup cols={2}>
  <Card title="Rule Packs" icon="layer-group" href="/api-reference/rulepacks">
    List managed and custom packs; use returned ids in `validation_scope.rulepacks`.
  </Card>

  <Card title="Export Activity" icon="file-export" href="/guides/export-activity">
    Pull the verdict log for reporting and threshold tuning.
  </Card>
</CardGroup>
