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

# Long-Form Content

> Enforce documents, articles, and long messages with the async flow.

The content is more than a few hundred words, or you are enforcing many pieces at once: newsletters, client letters, reports, proposals, disclosures, landing pages. Async returns immediately with a `job_id`; you poll for the verdict.

## Prerequisites

* An API key ([Quickstart](/quickstart)).
* Content as plain text. Convert PDFs and DOCX to text before sending. Policy import accepts files; content enforcement takes text.

## The flow

```
POST /api/v3/content/validate (async)  →  202 { job_id, poll.url }
GET  /api/v3/jobs/{job_id}             →  status: queued | started | in_progress | completed | done | failed
```

After `completed` (or `done`), apply confident fixes and escalate the rest. When `revalidation_recommended` is true, re-enforce the fixed text once before publishing. For the Pass / Fix / Block / Escalate decision table, see [Act on Each Verdict](/guides/act-on-verdicts).

## Code

Set `mode` to `"async"`. Public scenario ids include `scenario_email_general` (default) and `scenario_retail_investor_letter`.

<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": "Dear client,\n\nOur fund guarantees 18% returns with no downside risk.\n\nSincerely,\nAdvisor",
      "model_engine": "anchor_3_0",
      "mode": "async",
      "document_category": "scenario_retail_investor_letter"
    }'

  # Then poll until status is completed, done, or failed:
  # curl "https://api.zerodrift.ai/api/v3/jobs/{job_id}" \
  #   -H "x-api-key: $ZERODRIFT_API_KEY"
  ```

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

  import requests

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

  NEWSLETTER = (
      "Dear client,\n\n"
      "Our fund guarantees 18% returns with no downside risk.\n\n"
      "Sincerely,\nAdvisor"
  )


  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 submit(text: str, scenario: str = "scenario_retail_investor_letter") -> str:
      r = requests.post(
          f"{API}/api/v3/content/validate",
          headers=HEADERS,
          json={
              "content": text,
              "model_engine": "anchor_3_0",
              "mode": "async",
              "document_category": scenario,
          },
          timeout=30,
      )
      r.raise_for_status()
      return r.json()["poll"]["url"]


  def wait(poll_url: str, timeout_s: int = 180) -> dict:
      delay, waited = 1.0, 0.0
      while waited < timeout_s:
          response = requests.get(f"{API}{poll_url}", headers=HEADERS, timeout=10)
          response.raise_for_status()
          job = response.json()
          if job["status"] == "failed":
              raise RuntimeError(job.get("error"))
          if job["status"] in TERMINAL:
              return job
          time.sleep(delay)
          waited += delay
          delay = min(delay * 2, 5.0)
      raise TimeoutError(poll_url)


  def fix_all(text: str, job: dict) -> tuple[str, list[dict]]:
      """Apply confident line-level replace only. Leave Block and grouped-only findings for review."""
      body = payload(job)
      items = body.get("violations_by_line") or body.get("violations") or []
      if body.get("overall_status") == "do not send":
          return text, items or [{"overall_status": "do not send"}]
      if not items:
          if body.get("overall_status") == "approved":
              return text, []
          return text, [{"overall_status": body.get("overall_status")}]
      needs_review = []
      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):
              needs_review.append(v)
              continue
          if v.get("line_number"):
              lines = text.split("\n")
              lines[v["line_number"] - 1] = suggested
              text = "\n".join(lines)
          else:
              quote = v.get("quote") or v.get("line_text") or ""
              if quote and quote in text:
                  text = text.replace(quote, suggested)
              else:
                  needs_review.append(v)
      return text, needs_review


  poll_url = submit(NEWSLETTER)
  job = wait(poll_url)
  body = payload(job)
  print(body.get("overall_status"), body.get("summary"), body.get("revalidation_recommended"))
  fixed, review = fix_all(NEWSLETTER, job)
  print(fixed)
  print("needs_review:", len(review))

  if body.get("revalidation_recommended") and fixed != NEWSLETTER and not review:
      recheck = payload(wait(submit(fixed)))
      print("revalidation:", recheck.get("overall_status"))
  ```

  ```typescript TypeScript theme={null}
  const API = "https://api.zerodrift.ai";
  const HEADERS = {
    "x-api-key": process.env.ZERODRIFT_API_KEY!,
    "Content-Type": "application/json",
  };
  const TERMINAL = new Set(["completed", "done", "failed"]);

  const NEWSLETTER =
    "Dear client,\n\nOur fund guarantees 18% returns with no downside risk.\n\nSincerely,\nAdvisor";

  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 submit(
    text: string,
    scenario = "scenario_retail_investor_letter",
  ): Promise<string> {
    const r = await fetch(`${API}/api/v3/content/validate`, {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify({
        content: text,
        model_engine: "anchor_3_0",
        mode: "async",
        document_category: scenario,
      }),
    });
    if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
    return (await r.json()).poll.url;
  }

  export async function wait(pollUrl: string, timeoutMs = 180_000) {
    let delay = 1000;
    let waited = 0;
    while (waited < timeoutMs) {
      const response = await fetch(`${API}${pollUrl}`, { headers: HEADERS });
      if (!response.ok) throw new Error(`ZeroDrift ${response.status}`);
      const job = await response.json();
      if (job.status === "failed") throw new Error(job.error);
      if (TERMINAL.has(job.status)) return job;
      await new Promise((res) => setTimeout(res, delay));
      waited += delay;
      delay = Math.min(delay * 2, 5000);
    }
    throw new Error(`timeout ${pollUrl}`);
  }

  export function fixAll(text: string, job: any) {
    const body = payload(job);
    const items = body.violations_by_line ?? body.violations ?? [];
    if (body.overall_status === "do not send") {
      return { fixed: text, needsReview: items.length ? items : [{ overall_status: "do not send" }] };
    }
    if (!items.length) {
      if (body.overall_status === "approved") return { fixed: text, needsReview: [] };
      return { fixed: text, needsReview: [{ overall_status: body.overall_status }] };
    }
    const needsReview: any[] = [];
    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)) {
        needsReview.push(v);
        continue;
      }
      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 {
          needsReview.push(v);
        }
      }
    }
    return { fixed: text, needsReview };
  }

  const pollUrl = await submit(NEWSLETTER);
  const job = await wait(pollUrl);
  const body = payload(job);
  console.log(body.overall_status, body.summary, body.revalidation_recommended);
  const { fixed, needsReview } = fixAll(NEWSLETTER, job);
  console.log(fixed);
  console.log("needs_review:", needsReview.length);

  if (body.revalidation_recommended && fixed !== NEWSLETTER && needsReview.length === 0) {
    const recheck = payload(await wait(await submit(fixed)));
    console.log("revalidation:", recheck.overall_status);
  }
  ```
