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

# Enforce a Chatbot

> Enforce every reply your chatbot generates before the customer sees it.

Your chatbot or virtual assistant generates replies with an LLM and sends them to customers. Check every reply against regulations and your policies, then fix or block it, before it leaves your server.

## Prerequisites

* An API key ([Quickstart](https://docs.zerodrift.com/quickstart.md), Step 1).
* Optional: rule packs activated in Policy. Note their ids from `GET /api/rulepacks/` if you want to narrow `validation_scope`.

## The pattern

```
LLM reply  →  POST /api/v3/content/validate (sync)  →  verdict  →  send reply | send fix | send fallback
```

One call per reply, in the response path, after generation and before delivery. Build on the [Act on Each Verdict](https://docs.zerodrift.com/guides/act-on-verdicts.md) handler.

## Code

```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": "Our fund guarantees 20% returns with zero risk!",
    "model_engine": "anchor_3_0",
    "mode": "sync",
    "metadata": {
      "conversation_id": "conv_123",
      "surface": "chat"
    }
  }'
```

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

import requests

API = "https://api.zerodrift.ai"
HEADERS = {"x-api-key": os.environ["ZERODRIFT_API_KEY"]}
FALLBACK = (
    "I can't help with that here. I've passed your question to a specialist "
    "who will follow up."
)


def payload(job: dict) -> dict:
    inner = job.get("result")
    return inner if isinstance(inner, dict) else job


def confident(value) -> bool:
    if isinstance(value, str):
        return value == "high"
    try:
        return float(value) >= 0.85
    except (TypeError, ValueError):
        return False


def enforce_reply(reply: str, conversation_id: str) -> str:
    r = requests.post(
        f"{API}/api/v3/content/validate",
        headers=HEADERS,
        json={
            "content": reply,
            "model_engine": "anchor_3_0",
            "mode": "sync",
            "metadata": {
                "conversation_id": conversation_id,
                "surface": "chat",
            },
        },
        timeout=30,
    )
    r.raise_for_status()
    job = r.json()

    # Long replies can fall back to async; poll or fail closed.
    if job.get("mode") == "async":
        return FALLBACK

    body = payload(job)
    status = body.get("overall_status")
    if status == "approved":
        return reply
    if status == "do not send":
        return FALLBACK

    text = reply
    items = body.get("violations_by_line") or body.get("violations") or []
    if not items:
        return FALLBACK
    for v in items:
        fix = v.get("best_fix") or v.get("fix") or {}
        action = fix.get("action") or "replace"
        suggested = fix.get("suggested_text")
        conf = fix.get("confidence", v.get("confidence"))
        if action != "replace" or not suggested or not confident(conf):
            return FALLBACK
        quote = v.get("quote") or v.get("line_text")
        if v.get("line_number"):
            lines = text.split("\n")
            lines[v["line_number"] - 1] = suggested
            text = "\n".join(lines)
        elif quote and quote in text:
            text = text.replace(quote, suggested)
        else:
            return FALLBACK
    return text


print(enforce_reply("Our fund guarantees 20% returns with zero risk!", "conv_123"))
```

```typescript TypeScript theme={null}
const API = "https://api.zerodrift.ai";
const FALLBACK =
  "I can't help with that here. I've passed your question to a specialist who will follow up.";

function payload(job: any) {
  return job?.result && typeof job.result === "object" ? job.result : job;
}

function confident(value: unknown) {
  if (typeof value === "string") return value === "high";
  const n = Number(value);
  return Number.isFinite(n) && n >= 0.85;
}

export async function enforceReply(
  reply: string,
  conversationId: string,
): Promise<string> {
  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: reply,
      model_engine: "anchor_3_0",
      mode: "sync",
      metadata: { conversation_id: conversationId, surface: "chat" },
    }),
  });
  if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
  const job = await r.json();

  // Long replies can fall back to async; poll or fail closed.
  if (job.mode === "async") return FALLBACK;
  const body = payload(job);
  if (body.overall_status === "approved") return reply;
  if (body.overall_status === "do not send") return FALLBACK;

  let text = reply;
  const items = body.violations_by_line ?? body.violations ?? [];
  if (!items.length) return FALLBACK;
  for (const v of items) {
    const fix = v.best_fix ?? v.fix ?? {};
    const action = fix.action ?? "replace";
    const suggested = fix.suggested_text;
    const conf = fix.confidence ?? v.confidence;
    if (action !== "replace" || !suggested || !confident(conf)) return FALLBACK;
    if (v.line_number) {
      const lines = text.split("\n");
      lines[v.line_number - 1] = suggested;
      text = lines.join("\n");
    } else {
      const quote = v.quote ?? v.line_text;
      if (quote && text.includes(quote)) {
        text = text.replaceAll(quote, suggested);
      } else {
        return FALLBACK;
      }
    }
  }
  return text;
}

console.log(
  await enforceReply("Our fund guarantees 20% returns with zero risk!", "conv_123"),
);
```

To poll instead of returning the fallback when sync falls back to async, use the flow in [Long-Form Content](https://docs.zerodrift.com/guides/long-form-content.md).

## What the customer sees

| Verdict | Customer sees |
| - | - |
| Pass | The original reply. |
| Fix | The fixed reply. Same answer, compliant wording, required disclosure added. |
| Block | The fallback message. Log the original in your system for review. |
| Escalate | The fallback message now, and a human follows up. |

## Gotchas

* **Sync can fall back to async.** Check `mode` in every response. For long answers, use the async flow in [Long-Form Content](https://docs.zerodrift.com/guides/long-form-content.md).
* **Fail open or fail closed.** Set a client timeout. If ZeroDrift is unreachable, decide once whether your bot fails open (send the reply) or fails closed (send the fallback). Regulated deployments fail closed.
* **Streaming.** Enforce the complete reply, then stream it. Do not stream tokens to the customer before the verdict.
* **Latency.** Typical Flagship (Anchor 3.0) latency is 1.63 seconds. See the [Anchor 3.0 benchmark](https://zerodrift.com/model/anchor-3-benchmark) for published quality and latency. Measure your own path for the reply lengths you serve.
* **Completed envelope.** Live jobs finish as `status: "completed"` with the verdict under `result`. See [Act on Each Verdict](https://docs.zerodrift.com/guides/act-on-verdicts.md).

## Next

> **[Act on Each Verdict](https://docs.zerodrift.com/guides/act-on-verdicts.md)**
>
> Thresholds, logging, and a human review queue.

> **[Per-Tenant Rulepacks](https://docs.zerodrift.com/guides/per-tenant-rulepacks.md)**
>
> Different rules per customer when one bot serves many tenants.


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