> ## 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](/quickstart), 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](/guides/act-on-verdicts) handler.

Omit `document_category`. The public API has no support-chat scenario today, so requests use the default `scenario_email_general`.

## Code

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

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

## 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](/guides/long-form-content).
* **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.** ZeroDrift does not publish sync p50/p95 figures. Measure latency in your own environment for the reply lengths you serve.
* **Completed envelope.** Live jobs finish as `status: "completed"` with the verdict under `result`. See [Act on Each Verdict](/guides/act-on-verdicts).

## Next

<CardGroup cols={2}>
  <Card title="Act on Each Verdict" icon="scale-balanced" href="/guides/act-on-verdicts">
    Thresholds, logging, and a human review queue.
  </Card>

  <Card title="Per-Tenant Rulepacks" icon="building" href="/guides/per-tenant-rulepacks">
    Different rules per customer when one bot serves many tenants.
  </Card>
</CardGroup>
