> ## 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](https://docs.zerodrift.com/api-reference/rulepacks.md) (`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](https://docs.zerodrift.com/guides/import-a-policy.md) run for the account by default. Untrained custom rules run on Max (`anchor_3_0_max`). 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.

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

## 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](https://docs.zerodrift.com/guides/import-a-policy.md), 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](https://docs.zerodrift.com/api-reference/validate/validate-content.md)). Activity list filters are the documented query params on [List Activity](https://docs.zerodrift.com/api-reference/activities/list-activities.md) (`from`, `to`, `status`, `agent_id`, `source`, `rule_id`, `search`, `cursor`, `limit`) — do not assume arbitrary metadata keys are Activity filters.

```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_max\",
    \"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_max",
            "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_max",
      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.",
  ),
);
```

## Tenant-specific policies

A tenant's own rulebook becomes an `import_id` through [Import a Policy](https://docs.zerodrift.com/guides/import-a-policy.md). 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.
* **Untrained custom rules need Max.** Send `model_engine: "anchor_3_0_max"` when the tenant's scope includes custom imports or rule IDs. See [Enforce Content](https://docs.zerodrift.com/api-reference/validate/validate-content.md).
* **`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

> **[Rule Packs](https://docs.zerodrift.com/api-reference/rulepacks.md)**
>
> List managed and custom packs; use returned ids in `validation_scope.rulepacks`.

> **[Export Activity](https://docs.zerodrift.com/guides/export-activity.md)**
>
> Pull the verdict log for reporting and threshold tuning.


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