</CodeGroup>

## Reading the result

Completed jobs nest these fields under `result` (they may also appear at the top level on some responses):

| Field                                                                        | Use it for                                                                                   |
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `overall_status`                                                             | The verdict for the whole document.                                                          |
| `summary.do_not_send`, `summary.send_with_caution`, `summary.document_pages` | Counts for a dashboard or a review header.                                                   |
| `violations[]`                                                               | Current Anchor shape: `quote`, `rule_id`, `signal_name`, `severity`, `confidence`, `fix`.    |
| `violations_by_line[]`                                                       | Line-grouped shape when present: `line_number`, `line_text`, `rules_violated[]`, `best_fix`. |
| `revalidation_recommended`                                                   | `true` when more than one fix is found. Re-enforce the fixed text once before publishing.    |
| `violating_line_count`, `compliant_line_count`, `total_line_count`           | Progress and coverage when present.                                                          |
| `input_tokens`, `output_tokens`                                              | Usage accounting when present.                                                               |

## Gotchas

* **Poll until `completed` or `done`.** Live jobs use `completed`. Keep polling until a terminal status; there is no published fixed completion time.
* **Re-enforce after Fix All** when `revalidation_recommended` is true. Fixes to adjacent lines can interact.
* **Quotes and line numbers refer to the text exactly as you sent it.** Do not normalize whitespace between submit and fix.
* **Batch carefully.** Submit and poll within the limits configured for your account. Back off on `429` responses.

## Next

<CardGroup cols={2}>
  <Card title="Act on Each Verdict" icon="scale-balanced" href="/guides/act-on-verdicts">
    Pass / Fix / Block / Escalate after the job completes.
  </Card>

  <Card title="Export Activity" icon="file-export" href="/guides/export-activity">
    Pull verdicts for every document you have enforced.
  </Card>
</CardGroup>
