> ## 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](https://docs.zerodrift.com/quickstart.md)).
* 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 | processing | completed | failed
```

After `completed`, 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](https://docs.zerodrift.com/guides/act-on-verdicts.md).

## Code

Set `mode` to `"async"`.

```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"
  }'

# Then poll until status is completed 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", "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) -> str:
    r = requests.post(
        f"{API}/api/v3/content/validate",
        headers=HEADERS,
        json={
            "content": text,
            "model_engine": "anchor_3_0",
            "mode": "async",
        },
        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", "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): 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",
    }),
  });
  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);
}
```

## 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. |
| `units`, `cost` | Billed usage and USD list price when present. |

## Gotchas

* **Poll until `completed` or `failed`.** 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

> **[Act on Each Verdict](https://docs.zerodrift.com/guides/act-on-verdicts.md)**
>
> Pass / Fix / Block / Escalate after the job completes.

> **[Export Activity](https://docs.zerodrift.com/guides/export-activity.md)**
>
> Pull verdicts for every document you have enforced.


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