# Get Activity Source: https://docs.zerodrift.com/api-reference/activities/get-activity GET /api/activities/{activity_id} Get one Command activity event with timeline and rule detail. Return the full activity timeline and per-rule detail for one Command activity event. Cross-customer access returns `404`. ## Path Parameters Activity identifier from [List Activity](/api-reference/activities/list-activities) ## Response Includes all list-summary fields, plus: Originating API endpoint when available Request correlation ID when available Full rule detail including `snippet`, `suggested_text`, and `fix_note` Ordered steps with `step` and `at` (ISO 8601) ## Errors | Status | Meaning | | - | - | | `404` | Not found, including cross-customer access | # List Activity Source: https://docs.zerodrift.com/api-reference/activities/list-activities GET /api/activities List Command activity for your account. Return a newest-first, eventually-consistent activity feed for your account. Date ranges are capped at 30 days. ## Query Parameters Start of the window (ISO 8601 date-time) End of the window (ISO 8601 date-time) Filter by status. Repeatable. Values: `auto_fixed`, `blocked`, `warned`, `passed` Filter by agent ID. Repeatable. Filter by source. Repeatable. Filter by rule ID. Repeatable. Free-text search Opaque pagination cursor from a previous response Page size (1–100) ## Response Activity summaries Activity identifier ISO 8601 timestamp Event label `auto_fixed`, `blocked`, `warned`, or `passed` Originating source LLM provider when applicable Model name when applicable Agent `{ id, name }` Rule summaries with `id`, `name`, `severity`, `type`, and `policy` Total rules associated with the activity Number of rules omitted from the summary payload Cursor for the next page, or `null` when finished Pagination metadata Total pages for the current query Current 1-based page ## Errors | Status | Meaning | | - | - | | `400` | Invalid date range (maximum 30 days) | | `401` | Invalid API key | # Activate Rules Source: https://docs.zerodrift.com/api-reference/custom-policies/activate-rules POST /api/policies/import/{import_id}/activate Activate extracted rules from a policy import so they run during validation Activate rules extracted from a policy import. Once activated, these rules will be enforced during document validation for your account. You can activate all rules or a specific subset. This is also the activation step for a document synced from Notion, Linear, Confluence, or Google Drive. After Command shows the import as pending review, call this endpoint with that `import_id`. Re-syncing the same source document refreshes the existing policy rather than creating a second one — see [MCP Connectors](/connecting-mcp). Activated rules apply **automatically** on every validation made with your API key — there is no per-request flag to select them, and `document_category` / `document_metadata` do not control them (those only select ZeroDrift's built-in default rules). Use [Deactivate Rules](/api-reference/custom-policies/deactivate-rules) to stop a rule from running. After activation, you can [train a policy-specific adapter](/guides/train-an-adapter) for this import. ## Path Parameters The import ID to activate rules from ## Request Body Optional list of specific rule IDs to activate. If omitted, all extracted rules are activated. Set to `true` to overwrite rules that have already been activated from a previous import. Defaults to `false`. When `overwrite` is `false` (default), if a rule ID already exists the request stops and returns a `409` conflict response. The 409 error body includes an error message that identifies the conflicting rule ID and indicates which rules were successfully activated before the collision. Use `overwrite: true` to replace existing rules unconditionally. ## Response Import identifier Updated import status: `activated` Number of rules successfully activated List of activated rule IDs (prefixed with `cust__`) For `409` responses, the rule ID that caused the conflict. ```json 200 Activated theme={null} { "import_id": "550e8400-e29b-41d4-a716-446655440000", "status": "activated", "activated_count": 5, "activated_rule_ids": [ "cust_abc123_no_misleading_claims", "cust_abc123_benchmark_comparison_required", "cust_abc123_risk_disclosure_present", "cust_abc123_fee_transparency", "cust_abc123_past_performance_disclaimer" ] } ``` ```json 400 Bad Request theme={null} { "error": "No matching rule_ids found in import" } ``` ```json 409 Rule ID Conflict theme={null} { "error": "Rule 'cust_abc123_no_misleading_claims' was created by a concurrent request", "conflicting_rule_id": "cust_abc123_no_misleading_claims", "activated_rule_ids": [ "cust_abc123_benchmark_comparison_required" ] } ``` ## Example Two request shapes: activate every extracted rule, or activate a subset with `rule_ids` and `overwrite`. ```bash cURL (activate all) theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/import/550e8400-e29b-41d4-a716-446655440000/activate" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ```bash cURL (activate subset) theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/import/550e8400-e29b-41d4-a716-446655440000/activate" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "rule_ids": ["no_misleading_claims", "risk_disclosure_present"], "overwrite": true }' ``` ```python Python theme={null} import requests API_KEY = "YOUR_API_KEY" API_BASE = "https://api.zerodrift.ai" IMPORT_ID = "550e8400-e29b-41d4-a716-446655440000" # Activate all rules response = requests.post( f"{API_BASE}/api/policies/import/{IMPORT_ID}/activate", headers={ "x-api-key": API_KEY, "Content-Type": "application/json" } ) result = response.json() print(f"Activated {result['activated_count']} rules") for rule_id in result["activated_rule_ids"]: print(f" - {rule_id}") ``` ```javascript JavaScript theme={null} const axios = require('axios'); const API_KEY = 'YOUR_API_KEY'; const API_BASE = 'https://api.zerodrift.ai'; const IMPORT_ID = '550e8400-e29b-41d4-a716-446655440000'; // Activate specific rules const response = await axios.post( `${API_BASE}/api/policies/import/${IMPORT_ID}/activate`, { rule_ids: ['no_misleading_claims', 'risk_disclosure_present'], overwrite: true }, { headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' } } ); console.log(`Activated ${response.data.activated_count} rules`); response.data.activated_rule_ids.forEach(id => console.log(` - ${id}`)); ``` # Create Custom Rule Source: https://docs.zerodrift.com/api-reference/custom-policies/create-rule POST /api/policies/rules Create a customer-owned AI_SIGNAL, REGEX, or REQUIRE_NEAR rule Create a custom rule that belongs to your account. Supports `AI_SIGNAL`, `REGEX`, and `REQUIRE_NEAR` rule types. Requires a full-access API key. Created rules start active. They run on **Max** (`anchor_3_0_max`) until you train an adapter that Flagship can check. Copy `rule_id` from the response for `validation_scope.rules`. See [Enforce Content](/api-reference/validate/validate-content). ## Request Body The body is a discriminated union on `type`. Shared fields: Human-readable rule name `AI_SIGNAL`, `REGEX`, or `REQUIRE_NEAR` `fix` or `flag` `do_not_send` or `send_with_caution` Editable suggested replacement or warning guidance. Omit to allow warning fallback; `null` clears and suppresses warning fallback. Optional remediation guidance ### AI\_SIGNAL fields AI instruction for the signal Keyword gate (at least one string) `sentence`, `paragraph`, or `document` ### REGEX fields Raw Python regex (max 512 chars) ### REQUIRE\_NEAR fields Raw Python regex for the anchor pattern (max 512 chars) Raw Python regex patterns checked near the anchor Character distance around the anchor `before`, `after`, or `either` Case-insensitive matching `document` or `page` Minimum near-pattern hits required ## Response Returns the created custom rule (`CustomRuleResponse`), including `rule_id`, `source: "custom"`, `active`, timestamps, and type-specific fields. ## Errors | Status | Meaning | | - | - | | `400` | Invalid custom rule payload | | `403` | Full-access API key required | ## Example Two request bodies for the same endpoint: a `REGEX` rule and an `AI_SIGNAL` rule. ```bash cURL (REGEX) theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/rules" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Block SSN patterns", "type": "REGEX", "action": "flag", "severity": "do_not_send", "regex_pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" }' ``` ```bash cURL (AI_SIGNAL) theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/rules" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "No guaranteed returns", "type": "AI_SIGNAL", "action": "fix", "severity": "do_not_send", "prompt": "Flag language that guarantees investment returns.", "keywords": ["guarantee", "guaranteed returns"], "window": "sentence" }' ``` # Deactivate Rules Source: https://docs.zerodrift.com/api-reference/custom-policies/deactivate-rules POST /api/policies/rules/deactivate Deactivate rules by rule pack, import, rule-pack scenario selector, or specific IDs Deactivate rules so they are skipped during validation. Custom rules are soft-deactivated (preserved for audit trail). Default rules are stored as deactivated on your customer record. ## Request Body Deactivate all rules in a rule pack (e.g., `sec_finra_comms_v2`) Deactivate all custom rules from this import Deactivate all default rules for this rule-pack scenario selector Array of rule ID strings to deactivate (custom or default) At least one field is required. Multiple fields can be combined in a single request. ## Response Number of custom rules deactivated List of deactivated custom rule IDs Number of default rules deactivated List of deactivated default rule IDs List of error messages for rules that could not be deactivated. Empty array when no errors. ```json 200 OK theme={null} { "deactivated_custom_count": 2, "deactivated_custom_rule_ids": [ "cust_abc123_require_risk_disclosure", "cust_abc123_past_performance_disclaimer" ], "deactivated_default_count": 47, "deactivated_default_rule_ids": [ "signal_mnpi_leak", "signal_promissory_tone" ], "errors": [] } ``` ## Example Three ways to select what to deactivate on the same endpoint: by `rule_pack_id`, by `import_id`, or by specific `rule_ids`. ```bash cURL — by rule pack theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/rules/deactivate" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"rule_pack_id": "sec_finra_comms_v2"}' ``` ```bash cURL — by import theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/rules/deactivate" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"import_id": "550e8400-e29b-41d4-a716-446655440000"}' ``` ```bash cURL — by specific rules theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/rules/deactivate" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"rule_ids": ["signal_mnpi_leak", "cust_abc123_rule1"]}' ``` # Delete Custom Rule Source: https://docs.zerodrift.com/api-reference/custom-policies/delete-rule DELETE /api/policies/rules/{rule_id} Permanently delete a customer-owned custom rule Permanently delete a customer-owned custom rule and remove its sole policy-import reference. Only `cust_*` definitions owned by the authenticated customer can be deleted; YAML/system pack rules are rejected. Deletion is rejected while the policy is pending review, training is in progress, a trained adapter still references the policy, or multiple policy imports reference the rule. Requires a full-access API key. ## Path Parameters Custom rule identifier (must be a `cust_*` definition) ## Response Returns `204 No Content` with no body on success. ## Errors | Status | Meaning | | - | - | | `400` | Rule is not a customer-owned `cust_*` definition | | `403` | Forbidden — full-access API key required | | `404` | Custom rule not found | | `409` | Rule has conflicting policy or training references, or changed concurrently | # Edit Imported Rule Source: https://docs.zerodrift.com/api-reference/custom-policies/edit-imported-rule PATCH /api/policies/import/{import_id}/rules/{rule_id} Partially edit a pending imported rule before activation Partially edit `prompt`, remediation guidance (`fix_note`), or extraction `confidence` for one rule while its customer-owned import is pending review. Requires a full-access API key. The import must still be in a pending-review state (or equivalent pending status that allows edit); concurrent changes return `409`. ## Path Parameters Policy import identifier Extracted rule ID within the import (unprefixed) ## Request Body At least one field is required. Explicit `null` clears `prompt` or `fix_note`. `prompt` is accepted only for `AI_SIGNAL` rules. AI instruction for `AI_SIGNAL` rules. Nullable to clear. Remediation guidance. Nullable to clear. Extraction confidence from 0 through 1 ## Response Returns the updated extracted rule object (same shape as entries under [Get Import Details](/api-reference/custom-policies/get-import-details)). ## Errors | Status | Meaning | | - | - | | `400` | Invalid imported rule update | | `403` | Full-access API key required | | `404` | Import or imported rule not found | | `409` | Import is not pending review, or changed concurrently | # Edit Custom Rule Source: https://docs.zerodrift.com/api-reference/custom-policies/edit-rule PATCH /api/policies/rules/{rule_id} Partially update an existing customer-owned custom rule Partially update an existing customer-owned `AI_SIGNAL`, `REGEX`, or `REQUIRE_NEAR` rule. Rule type and ownership metadata are immutable. Active status is changed via [Activate Rules](/api-reference/custom-policies/activate-rules) / [Deactivate Rules](/api-reference/custom-policies/deactivate-rules), not this endpoint. Requires a full-access API key. Supply only fields applicable to the stored rule type. ## Path Parameters Custom rule identifier ## Request Body At least one field is required. Common editable fields: Human-readable rule name `fix` or `flag` `do_not_send` or `send_with_caution` Editable suggestion. Omit to preserve; `null` clears and suppresses warning fallback. Remediation guidance. Nullable to clear. Type-specific fields match [Create Custom Rule](/api-reference/custom-policies/create-rule) (`prompt` / `keywords` / `window` for `AI_SIGNAL`, `regex_pattern` for `REGEX`, `anchor` / `near` / distance options for `REQUIRE_NEAR`). ## Response Returns the updated custom rule definition (same shape as [Get Custom Rule](/api-reference/custom-policies/get-rule)). ## Errors | Status | Meaning | | - | - | | `400` | Invalid custom rule update | | `403` | Full-access API key required | | `404` | Custom rule not found | | `409` | Concurrent custom rule update | # Get Import Details Source: https://docs.zerodrift.com/api-reference/custom-policies/get-import-details GET /api/policies/import/{import_id} Retrieve details and extracted rules for a specific policy import Retrieve the full details of a policy import, including all extracted rules and their properties. Use this endpoint to poll for results after submitting an import, and to review rules before activation. Extraction runs in the background. Small documents are usually `pending_review` within seconds; large documents (hundreds of pages) are extracted in chunks and can stay in `processing` for several minutes. Poll until the status is no longer `processing`. ## Path Parameters The import ID returned from the import endpoint ## Response Unique import identifier Account the import belongs to Import status: `processing`, `pending_review`, `no_rules_found`, `failed`, `activated`, or `partially_activated` Where the document came from: a connector's provider slug (`notion`, `linear`, `atlassian`, `google_drive`) when a connected workspace produced it, `uploaded` when it was staged through this API as a file, or `built` when it was authored in your own UI. Treat this as an open set — a new connector adds a new slug. How the content was submitted: `file` or `text` How the stored document is read during extraction: `raw` for uploaded bytes, `text` for plain text Original filename, when the import was submitted with one Number of extracted rules (present once extraction completes) ISO 8601 timestamp of the import ISO 8601 timestamp of the most recent activation, when any rules have been activated Customer-prefixed rule IDs activated from this import, sorted. Present once at least one rule has been activated. Error details when status is `failed` Full list of extracted rules with details. Every rule carries the whole field set; fields that do not apply to the rule's `type` are returned as `null`. Rule identifier within the import Rule type: `AI_SIGNAL`, `REGEX`, or `REQUIRE_NEAR` Human-readable rule name Longer description of the rule, when the extraction produced one `do_not_send` or `send_with_caution`. Rules extracted before these values existed are normalized on read, so the response never returns `low`, `medium`, or `high`. Suggested action: `flag` (warn only) or `fix` (suggest replacement text). Unknown labels may be preserved for review, but activation rejects unsupported actions. Source policy text retained as provenance, not an editable runtime condition AI instruction (`AI_SIGNAL` only). Omitted for other rule types, and `null` when cleared via [Edit Imported Rule](/api-reference/custom-policies/edit-imported-rule). Present on every extracted rule; used as a runtime gate only by `AI_SIGNAL` rules Context window used by `AI_SIGNAL` rules: `sentence`, `paragraph`, or `document` Raw Python regex (`REGEX` only) Remediation guidance extracted from the source policy, when present Exact suggestion preserved by the import. Explicit `null` suppresses the `fix_note` fallback; when omitted, a warning rule may derive its suggestion from `fix_note`. Extraction confidence from 0 through 1 Derived at read time; `true` when the pending rule can run against a text snippet Derived at read time: the customer-prefixed rule ID once this rule has been activated, `null` while it is still pending — including pending siblings in a `partially_activated` import Anchor regex (`REQUIRE_NEAR` only) Regex patterns checked near the anchor (`REQUIRE_NEAR` only) Character distance from the anchor (`REQUIRE_NEAR`, default `100`) `before`, `after`, or `either` (`REQUIRE_NEAR`, default `either`) Case-insensitive matching (`REQUIRE_NEAR`, default `false`) `document` or `page` (`REQUIRE_NEAR`, default `document`) Minimum near hits (`REQUIRE_NEAR`, default `1`) ### Connector provenance Imports produced by a connected workspace also carry the audit trail of where the document came from. These fields are absent on documents uploaded through this API. Connector that produced the document The connection the document was fetched through The provider's own identifier for the source document The document's URL at the provider, recorded verbatim The document's title at the provider, when it has one Who the provider says last edited the document When the provider says it last changed, in the provider's own format The MCP tool whose result carried the document Digest of the imported content as `sha256:`. Re-importing content with this digest is a no-op. Size of the document the connector fetched, as the connector measured it Media type the provider reported for the document, e.g. `text/markdown` When the connector fetched the document, ISO 8601 UTC ```json 200 Success theme={null} { "import_id": "10f4ed8e-5240-4d87-8828-6e95f3e58a93", "customer_id": "9a06364f-17c7-4a3f-9d98-61bde8696a2a", "status": "pending_review", "origin": "uploaded", "source_type": "file", "document_kind": "raw", "filename": "Client_Communications_Policy.pdf", "rule_count": 2, "created_at": "2026-10-05T09:23:39.277706+00:00", "extracted_rules": [ { "id": "no_performance_guarantees", "name": "No Performance Guarantees", "type": "AI_SIGNAL", "severity": "do_not_send", "action": "flag", "prompt": "Check if the document states or implies guaranteed returns, risk-free outcomes, or certainty of future investment performance. Return 1 if violation found, 0 if compliant.", "source_text": "Employees must not state or imply guaranteed returns, risk-free outcomes, or certainty of future investment performance in any client communication.", "keywords": [ "guaranteed returns", "risk-free outcomes", "certainty of future investment performance", "can't lose" ], "window": "document", "fix_note": "Remove any statements or implications of guaranteed returns or risk-free outcomes from the communication.", "confidence": 0.95, "regex_pattern": null, "anchor": null, "near": null, "distance_chars": null, "direction": null, "case_insensitive": null, "mode": null, "min_hits": null, "simulatable": true, "activated_rule_id": null }, { "id": "risk_disclosure_present", "name": "Risk Disclosure Near Performance Data", "type": "REQUIRE_NEAR", "severity": "send_with_caution", "action": "flag", "source_text": "Any presentation of past performance must appear alongside the standard risk disclosure.", "keywords": [ "past performance", "risk disclosure" ], "window": null, "anchor": "past performance", "near": [ "past performance is not indicative of future results" ], "distance_chars": 300, "direction": "either", "case_insensitive": true, "mode": "document", "min_hits": 1, "fix_note": "Add the standard risk disclosure near the performance figures.", "confidence": 0.92, "regex_pattern": null, "simulatable": true, "activated_rule_id": null } ] } ``` ```json 404 Not Found theme={null} { "error": "Import 10f4ed8e-5240-4d87-8828-6e95f3e58a93 not found" } ``` ## Errors | Status | Meaning | | - | - | | `403` | Invalid API key, or the import belongs to another customer | | `404` | Import not found | | `500` | Failed to retrieve import | # Get Custom Rule Source: https://docs.zerodrift.com/api-reference/custom-policies/get-rule GET /api/policies/rules/{rule_id} Return the full editable definition for a customer-owned custom rule Return the full editable definition for a customer-owned custom rule. ## Path Parameters Custom rule identifier (typically prefixed with `cust_`) ## Response Rule identifier Human-readable rule name `AI_SIGNAL`, `REGEX`, or `REQUIRE_NEAR` `fix` or `flag` `do_not_send` or `send_with_caution` Stored suggested replacement or warning guidance when present Always `custom` Whether the rule currently runs during validation Remediation guidance when present AI instruction (`AI_SIGNAL` only) Keyword gate (`AI_SIGNAL` only) `sentence`, `paragraph`, or `document` (`AI_SIGNAL` only) Regex pattern (`REGEX` only) Anchor regex (`REQUIRE_NEAR` only) Near-pattern regexes (`REQUIRE_NEAR` only) Character distance (`REQUIRE_NEAR`) `before`, `after`, or `either` (`REQUIRE_NEAR`) Case-insensitive matching (`REQUIRE_NEAR`) `document` or `page` (`REQUIRE_NEAR`) Minimum near hits (`REQUIRE_NEAR`) Source import ID when the rule was created via policy import ISO 8601 creation timestamp ISO 8601 last-update timestamp when present ## Errors | Status | Meaning | | - | - | | `403` | Forbidden | | `404` | Custom rule not found | # Import Policy Source: https://docs.zerodrift.com/api-reference/custom-policies/import-policy POST /api/policies/import Upload a policy and extract enforceable rules. Upload a policy and extract enforceable rules. The endpoint returns immediately with a `processing` status while rule extraction runs in the background. Poll [Get Import Details](/api-reference/custom-policies/get-import-details) to check when extraction is complete. Documents you pick from a Notion, Linear, Confluence, or Google Drive connection go through this same import pipeline. Connect the source in Command, then treat the resulting `import_id` like any other import — see [MCP Connectors](/connecting-mcp). This endpoint sends the document inline in the request body (base64-encoded when `source` is `file`), which is capped by API Gateway / Lambda payload limits (a few MB after encoding). For large policy files, use the presigned upload flow instead: [Get Import Presigned URL](/api-reference/custom-policies/import-presigned-url) → upload to S3 → [Start Import](/api-reference/custom-policies/start-import). ## Request Body Content type indicator: `file` for base64-encoded documents or `text` for plain text. The policy content. Base64-encoded document bytes when `source` is `file`, or plain text when `source` is `text`. Original filename for reference (optional, used with `file` source). ## Response Unique identifier for the import. Use this to poll for status, view details, or activate rules. Initial status: `processing`. Poll the [Get Import Details](/api-reference/custom-policies/get-import-details) endpoint until status transitions to `pending_review`, `no_rules_found`, or `failed`. Human-readable message with next steps. ```json 202 Import Accepted theme={null} { "import_id": "550e8400-e29b-41d4-a716-446655440000", "status": "processing", "message": "Import accepted. Poll GET /api/policies/import/{import_id} for results." } ``` ```json 400 Bad Request theme={null} { "error": "Missing required field: source" } ``` ```json 422 Unprocessable Entity theme={null} { "error": "Cannot extract text from document" } ``` ## Status Lifecycle | Status | Description | | - | - | | `processing` | Import accepted, AI extraction in progress | | `pending_review` | Extraction complete, rules ready for review and activation | | `no_rules_found` | Extraction complete but no compliance rules were identified | | `failed` | Extraction failed (see `error_message` in [Get Import Details](/api-reference/custom-policies/get-import-details)) | | `activated` | All extracted rules have been activated | | `partially_activated` | Some rules activated, others still pending | ## Example Two request shapes (`source: "file"` with a base64 PDF, and `source: "text"`), then poll [Get Import Details](/api-reference/custom-policies/get-import-details) until extraction finishes. ```bash cURL (file) theme={null} DOC_BASE64=$(base64 -i compliance-policy.pdf) # Submit the import curl -X POST "https://api.zerodrift.ai/api/policies/import" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"source\": \"file\", \"content\": \"$DOC_BASE64\", \"filename\": \"compliance-policy.pdf\" }" # Poll for results (returns 'processing' until extraction completes) curl -X GET "https://api.zerodrift.ai/api/policies/import/IMPORT_ID" \ -H "x-api-key: YOUR_API_KEY" ``` ```bash cURL (text) theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/import" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "text", "content": "All marketing materials must include a risk disclosure. Performance claims require a 1-year, 5-year, and since-inception comparison to the benchmark." }' ``` ```python Python theme={null} import requests import base64 import time API_KEY = "YOUR_API_KEY" API_BASE = "https://api.zerodrift.ai" # Submit file import with open("compliance-policy.pdf", "rb") as f: doc_bytes = base64.b64encode(f.read()).decode() response = requests.post( f"{API_BASE}/api/policies/import", headers={ "x-api-key": API_KEY, "Content-Type": "application/json" }, json={ "source": "file", "content": doc_bytes, "filename": "compliance-policy.pdf" } ) import_id = response.json()["import_id"] print(f"Import ID: {import_id} — polling for results...") # Poll until extraction completes while True: details = requests.get( f"{API_BASE}/api/policies/import/{import_id}", headers={"x-api-key": API_KEY} ).json() if details["status"] != "processing": break time.sleep(3) print(f"Status: {details['status']}") if details.get("rule_count") is not None: print(f"Rules extracted: {details['rule_count']}") ``` ```javascript JavaScript theme={null} const fs = require('fs'); const axios = require('axios'); const API_KEY = 'YOUR_API_KEY'; const API_BASE = 'https://api.zerodrift.ai'; const docBytes = fs.readFileSync('compliance-policy.pdf').toString('base64'); // Submit import const { data } = await axios.post( `${API_BASE}/api/policies/import`, { source: 'file', content: docBytes, filename: 'compliance-policy.pdf' }, { headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' } } ); console.log(`Import ID: ${data.import_id} — polling for results...`); // Poll until extraction completes let details; do { await new Promise(r => setTimeout(r, 3000)); const res = await axios.get( `${API_BASE}/api/policies/import/${data.import_id}`, { headers: { 'x-api-key': API_KEY } } ); details = res.data; } while (details.status === 'processing'); console.log(`Status: ${details.status}`); if (details.rule_count != null) { console.log(`Rules extracted: ${details.rule_count}`); } ``` # Get Import Presigned URL Source: https://docs.zerodrift.com/api-reference/custom-policies/import-presigned-url POST /api/policies/import/presigned_url Get an S3 presigned URL for importing large policy files Step 1 of the large-file custom policy import flow. Returns an `import_id` and an S3 presigned `upload_url` so you can upload large policy documents directly to S3, bypassing the API Gateway and Lambda payload limits that cap the inline [Import Policy](/api-reference/custom-policies/import-policy) endpoint. After uploading, call [Start Import](/api-reference/custom-policies/start-import) with the `import_id` to begin rule extraction. Use the inline [Import Policy](/api-reference/custom-policies/import-policy) endpoint for plain text and small files. Use this presigned flow for large files (above a few MB). Maximum upload size is **1 GB**. Large documents (e.g. a multi-hundred-page regulatory handbook) are fully extracted — rule extraction runs in the background in chunks, so the entire document is analyzed, not just the first pages. The trade-off is time: a very large document can stay in `processing` for several minutes. Keep polling [Get Import Details](/api-reference/custom-policies/get-import-details) until the status leaves `processing`. ## Request Body Original filename, for reference/audit. MIME type of the file you will upload. Must match the `Content-Type` header you send on the PUT. ## Response Unique identifier for the import. Pass this to [Start Import](/api-reference/custom-policies/start-import) after uploading. S3 presigned URL for the file upload (HTTP PUT). HTTP method to use for upload (always `PUT`). S3 bucket name. S3 object key the file will be uploaded to. **Important:** Headers that MUST be included on the upload PUT. The presigned URL is signed with these, so omitting any results in a `SignatureDoesNotMatch` error from S3. Seconds until the presigned URL expires (default 900). ISO 8601 timestamp when the URL expires. Maximum allowed upload size in bytes, enforced at [Start Import](/api-reference/custom-policies/start-import). Step-by-step instructions for uploading and starting the import. ```json 200 OK theme={null} { "import_id": "550e8400-e29b-41d4-a716-446655440000", "upload_url": "https://example-bucket.s3.amazonaws.com/policy-imports/raw/550e8400-e29b-41d4-a716-446655440000.bin?X-Amz-Algorithm=...", "upload_method": "PUT", "s3_bucket": "example-bucket", "s3_key": "policy-imports/raw/550e8400-e29b-41d4-a716-446655440000.bin", "required_headers": { "Content-Type": "application/pdf" }, "expires_in_seconds": 900, "expires_at": "2026-06-15T10:45:00Z", "max_upload_bytes": 1073741824, "next_steps": { "step_1": "PUT the raw file bytes to upload_url with the exact required_headers", "step_2": "POST /api/policies/import/start with { \"import_id\": \"550e8400-...\" } to begin extraction" } } ``` **Required headers:** Include **all** headers from `required_headers` exactly as provided when uploading your file. Missing or mismatched headers (including `Content-Type`) can cause S3 to reject the upload with `SignatureDoesNotMatch`. ## Example Get a presigned URL from this endpoint, then `PUT` the file directly to S3 with every header from `required_headers`. Rule extraction still needs a follow-up [Start Import](/api-reference/custom-policies/start-import) call. ```bash cURL theme={null} # Step 1: Get the presigned URL RESPONSE=$(curl -s -X POST "https://api.zerodrift.ai/api/policies/import/presigned_url" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "compliance-policy.pdf", "content_type": "application/pdf" }') IMPORT_ID=$(printf '%s' "$RESPONSE" | jq -r '.import_id') UPLOAD_URL=$(printf '%s' "$RESPONSE" | jq -r '.upload_url') # Build curl -H args from required_headers CURL_HEADERS=() while IFS=$'\t' read -r key value; do CURL_HEADERS+=(-H "$key: $value") done < <(printf '%s' "$RESPONSE" | jq -r '.required_headers | to_entries[] | "\(.key)\t\(.value)"') # Step 2: Upload the raw file directly to S3 (no API key) curl -X PUT "$UPLOAD_URL" \ "${CURL_HEADERS[@]}" \ --data-binary @compliance-policy.pdf echo "Import ID: $IMPORT_ID" # Step 3: call POST /api/policies/import/start with this import_id ``` ```python Python theme={null} import requests API_KEY = "YOUR_API_KEY" API_BASE = "https://api.zerodrift.ai" # Step 1: Get the presigned URL resp = requests.post( f"{API_BASE}/api/policies/import/presigned_url", headers={"x-api-key": API_KEY, "Content-Type": "application/json"}, json={"filename": "compliance-policy.pdf", "content_type": "application/pdf"}, ).json() import_id = resp["import_id"] upload_url = resp["upload_url"] required_headers = resp["required_headers"] # Step 2: Upload the raw file directly to S3 with open("compliance-policy.pdf", "rb") as f: requests.put(upload_url, headers=required_headers, data=f) print(f"Import ID: {import_id}") # Step 3: call POST /api/policies/import/start with this import_id ``` ```javascript JavaScript theme={null} const fs = require('fs'); const axios = require('axios'); const API_KEY = 'YOUR_API_KEY'; const API_BASE = 'https://api.zerodrift.ai'; (async () => { // Step 1: Get the presigned URL const { data } = await axios.post( `${API_BASE}/api/policies/import/presigned_url`, { filename: 'compliance-policy.pdf', content_type: 'application/pdf' }, { headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' } } ); // Step 2: Upload the raw file directly to S3 with the signed required_headers const fileStream = fs.createReadStream('compliance-policy.pdf'); await axios.put(data.upload_url, fileStream, { headers: data.required_headers, maxBodyLength: Infinity, maxContentLength: Infinity, }); console.log(`Import ID: ${data.import_id}`); // Step 3: call POST /api/policies/import/start with this import_id })(); ``` # List Policy Imports Source: https://docs.zerodrift.com/api-reference/custom-policies/list-imports GET /api/policies/import List all policy imports for your account Retrieve a list of all policy imports associated with your API key. ## Response Array of import records Unique import identifier Import status: `processing`, `pending_review`, `no_rules_found`, `failed`, `activated`, or `partially_activated` How the content was submitted: `file` or `text` Where the document came from: a connector's provider slug (`notion`, `linear`, `atlassian`, `google_drive`), `uploaded`, or `built` Number of rules extracted Original filename, when the import was submitted with one ISO 8601 timestamp of the import ISO 8601 timestamp of the most recent activation, or `null` when nothing has been activated Total number of imports ```json 200 Success theme={null} { "imports": [ { "import_id": "550e8400-e29b-41d4-a716-446655440000", "status": "activated", "source_type": "file", "origin": "uploaded", "rule_count": 5, "filename": "trading-policy.pdf", "created_at": "2026-01-15T10:30:00Z", "activated_at": "2026-01-15T11:02:00Z" }, { "import_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "status": "pending_review", "source_type": "text", "origin": "notion", "rule_count": 3, "filename": "Client Communications Policy", "created_at": "2026-01-20T14:15:00Z", "activated_at": null } ], "count": 2 } ``` # List Rules Source: https://docs.zerodrift.com/api-reference/custom-policies/list-rules GET /api/policies/rules List all default and custom rules with their active/deactivated status Retrieve all rules for your account, including default rules from rule packs and custom rules from policy imports. Each rule includes an `active` field indicating whether it will run during validation. Use `rule_id` from this response in `validation_scope.rules` on [Enforce Content](/api-reference/validate/validate-content). Custom IDs are prefixed `cust_`. For rulepack IDs, use `id` from [Rule Packs](/api-reference/rulepacks), not `rule_pack_id` on this list. Untrained custom rules run on `anchor_3_0_max`. ## Response Your customer identifier Default rules from rule packs Rule identifier (e.g., `signal_mnpi_leak`) `AI_SIGNAL` or `REGEX` Always `default` for built-in rules Whether this rule is currently active for your account Whether this rule can run in simulation. Non-simulatable rules are still listed so clients can render them disabled. Rule pack this rule belongs to (e.g., `sec_finra_comms_v2`) Rule-pack grouping identifiers for this rule Confidence threshold for AI signals Threshold comparison operator (e.g., `>=`) Custom rules from policy imports Rule identifier (prefixed with `cust_`) `AI_SIGNAL`, `REGEX`, or `REQUIRE_NEAR` Human-readable rule name Always `custom` for customer-owned rules Whether this rule is currently active Whether this rule can run in simulation Import ID this rule was extracted from (when created via policy import) `do_not_send` or `send_with_caution` `fix` or `flag` Text window for evaluation ISO 8601 creation timestamp Total number of default rules (active + deactivated) Number of active default rules Total number of custom rules (active + deactivated) Number of active custom rules Total rules across default + custom Total active rules across default + custom ```json 200 OK theme={null} { "customer_id": "abc-123", "default_rules": [ { "rule_id": "signal_mnpi_leak", "type": "AI_SIGNAL", "source": "default", "active": true, "rule_pack_id": "sec_finra_comms_v2", "rule_pack_scenario_ids": [], "threshold": 0.60, "operator": ">=" } ], "custom_rules": [ { "rule_id": "cust_abc123_require_risk_disclosure", "type": "AI_SIGNAL", "name": "Risk Disclosure Required", "severity": "do_not_send", "source": "custom", "active": true, "import_id": "550e8400-e29b-41d4-a716-446655440000" } ], "default_rules_count": 119, "default_rules_active_count": 115, "custom_rules_count": 5, "custom_rules_active_count": 5, "total_rules_count": 124, "total_active_count": 120 } ``` # Reactivate Rules Source: https://docs.zerodrift.com/api-reference/custom-policies/reactivate-rules POST /api/policies/rules/activate Reactivate previously deactivated rules Reactivate rules that were previously deactivated. Custom rules have their status set back to active. Default rules are removed from your deactivated list. This endpoint is for re-activating **previously deactivated** rules. To activate rules from a new policy import for the first time, use [POST /api/policies/import//activate](/api-reference/custom-policies/activate-rules) instead. ## Request Body Reactivate all rules in a rule pack (e.g., `sec_finra_comms_v2`) Reactivate all custom rules from this import Reactivate all default rules for this rule-pack scenario selector Array of rule ID strings to reactivate (custom or default) At least one field is required. Multiple fields can be combined in a single request. ## Response Number of custom rules reactivated List of reactivated custom rule IDs Number of default rules reactivated List of reactivated default rule IDs List of error messages for rules that could not be activated. Empty array when no errors. ```json 200 OK theme={null} { "activated_custom_count": 3, "activated_custom_rule_ids": [ "cust_abc123_require_risk_disclosure", "cust_abc123_past_performance_disclaimer", "cust_abc123_no_specific_client_returns" ], "activated_default_count": 0, "activated_default_rule_ids": [], "errors": [] } ``` ## Example Two ways to select what to reactivate on the same endpoint: by `rule_pack_id` or by `import_id`. ```bash cURL — by rule pack theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/rules/activate" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"rule_pack_id": "sec_finra_comms_v2"}' ``` ```bash cURL — by import theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/rules/activate" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"import_id": "550e8400-e29b-41d4-a716-446655440000"}' ``` # Start Import Source: https://docs.zerodrift.com/api-reference/custom-policies/start-import POST /api/policies/import/start Start rule extraction after uploading a policy file via presigned URL Step 2 of the large-file custom policy import flow (after the S3 upload). Call this after uploading your file to the presigned `upload_url` from [Get Import Presigned URL](/api-reference/custom-policies/import-presigned-url). It verifies the uploaded object and enforces the size cap, then — once malware scanning has completed successfully — starts asynchronous rule extraction and returns `status: "processing"`. If the scan is still running, it returns `status: "scan_pending"` immediately instead; retry the call until extraction starts (see the note below). Poll [Get Import Details](/api-reference/custom-policies/get-import-details) until the status transitions to `pending_review`, `no_rules_found`, or `failed` — identical to the inline [Import Policy](/api-reference/custom-policies/import-policy) flow. ## Request Body The `import_id` returned by [Get Import Presigned URL](/api-reference/custom-policies/import-presigned-url). Optional filename to record on the import (overrides the value from the presigned step). ## Response Unique identifier for the import. Use it to poll for status or activate rules. `processing` once extraction has started, or `scan_pending` if the malware scan is still running (retry shortly). Human-readable message with next steps. ```json 202 Import Accepted theme={null} { "import_id": "550e8400-e29b-41d4-a716-446655440000", "status": "processing", "message": "Import accepted. Poll GET /api/policies/import/{import_id} for results." } ``` ```json 202 Scan Pending theme={null} { "import_id": "550e8400-e29b-41d4-a716-446655440000", "status": "scan_pending", "message": "File is still being scanned. Retry POST /api/policies/import/start shortly." } ``` ```json 404 Not Found theme={null} { "error": "No uploaded file found for this import_id", "import_id": "550e8400-e29b-41d4-a716-446655440000", "message": "Upload the file with the presigned URL before calling /start" } ``` ## Error Responses | Status | Description | | - | - | | 400 | Missing/invalid `import_id`, or the uploaded file is empty | | 403 | Invalid API key, or the import belongs to another customer | | 404 | No uploaded file found for this `import_id` (upload step skipped or failed) | | 409 | Import already started or completed | | 413 | Uploaded file exceeds the maximum allowed size (1 GB) | | 422 | File failed malware/content scanning | If you receive a `202` with `status: "scan_pending"`, the malware scan is still running (usually a few seconds). Retry this request shortly — rule extraction does not start until the scan completes successfully. ## Example Retry this endpoint while malware scan is still `scan_pending`. The Python sample then polls [Get Import Details](/api-reference/custom-policies/get-import-details) until extraction finishes. The cURL sample is the single POST. ```bash cURL theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/import/start" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "import_id": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python Python theme={null} import requests import time API_KEY = "YOUR_API_KEY" API_BASE = "https://api.zerodrift.ai" import_id = "550e8400-e29b-41d4-a716-446655440000" # Start (retry while the malware scan is still pending) while True: resp = requests.post( f"{API_BASE}/api/policies/import/start", headers={"x-api-key": API_KEY, "Content-Type": "application/json"}, json={"import_id": import_id}, ).json() if resp.get("status") != "scan_pending": break time.sleep(3) # An error payload (e.g. 404) has no "status" — surface it instead of crashing if "status" not in resp: raise SystemExit(f"Start failed: {resp.get('error', resp)}") print(resp["status"]) # Poll for extraction results while True: details = requests.get( f"{API_BASE}/api/policies/import/{import_id}", headers={"x-api-key": API_KEY}, ).json() if details.get("status") != "processing": break time.sleep(3) print(f"Status: {details.get('status', details)}") ``` ```javascript JavaScript theme={null} const axios = require('axios'); const API_KEY = 'YOUR_API_KEY'; const API_BASE = 'https://api.zerodrift.ai'; const importId = '550e8400-e29b-41d4-a716-446655440000'; (async () => { // Start (retry while the malware scan is still pending) let started; while (true) { try { const res = await axios.post( `${API_BASE}/api/policies/import/start`, { import_id: importId }, { headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' } } ); started = res.data; } catch (err) { const payload = err.response?.data ?? err.message; throw new Error(`Start failed: ${typeof payload === 'string' ? payload : JSON.stringify(payload)}`); } if (started?.status !== 'scan_pending') break; await new Promise(r => setTimeout(r, 3000)); } if (!started?.status) { throw new Error(`Start failed: ${JSON.stringify(started)}`); } console.log(started.status); })(); ``` # Errors Source: https://docs.zerodrift.com/api-reference/errors Every HTTP status code the ZeroDrift Enforcement API returns, and what to do. HTTP errors return JSON. Responses generated by the API gateway use a top-level `message` field: ```json theme={null} { "message": "Forbidden" } ``` Validation and application errors may instead use a top-level `detail` field. Read either field when presenting an error to an operator. | Code | Meaning | What to do | | - | - | - | | `400` | Bad request, such as invalid parameters, training options, pagination, or an Activity range over 30 days | Correct the field named in the response. | | `401` | Invalid API key on the Activity API | Check the `x-api-key` header and the key in Command. | | `403` | Missing, invalid, inactive, or unresolved API key; or a key without permission for the operation | Check the key, or use a full-access key for write operations. | | `404` | Unknown `job_id`, `import_id`, `rule_id`, `activity_id`, or rulepack selector | Check the id. Jobs and activities age out according to [Data Retention](/data-retention). | | `409` | Resource-state conflict, such as an import that is not activated or a training run already in progress | Complete the required state transition or wait for the active operation. | | `410` | The original policy document expired | Re-import and activate the policy, then retry training. | | `413` | An uploaded policy is too large | Use a smaller document or the supported presigned-upload limits. | | `422` | Content, a policy document, or request fields could not be processed | Correct or provide fuller input. | | `429` | Rate limit exceeded | Retry with backoff. The standard limit is 10 requests per second with a burst of 50. | | `500` | Internal server error | Follow the endpoint-specific guidance. Some operations are safe to retry; `POST /api/policies/import/start` must not be retried with the same `import_id`. | | `502` | Training Studio upstream request failed | Retry with backoff. | | `503` | Training is not enabled, or the service is temporarily unavailable | For temporary failures, retry. For a disabled feature, contact support. | The exact set varies by endpoint. Each endpoint page lists the statuses that it returns, and the [OpenAPI specification](/api-reference/openapi) is the machine-readable contract. ## Retry pattern Retry only transient failures and rate limits: ```python theme={null} import time import requests def call_with_retry(send, retryable_statuses, attempts=4): response = None for attempt in range(attempts): response = send() if response.status_code not in retryable_statuses: return response if attempt == attempts - 1: break retry_after = response.headers.get("Retry-After") delay = float(retry_after) if retry_after else min(2**attempt, 8) time.sleep(delay) return response ``` Choose `retryable_statuses` from the endpoint's documented responses. For example, pass `{429, 500, 502, 503}` only when that operation is safe to repeat. For `POST /api/policies/import/start`, do not include `500`: create a new upload and `import_id` before trying again. Do not retry `400`, `401`, `403`, `404`, `409`, `410`, `413`, or `422` without changing the request or resource state. Never re-send content that received a Block verdict. ## Job failures are not HTTP errors An asynchronous enforcement job can finish with `status: "failed"`. In that case, the `error` field on the job explains the failure while the poll request itself returns HTTP `200`. See [Enforce Content](/api-reference/validate/validate-content). ```json theme={null} { "api_version": "v3", "job_id": "a1b2c3", "status": "failed", "mode": "async", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0", "error": "Validation engine unavailable" } ``` # Introduction Source: https://docs.zerodrift.com/api-reference/introduction ZeroDrift Enforcement API ## Overview The ZeroDrift Enforcement API enforces regulations, company policies, and security controls on content and communications. Every result is a verdict. Pass. Fix. Block. Escalate. The API returns the verdict; Guard applies it inline. ## Authentication All API endpoints require authentication via API Key in the request header. | Header | Description | | - | - | | `x-api-key` | Your ZeroDrift API key | Your Enforcement API key is provisioned when your account is created. ```bash theme={null} curl "https://api.zerodrift.ai/api/rulepacks/" \ -H "x-api-key: $ZERODRIFT_API_KEY" ``` ## Quick Start Enforcement runs in two steps. Send content and get a job ID. Poll for the verdict. ``` POST /api/v3/content/validate → GET /api/v3/jobs/{job_id} ``` 1. Send content with [Enforce Content](/api-reference/validate/validate-content) — the response returns a `job_id` 2. Poll [Get Verdict](/api-reference/validate/get-results) until the job is `completed` or `failed` ### Policy Import a policy and ZeroDrift extracts enforceable rules. Review, activate, then enforce. Training Studio is optional and Flagship-only (`model_engine: "anchor_3_0"`). On Max (`anchor_3_0_max`), skip training and pass the import in `validation_scope.imports`. See [Choose a model](/models) and [Pricing](/pricing). ``` import → poll extraction → review/simulate → activate → enforce ``` 1. Upload a policy document (PDF, DOCX, or plain text), or sync one from a [connected MCP source](/connecting-mcp) 2. Poll until AI rule extraction completes 3. Review or simulate the rules, then activate them 4. Enforce with the import ID in `validation_scope.imports` and `model_engine` set to `anchor_3_0` (Flagship) or `anchor_3_0_max` (Max) 5. Optionally on Flagship, [start adapter training](/api-reference/training-studio/train-policy) and [poll training status](/api-reference/training-studio/get-training-status) until the adapter succeeds. Training does not change whether activated rules run. You can also create and edit custom rules directly via `POST /api/policies/rules` and `PATCH /api/policies/rules/{rule_id}`. Untrained custom rules run on `anchor_3_0_max`. See [Enforce Content](/api-reference/validate/validate-content). ### Activity ``` GET /api/activities → GET /api/activities/{activity_id} ``` Every verdict is logged in Command. The Activity endpoints read that log for the last 30 days. ## OpenAPI specification A machine-readable OpenAPI 3.0 document of the public API is available at [`/openapi.json`](/openapi.json). See [OpenAPI specification](/api-reference/openapi) for the operations it covers, and [SDKs](/generate-sdk) to produce a typed Python or TypeScript client from it. ## Rate Limits | Tier | Requests/Second | Burst | | - | - | - | | Standard | 10 | 50 | See [Errors](/api-reference/errors) for `429` handling. Contact [support@zerodrift.ai](mailto:support@zerodrift.ai) for higher limits. ## Support For API support or to request higher rate limits, contact: **[support@zerodrift.ai](mailto:support@zerodrift.ai)** For data handling and retention, see [Data Retention](/data-retention). # OpenAPI specification Source: https://docs.zerodrift.com/api-reference/openapi Machine-readable OpenAPI 3.0 spec for the ZeroDrift Enforcement API The ZeroDrift Enforcement API is published as an OpenAPI 3.0 document. Use it to generate clients, import the API into Postman or similar tools, or give a language model a single file that describes every publicly documented endpoint. For a walkthrough in Python and TypeScript, see [SDKs](/generate-sdk). Open or download the OpenAPI 3.0 spec (JSON). ZeroDrift serves this file at `/openapi.json` on the docs site. After deploy, that URL is: ```text theme={null} https://docs.zerodrift.com/openapi.json ``` Fetch it with: ```bash theme={null} curl https://docs.zerodrift.com/openapi.json ``` ZeroDrift also lists the spec in the auto-generated [`llms.txt`](https://docs.zerodrift.com/llms.txt) index, which AI tools use to discover pages and API files. This spec covers enforcement, policy, activity, Training Studio, and rule packs. ## Operations | Group | Method | Path | Docs | | - | - | - | - | | Enforce | `POST` | `/api/v3/content/validate` | [Enforce Content](/api-reference/validate/validate-content) | | Enforce | `GET` | `/api/v3/jobs/{job_id}` | [Get Verdict](/api-reference/validate/get-results) | | Policy | `POST` | `/api/policies/import` | [Import Policy](/api-reference/custom-policies/import-policy) | | Policy | `GET` | `/api/policies/import` | [List Policy Imports](/api-reference/custom-policies/list-imports) | | Policy | `POST` | `/api/policies/import/presigned_url` | [Get Import Presigned URL](/api-reference/custom-policies/import-presigned-url) | | Policy | `POST` | `/api/policies/import/start` | [Start Import](/api-reference/custom-policies/start-import) | | Policy | `GET` | `/api/policies/import/{import_id}` | [Get Import Details](/api-reference/custom-policies/get-import-details) | | Policy | `PATCH` | `/api/policies/import/{import_id}/rules/{rule_id}` | [Edit Imported Rule](/api-reference/custom-policies/edit-imported-rule) | | Policy | `POST` | `/api/policies/import/{import_id}/activate` | [Activate Rules](/api-reference/custom-policies/activate-rules) | | Policy | `GET` | `/api/policies/rules` | [List Rules](/api-reference/custom-policies/list-rules) | | Policy | `POST` | `/api/policies/rules` | [Create Custom Rule](/api-reference/custom-policies/create-rule) | | Policy | `GET` | `/api/policies/rules/{rule_id}` | [Get Custom Rule](/api-reference/custom-policies/get-rule) | | Policy | `PATCH` | `/api/policies/rules/{rule_id}` | [Edit Custom Rule](/api-reference/custom-policies/edit-rule) | | Policy | `DELETE` | `/api/policies/rules/{rule_id}` | [Delete Custom Rule](/api-reference/custom-policies/delete-rule) | | Policy | `POST` | `/api/policies/rules/deactivate` | [Deactivate Rules](/api-reference/custom-policies/deactivate-rules) | | Policy | `POST` | `/api/policies/rules/activate` | [Reactivate Rules](/api-reference/custom-policies/reactivate-rules) | | Training Studio | `POST` | `/api/policies/import/{import_id}/train` | [Train Policy Adapter](/api-reference/training-studio/train-policy) | | Training Studio | `GET` | `/api/policies/import/{import_id}/train` | [Get Training Status](/api-reference/training-studio/get-training-status) | | Activity | `GET` | `/api/activities` | [List Activity](/api-reference/activities/list-activities) | | Activity | `GET` | `/api/activities/{activity_id}` | [Get Activity](/api-reference/activities/get-activity) | | Policy | `GET` | `/api/rulepacks/` | [Rule Packs](/api-reference/rulepacks) | ## Authentication Every operation uses the `x-api-key` header. See [Introduction](/api-reference/introduction) for base URL, rate limits, and workflows. # Rule Packs Source: https://docs.zerodrift.com/api-reference/rulepacks GET /api/rulepacks/ Query the normalized policy catalog of managed and custom rule packs Managed packs cover regulations. Import Policy and Training Studio make any rulebook enforceable, in any industry. Returns the normalized policy catalog for the active authenticated customer. Managed entries reflect the Anchor v3 runtime vocabulary. Custom entries are policy imports that have been activated at least once, including entries that are currently inactive. Supplying `id` selects detail and requires `type`. Copy `data[].id` into `validation_scope.rulepacks` on [Enforce Content](/api-reference/validate/validate-content). For custom rule IDs, use [List Rules](/api-reference/custom-policies/list-rules) instead. ## Query Parameters Rulepack ID for detail lookup; requires `type` `managed` or `custom`. Filter list results or disambiguate a detail ID One-based result page Results per page (1–100) Case-insensitive match against ID, name, or description Filter by current runtime active state: `'true'` or `'false'` ## Response - List When called without `id`, returns paginated summaries under `data`. Rulepack summaries Rulepack identifier (e.g., `zd-us-securities`) Human-readable rulepack name Rulepack description (nullable) `managed` or `custom` Supported runtime engine vocabulary: `anchor_3_0` Whether the rulepack is currently active for this customer Total rules in the rulepack Currently active rules Currently inactive rules Publisher (nullable) Rulepack version (nullable) Compliance domain (nullable) Applicable jurisdictions Last update date (nullable) Pagination metadata (nullable) Current one-based page Results per page Total matching rulepacks Last available page ```json 200 OK - List theme={null} { "data": [ { "id": "zd-us-securities", "name": "US Securities and Investment Communications", "description": "SEC and FINRA communication compliance", "type": "managed", "engine": "anchor_3_0", "active": true, "total_rules": 72, "active_rules": 72, "inactive_rules": 0, "publisher": "ZeroDrift", "version": "3.0.0", "domain": "securities", "jurisdictions": ["US"], "last_updated": "2026-07-28" } ], "meta": { "current_page": 1, "per_page": 15, "total": 13, "last_page": 1 } } ``` ## Response - Detail When called with `id` and `type`, returns one detailed rulepack under `data`, including its `rules`. All summary fields, plus: Rules in the rulepack Rule identifier Human-readable rule name Rule description (nullable) `AI_SIGNAL`, `REGEX`, `COMPOSITE`, `LEXICON`, `REQUIRE_NEAR`, or `CUSTOM` Rule severity (nullable) `do_not_send` or `send_with_caution` Whether the rule is currently active Lifecycle detail (nullable): `status`, `effective_from`, `last_reviewed` Regulatory citations with `framework`, `cite`, and `jurisdictions` Remediation guidance with `kind` and `note` ```json 200 OK - Detail theme={null} { "data": { "id": "zd-us-securities", "name": "US Securities and Investment Communications", "type": "managed", "engine": "anchor_3_0", "active": true, "total_rules": 72, "active_rules": 72, "inactive_rules": 0, "rules": [ { "id": "guarantee_of_returns", "name": "Promissory Tone", "description": "Promissory words are risky", "type": "AI_SIGNAL", "severity": "major", "verdict": "do_not_send", "active": true, "lifecycle": { "status": "in_force", "last_reviewed": "2026-07-28" }, "citations": [ { "framework": "FINRA Rule 2210", "jurisdictions": ["US"] } ], "remediation": { "kind": "rewrite", "note": "Review with compliance" } } ] } } ``` ## Errors | Status | Meaning | | - | - | | `400` | Invalid pagination, filter, or detail selector | | `403` | Forbidden — missing, invalid, inactive, or unresolved API key | | `404` | Rulepack not found or not accessible to this customer | | `500` | Catalog backend read failed or stored catalog data is inconsistent | ## Example Same endpoint in two modes: a paginated list (`per_page`, `active`) and one pack's detail (`id` plus required `type`). ```bash cURL - List theme={null} curl -X GET "https://api.zerodrift.ai/api/rulepacks/?per_page=15&active=true" \ -H "x-api-key: YOUR_API_KEY" ``` ```bash cURL - Detail theme={null} curl -X GET "https://api.zerodrift.ai/api/rulepacks/?id=zd-us-securities&type=managed" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import requests API_KEY = "YOUR_API_KEY" API_BASE = "https://api.zerodrift.ai" headers = {"x-api-key": API_KEY} # List rulepacks response = requests.get(f"{API_BASE}/api/rulepacks/", headers=headers) catalog = response.json() print(catalog["data"]) # Get one rulepack's detail (id requires type) response = requests.get( f"{API_BASE}/api/rulepacks/", headers=headers, params={"id": "zd-us-securities", "type": "managed"} ) rulepack = response.json() print(rulepack["data"]["rules"]) ``` ```javascript JavaScript theme={null} const axios = require('axios'); const API_KEY = 'YOUR_API_KEY'; const API_BASE = 'https://api.zerodrift.ai'; const headers = { 'x-api-key': API_KEY }; // List rulepacks const listResponse = await axios.get(`${API_BASE}/api/rulepacks/`, { headers }); console.log(listResponse.data.data); // Get one rulepack's detail (id requires type) const detailResponse = await axios.get(`${API_BASE}/api/rulepacks/`, { headers, params: { id: 'zd-us-securities', type: 'managed' } }); console.log(detailResponse.data.data.rules); ``` # Get Training Status Source: https://docs.zerodrift.com/api-reference/training-studio/get-training-status GET /api/policies/import/{import_id}/train Poll a policy import until its Training Studio adapter is ready Return the current training run for a policy import. Poll after [Train Policy Adapter](/api-reference/training-studio/train-policy) until `training_status` or `status` is `succeeded` or `failed`. Live GET responses currently return `training_status`. The successful poll is part of activation: when Training Studio reports `succeeded`, ZeroDrift records the run as the import's active adapter. A failed retraining attempt does not replace an earlier working adapter. ## Path Parameters The policy import whose training run you want to inspect. ## Response Policy import associated with the run. Training Studio run identifier. Current run status on live GET responses. `succeeded` and `failed` are terminal. Alias for `training_status` on some responses. `succeeded` and `failed` are terminal. Current training pipeline stage, when reported. Training progress details, when reported. Failure detail when `status` is `failed`. ```json 200 Training Succeeded theme={null} { "import_id": "550e8400-e29b-41d4-a716-446655440000", "training_run_id": "run-808961af", "status": "succeeded", "stage": "complete", "progress": { "completed": 4, "total": 4 }, "error": null } ``` ## Error Responses | Status | Description | | - | - | | 403 | Invalid API key, or the import belongs to another customer | | 404 | Import not found, or no training run has been started | | 500 | Import state could not be read or updated | | 502 | Training Studio status request failed | | 503 | Training is not configured in this environment | ## Complete flow Training extends the custom-policy flow: 1. [Import Policy](/api-reference/custom-policies/import-policy). 2. Poll [Get Import Details](/api-reference/custom-policies/get-import-details) until extraction reaches `pending_review`. 3. Review or simulate the extracted rules, then [Activate Rules](/api-reference/custom-policies/activate-rules). 4. [Train Policy Adapter](/api-reference/training-studio/train-policy). 5. Poll this endpoint until `status` is `succeeded` or `failed`. Stop on either terminal status; do not keep polling a failed run. 6. If the run `succeeded`, enforce content with [`validation_scope.imports`](/api-reference/validate/validate-content) containing the import ID. Do not submit content for enforcement to a still-training run. ZeroDrift only promotes `training_run_id` after a successful status poll; until then, scoped enforcement follows its configured fallback path. ## Example This sample starts training and polls to a terminal state. ```python Python theme={null} import time import requests API_BASE = "https://api.zerodrift.ai" API_KEY = "YOUR_API_KEY" IMPORT_ID = "550e8400-e29b-41d4-a716-446655440000" URL = f"{API_BASE}/api/policies/import/{IMPORT_ID}/train" HEADERS = {"x-api-key": API_KEY} started = requests.post(URL, headers=HEADERS, json={}) started.raise_for_status() print(started.json()) while True: response = requests.get(URL, headers=HEADERS) response.raise_for_status() training = response.json() print(training["status"]) if training["status"] in ("succeeded", "failed"): break time.sleep(10) if training["status"] == "failed": raise SystemExit(training.get("error", "Training failed")) ``` # Train Policy Adapter Source: https://docs.zerodrift.com/api-reference/training-studio/train-policy POST /api/policies/import/{import_id}/train Start a Training Studio adapter run from an activated policy import Start asynchronous training for an activated policy import. Training Studio re-reads the import's original document, generates and judges examples, and trains a policy-specific LoRA adapter. Call this after [Import Policy](/api-reference/custom-policies/import-policy) and [Activate Rules](/api-reference/custom-policies/activate-rules). Import extracts candidate rules; training is only accepted once those rules are activated (`409` if the import is still pending review). Then poll [Get Training Status](/api-reference/training-studio/get-training-status) until training succeeds or fails. Training uses the original imported document, not only the extracted rule definitions. If that document has expired under your retention policy, re-import and activate the policy before training. ## Path Parameters The activated policy import to train. ## Request Body The body is optional. Omit it to use Training Studio defaults. Optional maximum generation spend in US dollars for this run. Optional number of generated training examples per extracted rule. ## Response Returns `202 Accepted` after the training run has been created. Policy import being trained. Training Studio run identifier. Initial status reported for the training run. Human-readable next step, when present. Endpoint used to monitor this run. Always `GET`. `/api/policies/import/{import_id}/train` ```json 202 Training Accepted theme={null} { "import_id": "550e8400-e29b-41d4-a716-446655440000", "training_run_id": "run-808961af", "status": "queued", "message": "Training started. Poll the training status endpoint for progress.", "poll": { "method": "GET", "url": "/api/policies/import/550e8400-e29b-41d4-a716-446655440000/train" } } ``` ## Error Responses | Status | Description | | - | - | | 400 | Invalid training options | | 403 | A full-access API key is required, or the import belongs to another customer | | 404 | Import not found | | 409 | Import is not activated, or another training run is already in progress | | 410 | Original document expired; re-import the policy | | 422 | Document is too short to train, or Training Studio rejected it | | 500 | Import state could not be read or updated | | 502 | Training Studio request failed | | 503 | Training is not configured in this environment | ## Example ```bash cURL theme={null} curl -X POST \ "https://api.zerodrift.ai/api/policies/import/550e8400-e29b-41d4-a716-446655440000/train" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` # Get Verdict Source: https://docs.zerodrift.com/api-reference/validate/get-results GET /api/v3/jobs/{job_id} Retrieve the verdict for an enforcement job. Retrieve the verdict for an enforcement job started via [Enforce Content](/api-reference/validate/validate-content). Poll this endpoint until `status` is `completed` or `failed`. Sync responses and polled async responses use the same envelope. Completed jobs nest the verdict under `result`; the async envelope reports `mode: "async"`. Public responses report billed `result.units` and `result.cost`, not raw input/output token counts. ## Path Parameters The `job_id` returned by [Enforce Content](/api-reference/validate/validate-content). ## Response Always `v3`. Unique identifier for the enforcement job. Job status: `queued`, `processing`, `completed`, or `failed`. `async` for a polled job. Time when the job was submitted. Requested engine after explicit custom-rule routing: `anchor_3_0_mini` (Mini), `anchor_3_0` (Flagship), or `anchor_3_0_max` (Max). A scope-less Flagship request stays on Flagship. See [Choose a model](/models). Present when a Flagship request explicitly named custom rule text or a `cust_*` rule and was upgraded to Max. Engine the job fell back to when Max could not complete the request. Why fallback ran (for example `client_error`). Present with `fallback_engine`. The enforced scope as `source` plus rule, rulepack, or import counts. Full rule ID lists are not echoed in HTTP responses. Error details (present only when `status` is `failed`). Verdict payload on completed jobs. Fields below are nested inside this object. Overall compliance assessment: `approved`, `needs_changes`, or `do not send`; trained adapters can also return `send with caution`. `do not send` contains spaces. This is the verdict: `approved` = Pass, `needs_changes` = Fix, and `do not send` = Block. Violation counts by severity. Count of blocking severity violations Count of caution severity violations Number of pages in the document Number of lines with at least one compliance violation. Absent for older jobs or when only grouped fixes exist (per-line positions unavailable). Number of lines with no violations. Absent for older jobs or when only grouped fixes exist. Total number of lines in the document. Absent for older jobs or when only grouped fixes exist. True when more than one fix is found, suggesting a re-scan after applying fixes. Rule IDs the Anchor 3.0 SLM identified as relevant to the content (the per-request evaluated rule set). Billed validation units: zero when no input tokens were consumed, otherwise the ceiling of input tokens divided by 2,000. Billed list price with `currency: "USD"` and decimal-string `amount`. One entry per violating line, with all rules violated and the best fix. Absent for older jobs or when only grouped fixes exist. Present but empty for fully compliant documents. 1-indexed line number in the document The text content of the violating line (trimmed) Character offset of the best fix's original text start Character offset of the best fix's original text end Page number where the violation appears Number of distinct rules violated on this line All rules violated on this line, sorted by severity then confidence. Each entry carries `rule_id` (canonical rule identifier — rule-pack `id`, or `cust_{customer_id}_{id}` for activated custom rules), `rule_name`, `rule_ref` (regulatory citation, e.g. `FINRA 2210(d)(1)(B)`), `severity` (`do_not_send` or `send_with_caution`), `confidence` (0–1), and `action` (`replace`, `insert_after`, `remove`, or `warning`). The recommended fix for this line (from the highest-severity, highest-confidence rule): `severity`, `rule_id`, `rule_name`, `rule_ref`, `action`, `suggested_text` (null when `action` is `remove`/`warning`), `human_action`, and `confidence`. Max does not rewrite: on a Max job `suggested_text` carries the rule's own fix note when it has one and is otherwise null or empty. See [Choose a model](/models). Individual fix entries (rule-centric, one per violation per quote). Each entry carries `rule_id` — the canonical rule identifier (rule-pack `id`, or `cust_{customer_id}_{id}` for activated custom rules). Grouped fixes for rules that aggregate across pages. Each value additionally carries `rule_id` (the canonical rule identifier — also the key under which the group is stored in this object). Mini's whole-message per-rule verdicts. Each entry includes `rule_id`, `signal_name`, `severity`, `confidence`, and probability `p`. Mini returns this instead of per-line `violations`. Mini's independent whole-message violation head. It can be true even when `flagged_rules` is empty. Probability behind `any_violation_detected` (Mini only). Document metadata submitted with the request. ## Status Values | Status | Description | | - | - | | `queued` | Async job accepted | | `processing` | Enforcement in progress | | `completed` | Success; verdict is under `result` | | `failed` | Enforcement failed with error | ## Error Responses | Status | Description | | - | - | | 403 | Forbidden — missing, invalid, inactive, or unresolved API key | | 404 | Job Not Found | ```json Processing theme={null} { "api_version": "v3", "job_id": "a1b2c3", "status": "processing", "mode": "async", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0", "progress": { "step": "score" } } ``` ```json Completed theme={null} { "api_version": "v3", "job_id": "a1b2c3", "status": "completed", "mode": "async", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0", "applied_validation_scope": { "source": "request", "rules_count": 235 }, "result": { "overall_status": "needs_changes", "summary": { "do_not_send": 0, "send_with_caution": 1, "document_pages": 1 }, "revalidation_recommended": false, "relevant_rule_ids": ["guarantee_of_returns"], "units": 1, "cost": { "currency": "USD", "amount": "0.01" }, "violations": [ { "rule_id": "guarantee_of_returns", "signal_name": "Guarantees of returns", "rule_type": "SLM_SIGNAL", "severity": "send_with_caution", "confidence": "high", "quote": "Our fund guarantees 18% returns with no downside risk.", "fix": { "action": "replace", "suggested_text": "Returns are not guaranteed and involve risk." } } ] }, "extracted_text": null } ``` ```json Completed (Mini) theme={null} { "api_version": "v3", "job_id": "mini123", "status": "completed", "mode": "async", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0_mini", "result": { "overall_status": "needs_changes", "summary": { "do_not_send": 0, "send_with_caution": 1, "document_pages": 1 }, "revalidation_recommended": false, "flagged_rules": [ { "rule_id": "guarantee_of_returns", "signal_name": "Guarantees of returns", "rule_type": "SLM_SIGNAL", "rule_pack_id": "zd-us-securities", "rule_pack_name": "US Securities", "severity": "send_with_caution", "confidence": "high", "p": 0.94 } ], "any_violation_detected": true, "any_violation_p": 0.97, "units": 1, "cost": { "currency": "USD", "amount": "0.002" } }, "extracted_text": null } ``` ```json Failed theme={null} { "api_version": "v3", "job_id": "a1b2c3", "status": "failed", "mode": "async", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0", "error": "Validation engine unavailable" } ``` ## Example Poll until the job is `completed` or `failed`. The Python and JavaScript samples wait 2 seconds between requests; the cURL sample is a single GET you repeat yourself. ```bash cURL theme={null} curl "https://api.zerodrift.ai/api/v3/jobs/a1b2c3" \ -H "x-api-key: $ZERODRIFT_API_KEY" ``` ```python Python theme={null} import os import requests import time API_BASE = "https://api.zerodrift.ai" def get_results(job_id): while True: response = requests.get( f"{API_BASE}/api/v3/jobs/{job_id}", headers={"x-api-key": os.environ["ZERODRIFT_API_KEY"]} ) result = response.json() if result["status"] in ("completed", "failed"): return result time.sleep(2) results = get_results("a1b2c3") print(results) ``` ```javascript JavaScript theme={null} const axios = require('axios'); const API_BASE = 'https://api.zerodrift.ai'; async function getResults(jobId) { while (true) { const response = await axios.get( `${API_BASE}/api/v3/jobs/${jobId}`, { headers: { 'x-api-key': process.env.ZERODRIFT_API_KEY } } ); if (['completed', 'failed'].includes(response.data.status)) { return response.data; } await new Promise(resolve => setTimeout(resolve, 2000)); } } const results = await getResults('a1b2c3'); console.log(results); ``` # Enforce Content Source: https://docs.zerodrift.com/api-reference/validate/validate-content POST /api/v3/content/validate Send content to Anchor, the compliance enforcement model, and get a verdict. Async or sync. Send content to Anchor, the compliance enforcement model, and get a verdict. Requests require `mode` and `model_engine`. With `mode: "async"`, the endpoint returns a `job_id` you poll via [Get Verdict](/api-reference/validate/get-results). Set `mode: "sync"` to run enforcement inline and receive the completed result in a single response. Pick Mini, Flagship, or Max with `model_engine`. Flagship is `anchor_3_0` (the default; older notes called this Base). See [Choose a model](/models) for when to use each tier, and the [Anchor 3.0 benchmark](https://zerodrift.com/model/anchor-3-benchmark) for quality and latency. Custom rules run on **Max** (`anchor_3_0_max`). A scope-less Flagship (`anchor_3_0`) request stays on Flagship and checks the active managed catalog; activated custom rules are not evaluated. If a Flagship request explicitly names a `cust_*` rule, the API upgrades that request to Max. See [Custom rules](#custom-rules). ## Request Body Plain text to enforce. Enforcement model. `anchor_3_0_mini` (Mini): detection-only yes/no plus a whole-message `flagged_rules` list — no per-line violations, rewrite, or `validation_scope`. `anchor_3_0` (Flagship, default): per-line violations plus rewrite; managed packs and trained adapters. `anchor_3_0_max` (Max): per-line violations, no rewrite; managed and custom rule text at inference. Ask for Max explicitly to enforce activated custom rules. See [Choose a model](/models). Execution mode: `async` returns `202` with a `job_id` to poll; `sync` runs inline for short content and returns the completed result with `200`, falling back to async if it would exceed the inline budget. Optional metadata merged into the job. The API currently forces `metadata.document_type` to `email`, even when a different value is submitted. Do not use that metadata field to distinguish chat, agent, or document traffic. Optional nested form to narrow live enforcement to a subset of already-active rules, rule packs, and imports. Resolve IDs from the Policy API — see [Rule and rulepack IDs](#rule-and-rulepack-ids). A scope-less Flagship call defaults to the active managed catalog. Explicitly naming a custom rule upgrades Flagship to Max. Rule IDs to evaluate. Use `rule_id` from [List Rules](/api-reference/custom-policies/list-rules). Custom IDs are prefixed `cust_`. Rule pack IDs to evaluate. Use `id` from [Rule Packs](/api-reference/rulepacks). Import IDs to evaluate. Use `import_id` from [Import a Policy](/guides/import-a-policy). Run every active managed catalog rule, and nothing else. Do not combine with `rules`, `rulepacks`, or `imports`. This is the best-effort default when a Flagship request omits `validation_scope`. ## Response Returns `202 Accepted` for async submissions. Poll [Get Verdict](/api-reference/validate/get-results) for the verdict. Always `v3`. Unique identifier for the enforcement job. `queued` for async submissions. For `sync` responses the completed result envelope is returned inline with `status: completed` and the verdict nested under `result`. Mode the request was actually processed in: `async` or `sync`. A `sync` request that fell back returns `async`. Time when the job was submitted. Engine that served the request: `anchor_3_0_mini` (Mini), `anchor_3_0` (Flagship), or `anchor_3_0_max` (Max). This can differ from the request when the API upgrades a Flagship call that named custom rules. See [Choose a model](/models). Present when a Flagship request explicitly named custom rule text or a `cust_*` rule and was upgraded to Max. A scope-less Flagship call does not upgrade merely because the workspace has activated custom rules. Counts for the scope the job ran against: `source` plus `rules_count`, `rulepacks_count`, or `imports_count`. The API does not echo the full rule ID list. A scope-less Flagship call reports `source: "request"` because the server supplies its managed-only default. Where to poll for the result (async only). Always `GET`. Poll URL, e.g. `/api/v3/jobs/{job_id}`. For the `sync` response body (HTTP 200), read the verdict from `result`. The same nested envelope is returned by [Get Verdict](/api-reference/validate/get-results) for async jobs, with `"mode": "async"`. Public responses report billed `result.units` and `result.cost`, not raw token counts. ## Custom rules Untrained custom rules (created with [Create Custom Rule](/api-reference/custom-policies/create-rule) or activated from an import) run on **Max** when Max is requested. A scope-less Flagship request checks the managed catalog only, even when custom rules are active. Flagship can check a custom policy through a **trained** adapter. | You send | What happens | | - | - | | `model_engine: "anchor_3_0"` with no `validation_scope` | Flagship stays on Flagship and checks active managed rules only. Activated custom rules are not evaluated. | | `model_engine: "anchor_3_0_max"` with no `validation_scope` | Max checks the workspace's activated managed and custom rules. | | `model_engine: "anchor_3_0_max"` and custom rules in `validation_scope` | Max runs that explicit scope. `mode: "sync"` completes inline when it fits the inline budget; otherwise the response falls back to async. | | `model_engine: "anchor_3_0"` and a `cust_*` ID in `validation_scope.rules` | The explicit custom-rule request upgrades to Max and reports `engine_upgrade_reason: "custom_rules"`. | | `model_engine: "anchor_3_0"` after [Train an Adapter](/guides/train-an-adapter) | Flagship can check that import. Scope with `validation_scope.imports`. | Max detects, it does not rewrite. A Max verdict carries a rule's own fix note when that rule has one, so `violations[].fix.suggested_text` is empty for most rules and custom rules never return a rewritten line. Treat an empty `suggested_text` as Escalate — see [Act on Each Verdict](/guides/act-on-verdicts). Flagship is the tier that returns a rewrite; to get one for an uploaded policy, train an [adapter](/guides/train-an-adapter) and scope the request to its import. Max billing for custom rules is in [Pricing](/pricing). ## Rule and rulepack IDs The Policy API returns the IDs `validation_scope` expects. Copy them from these endpoints rather than from the Command network panel. | You need | Call | Field to copy | | - | - | - | | Custom or managed rule ID | `GET /api/policies/rules` ([List Rules](/api-reference/custom-policies/list-rules)) | `custom_rules[].rule_id` or `default_rules[].rule_id` | | One custom rule after create | `POST /api/policies/rules` ([Create Custom Rule](/api-reference/custom-policies/create-rule)) | `rule_id` | | Custom rule IDs from an import | `POST /api/policies/import/{import_id}/activate` ([Activate Rules](/api-reference/custom-policies/activate-rules)) | `activated_rule_ids` | | Rulepack ID | `GET /api/rulepacks/` ([Rule Packs](/api-reference/rulepacks)) | `data[].id` | | Import ID | Policy import flow ([Import a Policy](/guides/import-a-policy)) | `import_id` | Custom rule IDs look like `cust_{customer_id}_{slug}` — for example `cust_abc123_required_terms`. Rulepack IDs look like `zd-us-securities`. ## Example Enforce one custom rule on Max. This example checks whether a financial advisor improperly disclaims fiduciary responsibility. Set `CUSTOM_RULE_ID` from List Rules; custom rule IDs are workspace-specific. ```bash cURL theme={null} : "${CUSTOM_RULE_ID:?set CUSTOM_RULE_ID from GET /api/policies/rules}" 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\": \"A. As your financial advisor, I am no longer in charge of the requirements for your investment account if you lose money that is completely on you.\", \"model_engine\": \"anchor_3_0_max\", \"mode\": \"sync\", \"validation_scope\": { \"rules\": [\"$CUSTOM_RULE_ID\"] } }" ``` ```python Python theme={null} import os import requests API = "https://api.zerodrift.ai" HEADERS = { "x-api-key": os.environ["ZERODRIFT_API_KEY"], "Content-Type": "application/json", } custom_rule_id = os.environ["CUSTOM_RULE_ID"] r = requests.post( f"{API}/api/v3/content/validate", headers=HEADERS, json={ "content": ( "A. As your financial advisor, I am no longer in charge of the " "requirements for your investment account if you lose money that " "is completely on you." ), "model_engine": "anchor_3_0_max", "mode": "sync", "validation_scope": {"rules": [custom_rule_id]}, }, timeout=30, ) r.raise_for_status() print(r.json()) ``` ```javascript JavaScript theme={null} const API = "https://api.zerodrift.ai"; const customRuleId = process.env.CUSTOM_RULE_ID; 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: "A. As your financial advisor, I am no longer in charge of the requirements for your investment account if you lose money that is completely on you.", model_engine: "anchor_3_0_max", mode: "sync", validation_scope: { rules: [customRuleId] }, }), }); if (!r.ok) throw new Error(`ZeroDrift ${r.status}`); console.log(await r.json()); ``` A Flagship request that names the same custom rule is upgraded to Max and can be accepted as async. Poll until `status` is `completed` or `failed`: ```json theme={null} { "api_version": "v3", "job_id": "a1b2c3", "status": "queued", "mode": "async", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0_max", "poll": { "method": "GET", "url": "/api/v3/jobs/a1b2c3" }, "engine_upgrade_reason": "custom_rules" } ``` ```bash theme={null} curl "https://api.zerodrift.ai/api/v3/jobs/a1b2c3" \ -H "x-api-key: $ZERODRIFT_API_KEY" ``` ## Error Responses | Status | Description | | - | - | | 400 | Bad Request — invalid `model_engine` or unsupported parameters | | 403 | Forbidden — missing, invalid, inactive, or unresolved API key | | 422 | Unprocessable Entity | ```json 202 Accepted (async) theme={null} { "api_version": "v3", "job_id": "a1b2c3", "status": "queued", "mode": "async", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0", "poll": { "method": "GET", "url": "/api/v3/jobs/a1b2c3" } } ``` ```json 202 Accepted (custom-rule upgrade) theme={null} { "api_version": "v3", "job_id": "a1b2c3", "status": "queued", "mode": "async", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0_max", "poll": { "method": "GET", "url": "/api/v3/jobs/a1b2c3" }, "engine_upgrade_reason": "custom_rules" } ``` ```json 200 Completed (sync) theme={null} { "api_version": "v3", "job_id": "a1b2c3", "status": "completed", "mode": "sync", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0", "applied_validation_scope": { "source": "request", "rules_count": 235 }, "result": { "overall_status": "approved", "summary": { "do_not_send": 0, "send_with_caution": 0, "document_pages": 1 }, "revalidation_recommended": false, "relevant_rule_ids": [], "units": 1, "cost": { "currency": "USD", "amount": "0.01" }, "violations": [] }, "extracted_text": null } ``` # Changelog Source: https://docs.zerodrift.com/changelog What changed in the ZeroDrift API and documentation. Newest first. Breaking changes are announced here before they ship. ## 2026-09-24: Max detects without rewriting * Custom rules now enforce on Max with `validation_scope.rules`; the [Enforce Content example](/api-reference/validate/validate-content#example) shows the confirmed request shape. * Max (`anchor_3_0_max`) returns per-line violations but no rewrite. Custom rules enforced on Max never return a rewritten line. The hosted OpenAPI `model_engine` description matches that contract. * On a Max job, `violations[].fix.suggested_text` carries the rule's own fix note when the rule has one and is otherwise empty. Escalate on an empty `suggested_text`, or run the same content through Flagship. * Rewrite remains a Flagship capability, including for an uploaded policy checked through a trained [adapter](/guides/train-an-adapter). [Pricing](/pricing) no longer lists rewrite as included on Max. ## 2026-09-23: Anchor routing and result payloads * A scope-less Flagship (`anchor_3_0`) request stays on Flagship and checks the active managed catalog only. Activated custom rules are not evaluated unless the caller asks for Max. * Max (`anchor_3_0_max`) enforces activated custom rules at inference. A Flagship request that explicitly names a `cust_*` rule still upgrades to Max and reports `engine_upgrade_reason: "custom_rules"`. * Mini (`anchor_3_0_mini`) runs unscoped against its frozen rulebook and returns whole-message `flagged_rules`; it cannot honor workspace pack toggles or `validation_scope`. * Sync and async completion payloads now share the same nested `result` shape. Public usage is returned as `result.units` and `result.cost`, and `applied_validation_scope` reports counts rather than the full rule ID list. * Rule and rulepack IDs for `validation_scope` come from [List Rules](/api-reference/custom-policies/list-rules) and [Rule Packs](/api-reference/rulepacks). See [Enforce Content](/api-reference/validate/validate-content). ## 2026-09-22: Anchor 3.0 Mini, Flagship, and Max * `model_engine` on `POST /api/v3/content/validate` accepts `anchor_3_0_mini`, `anchor_3_0` (Flagship, default), and `anchor_3_0_max`. * See [Choose a model](/models) for when to use each tier, [Pricing](/pricing) for per-validation rates, and the [Anchor 3.0 benchmark](https://zerodrift.com/model/anchor-3-benchmark) for quality and latency. ## 2026-09-18: Anchor 3.0 pricing Documented per-enforcement pricing for Anchor 3.0 Mini, Flagship, and Max, including rewrites, custom rules billed in groups of 10, and Flagship Training Studio. See [Pricing](/pricing). ## 2026-09-02: Guard for Agents * The Enforcement API became available in production for agent response paths. * MCP connectors can import policies from Notion, Linear, Confluence, and Google Drive. * Anchor 3.0 is available as `model_engine: "anchor_3_0"`. ## 2026-09-01: Training Studio API * Added `POST /api/policies/import/{import_id}/train`. * Added `GET /api/policies/import/{import_id}/train` for training status. Training Studio's broader product launch is separate from these endpoints. ## 2026-08-11: Command * Command launched with API keys, rulepack activation, policy import, and live activity. ## 2026-08-04: Activity API * Added `GET /api/activities`. * Added `GET /api/activities/{activity_id}`. * Activity queries support a 30-day window. # Concepts Source: https://docs.zerodrift.com/concepts The five words you need to read the rest of these docs. ## Verdict The decision ZeroDrift returns on a piece of content. There are four. | Verdict | Meaning | Where you see it today | | - | - | - | | **Pass** | Compliant. Deliver as is. | `overall_status: "approved"` | | **Fix** | A compliant version exists. Deliver the fix. | `overall_status: "needs_changes"` plus a non-empty `fix.suggested_text` | | **Block** | No compliant version exists. Do not deliver. | `overall_status: "do not send"` | | **Escalate** | A human must decide. | Your code, when `fix.action` is `remove_or_escalate`, `suggested_text` is empty, or your policy requires review | Every verdict is logged in Command and readable through the Activity endpoints for 30 days. ## Rulepack A managed set of rules covering one regulatory area, built and maintained by ZeroDrift. You activate a rulepack in Policy and scope a call to it with `validation_scope.rulepacks`. See the [Rulepack Catalog](/api-reference/rulepacks). ## Policy (imported) Your own rulebook, made enforceable. Upload a document or sync one from Notion, Linear, Confluence, or Google Drive. ZeroDrift extracts rules; you review and activate them; each import gets an `import_id` you can scope calls to. Untrained custom rules run on Max (`anchor_3_0_max`). See [Import Policy](/api-reference/custom-policies/import-policy) and [Enforce Content](/api-reference/validate/validate-content). ## Adapter A policy-specific model trained in Training Studio on top of Anchor, from an activated import. Anchor handles regulations. Your adapter handles your policies. At launch, adapters are Flagship-only (`model_engine: "anchor_3_0"`) and optional — activated rules run without one. See [Train an Adapter](/guides/train-an-adapter), [Choose a model](/models), and [Pricing](/pricing). ## Sync and async `mode` is required on every enforce request. `mode: "sync"` returns the verdict in the response when the job stays within the inline budget. On a completed sync call, read `overall_status` and `violations` from the nested `result` object. `mode: "async"` returns `202` with a `job_id` to poll. Best for long content and high volume. A sync call on long content falls back to async; the `mode` field in the response tells you which happened. Custom rules require `model_engine: "anchor_3_0_max"` for an inline sync verdict — see [Enforce Content](/api-reference/validate/validate-content). # MCP Connectors Source: https://docs.zerodrift.com/connecting-mcp Connect Notion, Linear, Confluence, or Google Drive through the Connections Service and turn a source document into an enforceable policy. ZeroDrift's Connections Service talks to a provider's MCP server as a read-only client. You sign in with your own Notion, Linear, Confluence, or Google Drive account, browse what that account can already see, pick a document, and ZeroDrift archives it and runs the same custom-policy import pipeline as a file upload. This page covers what the connection can access, how to set up each live connector, what happens after you pick a document, and how to diagnose a failed sync. Today, connections use the account of the person who clicks **Connect**. ZeroDrift does not write back to the provider, and it does not use a service account. ## What a connection can access A connection acts as the person who completed OAuth. It can read documents that person can already open in the provider. It does not combine content across accounts, and it only calls the read tools ZeroDrift registered for that provider. | Boundary | What it means | | - | - | | Acting user | The OAuth grant is for one human account. Switching Notion, Linear, Atlassian, or Google accounts requires disconnecting and connecting again. | | Read-only | ZeroDrift only calls read tools. For Confluence, the Atlassian grant is product-level and can include write scopes on the token; the Connections Service still refuses every write tool. | | One provider per connection | Notion, Linear, Confluence, and Google Drive are separate sources. A Linear issue is not visible through a Notion connection. | | No search language | Confluence CQL and similar query languages are not exposed. You browse the tree; you do not send an operator string. | | No scheduled re-sync | Folder watch and daily re-sync are not available, even though the Sources UI still advertises daily sync. Re-sync a document when you want to refresh the policy. | | Connector | Status | What you can browse | | - | - | - | | Notion | Live | Pages and databases the connected Notion account can read | | Linear | Live | Documents and issues the connected Linear account can read | | Confluence (Atlassian) | Live | Spaces and pages on sites included in the Atlassian grant | | Google Drive | Live | Folders and files under My Drive, Shared with me, and Recent | ## Set-up guide ### Prerequisites * A ZeroDrift Command workspace with access to **Settings → Sources** and **Policy Studio** * An account in Notion, Linear, Confluence, or Google Drive that can already open the documents you want to import * For Confluence: an Atlassian org that allows the classic Rovo MCP consent (see [Confluence](#confluence-atlassian) if consent is blocked) ### How to connect The flow is the same for every live connector: connect, browse, pick, sync. 1. In Command, open **Settings → Sources**. 2. Choose **Notion**, **Linear**, **Confluence**, or **Google Drive**, then **Connect**. 3. Complete the provider's OAuth screen in the new tab. Grant only the workspace, site, or pages you want ZeroDrift to read. 4. When the tab returns you to Command, the source is connected. Open it to browse the document tree. 5. Select one or more documents and sync. Each file is archived, then starts a custom policy import. 6. Open **Policy Studio**. The document appears as a custom policy import (`importing` while extraction runs, then pending review, failed with a reason, or ready to enforce). You can also start from Policy Studio: **Add Policy** → import from a connected source. ### Notion 1. On **Settings → Sources**, select **Notion** → **Connect**. Settings → Sources page listing Notion, Linear, Confluence, and Google Drive with Connect buttons Connect Notion panel listing read-only access, you pick the documents, and a kept-in-sync-daily note before authorizing The Connect panel and the Sources cards say picked documents are kept in sync daily. Scheduled re-sync is not live yet — re-sync a document yourself when you want to refresh the policy. 2. Sign in to Notion and approve access for the pages or workspace ZeroDrift should read. Notion OAuth screen granting mcp-dev.zerodrift.ai access to the ZeroDrift workspace Settings → Sources page showing Notion as Connected 3. Back in Command, open the Notion source. Browse pages and databases the connected account can see. 4. Pick the policy document and sync. Add policies from Notion panel browsing Teamspaces, Shared, and Private folders to pick documents If browse is empty, confirm you approved the Notion workspace that holds the policy, not a different workspace on the same email. ### Linear 1. On **Settings → Sources**, select **Linear** → **Connect**. Settings → Sources page with Linear not yet connected Connect Linear panel describing read-only access, document picking, and daily sync before authorizing 2. Sign in to Linear and approve access. Linear MCP authorization screen showing ZeroDrift requesting read access, with Approve and Cancel buttons 3. Browse documents and issues the connected Linear account can read. Settings → Sources page showing Linear as Connected Add policies from Linear panel with Documents, Issues, and Teams filters to pick items 4. Pick the item that holds the policy text and sync. If a document you can open in Linear does not appear, disconnect and connect again, then browse from the workspace root. Do not paste an issue id into the tree. ### Confluence (Atlassian) Confluence uses Atlassian's classic MCP facade (`/v1/mcp`), not the granular `/v1/mcp/authv2` path. Authv2 cannot finish consent against a real Atlassian org while **Permissions** on the Rovo MCP server are blocked. 1. On **Settings → Sources**, select **Confluence** → **Connect**. Settings → Sources page with Confluence not yet connected Connect Confluence panel describing read-only access, document picking, and daily sync before authorizing 2. Complete Atlassian consent. The screen is product checkboxes, not a narrowed scope list. Atlassian Rovo MCP authorization screen showing ZeroDrift requesting access to Jira, Confluence, and Compass 3. Browse spaces, then pages. Expand one level at a time; opening a space homepage does not list every page in the space. Settings → Sources page showing Confluence as Connected 4. Pick pages and sync. If consent says **Permissions are blocked for this site**, an Atlassian admin needs to unblock the Rovo MCP server: 1. Open Atlassian Administration. 2. Go to **Rovo** → **Rovo MCP server** → **Permissions**. 3. Allow the site, then retry **Connect** in Command. A connection created before Confluence browse shipped can return `409` when you try to browse. Disconnect and connect again so the grant includes the read tools. ### Google Drive 1. On **Settings → Sources**, select **Google Drive** → **Connect**. Settings → Sources page with Google Drive not yet connected Connect Google Drive panel describing read-only access, document picking, and daily sync before authorizing 2. Sign in with the Google account that can open the files you want to import, and approve access. 3. Browse from **All locations**. The first level is **My Drive**, **Shared with me**, and **Recent** — places to look, not a flat file list. Settings → Sources page showing Google Drive as Connected Add policies from Google Drive panel with All locations, My Drive, Shared with me, and Recent 4. Open a folder, pick files, and sync. Trashed files can still appear in listings; skip them. Drive file search from Command is browse-only — do not paste a Drive query string into the tree. ## After you pick a document A successful sync does not stop at archive. ZeroDrift: 1. Stores the document with provenance on the record (author, last edited time, source URL when the provider supplies one, and connection id). 2. Sends the bytes through the same extraction path as [Import Policy](/api-reference/custom-policies/import-policy). 3. Records the `import_id`. Status is `processing` until extraction finishes. 4. Surfaces the row in Policy Studio as a custom policy import. Re-syncing a document that already produced a policy **refreshes that policy** instead of creating a second one. The previous import's rules are retired before the replacement is activated, so you do not enforce both versions. Unchanged content is skipped on re-sync **only if** an `import_id` already exists. A document archived before imports existed still produces one on the next sync. If the import pipeline refuses the file, the sync fails with a reason you can read and the archived object is kept. The import is not shown as reviewable. From there the API path is unchanged: poll [Get Import Details](/api-reference/custom-policies/get-import-details), then [Activate Rules](/api-reference/custom-policies/activate-rules). To train a policy-specific adapter on that import, follow [Train an Adapter](/guides/train-an-adapter). ## Capabilities | Capability | What it does | Example | | - | - | - | | Connect | User-delegated OAuth; credentials are stored per workspace | Connect Notion from **Settings → Sources** | | Browse | Read-only tree of what the connected account can see | Open Confluence, expand a space, list child pages | | Sync | Fetch selected documents, archive them, start policy import | Pick a Notion page named "Communications policy" | | Refresh | Re-sync updates the same custom policy; superseded rules are retired first | Sync the Linear doc again after the policy text changes | | Revoke | Disconnect is final; ZeroDrift cannot call the provider until you connect again | Disconnect Linear on the Sources page | ZeroDrift does not expose these as public MCP tools for Claude or ChatGPT. The Connections Service is the MCP client. The public API you call after sync is the Custom Policies API. ## Troubleshooting ### The error names the provider, not "unreachable" When a provider rejects a call, Command shows that rejection. A 401 or 403 from Notion, Linear, Atlassian, or Google Drive means the grant is dead or forbidden. Reconnect the source. ZeroDrift does not keep retrying those failures. A transport timeout or DNS failure is different: you can retry that. If the UI still says the service is unreachable after a successful connect, check network access to Command, not the provider's status page first. ### Import failed, and the reason is on the row Failed imports show why they failed and are not presented as ready for review. Fix the document or the connection, then sync again. A generic "unreachable" message is no longer the expected failure text. ### Confluence consent never finishes If **Connect** fails with **Permissions are blocked for this site**, the block is in Atlassian Administration → **Rovo** → **Rovo MCP server** → **Permissions**, not in ZeroDrift. ### Browse returns 409 after connecting Confluence Disconnect and connect again. A grant created before Confluence browse shipped can omit the read tools. Browse stays refused until you reconnect. ### Re-sync created overlapping rules It should not. A refresh retires the previous import's rules before activating the new extraction. If you activated an older import through the API while a refresh was in flight, use [Deactivate Rules](/api-reference/custom-policies/deactivate-rules) on the superseded `import_id`. ### The Sources page cannot browse a connected source The UI now says what to do when a source will not browse, instead of failing with no next step. Follow that message. Typical causes: expired OAuth, Confluence permissions, or a connection that needs a reconnect. ## FAQ No. This feature connects ZeroDrift **to** Notion, Linear, Confluence, and Google Drive MCP servers so Command can import policy documents. It is not a way to query ZeroDrift from another AI assistant. No. Connecting and browsing happens in Command with OAuth. Use an API key after the document is a policy import: poll extraction, activate rules, or validate with that import. No. Connections are read-only. Even when Atlassian issues a token that includes write scopes, ZeroDrift only permits the read tools required to list and fetch pages. The same is true for Google Drive: ZeroDrift can fetch file bytes, not create or edit files. Re-sync skips extraction when the document has not changed **and** it already has an `import_id`. If there is no import yet, ZeroDrift still creates one. Not today. Every connection is the person who clicked **Connect**. # Data Retention Source: https://docs.zerodrift.com/data-retention How long ZeroDrift retains customer content, enforcement verdicts, and activity records. # Data Retention > How ZeroDrift retains and deletes customer data processed through the platform. This page is the customer-facing data retention policy for the ZeroDrift platform. It supplements the [Privacy Statement](https://zerodrift.com/legal/privacy-statement) and [Data Processing Practices](https://zerodrift.com/legal/data-processing-practices). Contractual terms in your Customer Agreement or DPA control if they differ. ## Principles * **Default to deletion.** Customer content is kept only as long as needed to deliver the service, then deleted. * **Contracts can extend retention.** Enterprise agreements may require longer retention for exam-ready records. * **No model training.** Customer data is not used to train, tune, or improve ZeroDrift or third-party AI models. * **Deletion is automated.** Retention is enforced with scheduled deletion jobs and object-store lifecycle rules. ## What we store | Category | Examples | | - | - | | Customer content | Message bodies, content, and files submitted for enforcement or policy import | | Transient uploads | Objects written to presigned S3 URLs before enforcement starts | | Enforcement jobs & verdicts | Verdicts, rules cited, and fixes returned by the API | | Activity / audit records | Compact activity events (verdict, rules cited, timeline) | | Workspace configuration | Users, API keys, rule packs, custom rules, and integration settings | ZeroDrift acts as a **processor** for customer-submitted communications content and processes it only on your documented instructions. ## Default retention schedule Unless your Customer Agreement or DPA states otherwise: | Data class | Default retention | | - | - | | Customer content submitted to the platform | **7 days**, then deleted | | Transient presigned upload objects (S3 staging) | **24 hours** | | Enforcement job records and verdicts | **7 days**, aligned with the underlying content | | Activity records | Available in Command and via the Activity endpoints for up to **30 days** | | Workspace configuration | Life of the customer relationship **+ 90 days** after termination | | Product audit logs (admin/user actions) | Life of the relationship **+ up to 24 months** after termination | | Aggregated / de-identified operational metrics | Retained as needed for capacity and reliability (no message content) | ### Contractual overrides If your agreement requires ZeroDrift to retain regulated communications or audit artefacts longer than the defaults (for example, to support your SEC/FINRA recordkeeping obligations), ZeroDrift retains the covered data for the longer of the contractual minimum and the default above. Shorter retention — including discard immediately after the response is returned — may be available under enterprise arrangements. Contact [support@zerodrift.ai](mailto:support@zerodrift.ai) or your ZeroDrift account team. ## Deletion and offboarding * Send deletion or return requests through the channel in your Customer Agreement, or to [support@zerodrift.ai](mailto:support@zerodrift.ai). * Verified requests are acknowledged within **5 business days** and approved deletions completed within **30 days** (backups cleared on the next backup cycle), except where a legal hold or contractual/regulatory minimum retention applies. * On termination, the data-handling clause in your Customer Agreement governs return or deletion. Where the agreement is silent, remaining customer data is returned or deleted at your election, except where retention is required by law. ## Subprocessors and AI providers Customer data may be processed by cloud infrastructure and AI model subprocessors solely to deliver the service. Current model providers include OpenAI and Anthropic. Those providers process data to deliver the inference and do not retain it for their own purposes or use it to train their models. See the current subprocessor list in the [ZeroDrift Trust Center](https://app.vanta.com/zerodrift.ai/trust/8dwp517ay48r0q3abhwy09). ## Support Questions about retention, deletion, or contractual overrides: [support@zerodrift.ai](mailto:support@zerodrift.ai) # For AI Agents Source: https://docs.zerodrift.com/for-agents Connect coding agents to ZeroDrift's machine-readable documentation, OpenAPI specification, and docs search MCP server. Use these public, read-only surfaces to discover and query the ZeroDrift documentation while you code. They are generated and hosted by Mintlify from the same source as this site. ## Machine-readable documentation | Surface | URL | Use it for | | - | - | - | | Documentation index | [`https://docs.zerodrift.com/llms.txt`](https://docs.zerodrift.com/llms.txt) | Discover every published page and its one-line description. | | Full documentation | [`https://docs.zerodrift.com/llms-full.txt`](https://docs.zerodrift.com/llms-full.txt) | Load the complete documentation corpus into a single context. | | Markdown pages | Append `.md` to any page URL, such as [`/quickstart.md`](/quickstart.md) | Read one page as Markdown without site navigation or presentation markup. | | OpenAPI specification | [`https://docs.zerodrift.com/openapi.json`](https://docs.zerodrift.com/openapi.json) | Inspect the API contract or generate a client. | | Documentation MCP | [`https://docs.zerodrift.com/mcp`](https://docs.zerodrift.com/mcp) | Search and retrieve published ZeroDrift documentation from an MCP client. | The documentation MCP is distinct from [MCP Connectors](/connecting-mcp). The documentation MCP lets your coding agent read ZeroDrift's public docs. MCP Connectors let ZeroDrift import your policy documents from connected services. ## Add ZeroDrift guidance to your repository Paste the following into your repository's `AGENTS.md` or `CLAUDE.md`: ```markdown theme={null} ## ZeroDrift integration ZeroDrift Enforcement API. Base URL: https://api.zerodrift.ai. Authenticate with the `x-api-key` header using the `ZERODRIFT_API_KEY` environment variable. Never expose the key in source control, logs, client-side code, or prompts. Enforce text: `POST /api/v3/content/validate` with `{content, model_engine: "anchor_3_0_mini"|"anchor_3_0"|"anchor_3_0_max", mode: "sync"|"async", validation_scope?: {rulepacks?, imports?, rules?, managed_only?}, metadata?}`. Default `model_engine` is Flagship (`anchor_3_0`; older notes called this Base). Mini is detection-only, runs unscoped against its frozen rulebook, and rejects `validation_scope`. A scope-less Flagship call stays on Flagship and checks active managed rules only; activated custom rules are not evaluated. Ask for `anchor_3_0_max` explicitly to enforce activated custom rules. Max returns per-line violations without a rewrite; `fix.suggested_text` is the rule's own fix note when present and otherwise empty. A Flagship request that explicitly names a `cust_*` rule is upgraded to Max (`engine_upgrade_reason: "custom_rules"`). See [Choose a model](/models). Quality and latency: https://zerodrift.com/model/anchor-3-benchmark Sync mode returns the result inline when processing completes within the inline budget; otherwise it falls back to async mode. Custom rules need `anchor_3_0_max` for an inline sync verdict. Async mode returns `202` with `{job_id, poll.url}`; poll `GET /api/v3/jobs/{job_id}` until `status` is `completed` or `failed`. Read the enforcement decision from `result.overall_status`: `approved` = Pass (deliver); `needs_changes` = Fix (apply `fix.suggested_text` only when `fix.action` is `replace`); `do not send` = Block (deliver a fallback, never the original). Treat `status` only as job lifecycle state. Escalate when `suggested_text` is empty, `action` is not `replace`, or your policy requires review. Public usage fields are `result.units` and `result.cost`, not raw token counts. Rulepacks: `GET /api/rulepacks/`; use the returned `id` in `validation_scope.rulepacks`. Rules: `GET /api/policies/rules`; use `rule_id` in `validation_scope.rules` (custom IDs are prefixed `cust_`). Policies: `POST /api/policies/import`, poll the import, activate its rules, then use the import ID in `validation_scope.imports`. Activity log (30 days): `GET /api/activities`. Rate limit: 10 requests per second with a burst of 50; back off on `429`. Docs index: https://docs.zerodrift.com/llms.txt Full docs: https://docs.zerodrift.com/llms-full.txt OpenAPI spec: https://docs.zerodrift.com/openapi.json Docs MCP: https://docs.zerodrift.com/mcp ``` ## Start an integration Follow the current onboarding flow, then ask your coding agent: “Add ZeroDrift enforcement to our support bot. Check each generated reply before sending it, apply safe fixes, block unsafe replies, and log escalations.” # SDKs Source: https://docs.zerodrift.com/generate-sdk Generate a typed Python or TypeScript client from ZeroDrift's public OpenAPI spec ZeroDrift publishes the public API as an OpenAPI 3.0 document. You generate a typed client from that file; ZeroDrift does not ship a first-party SDK package. The spec lives at: ```text theme={null} https://docs.zerodrift.com/openapi.json ``` See [OpenAPI specification](/api-reference/openapi) for the operations it covers. Authenticate every call with the `x-api-key` header. Base URL is `https://api.zerodrift.ai`. ## Generate with an assistant Download the spec, attach `openapi.json` to your coding assistant, and ask it to write a typed client. Name the API groups you need so the generated file covers them. Example prompt: ```text theme={null} Write a typed TypeScript client for this API — cover enforce, policy, activity, and rulepacks. ``` Use `https://docs.zerodrift.com/openapi.json` as the source, not a path on `api.zerodrift.ai`. After generation, confirm the client sends `x-api-key` and calls `https://api.zerodrift.ai`. ## TypeScript with openapi-typescript [`openapi-typescript`](https://github.com/openapi-ts/openapi-typescript) turns the spec into TypeScript types. It does not emit an HTTP client; you call `fetch` (or your own wrapper) and type the requests with the generated `paths` interface. ```bash theme={null} npx --yes openapi-typescript https://docs.zerodrift.com/openapi.json -o zerodrift.ts ``` That command writes `zerodrift.ts` with `paths` and `components`. Use it like this: ```ts theme={null} import type { paths } from "./zerodrift"; const BASE_URL = "https://api.zerodrift.ai"; type ValidateBody = paths["/api/v3/content/validate"]["post"]["requestBody"]["content"]["application/json"]; type ValidateResponse = paths["/api/v3/content/validate"]["post"]["responses"]["200"]["content"]["application/json"] | paths["/api/v3/content/validate"]["post"]["responses"]["202"]["content"]["application/json"]; export async function validateContent( apiKey: string, body: ValidateBody ): Promise { const response = await fetch(`${BASE_URL}/api/v3/content/validate`, { method: "POST", headers: { "x-api-key": apiKey, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`ZeroDrift API error ${response.status}: ${await response.text()}`); } return response.json() as Promise; } const result = await validateContent("YOUR_API_KEY", { content: "Our fund guarantees 20% returns with zero risk!", mode: "sync", model_engine: "anchor_3_0", }); ``` Regenerate `zerodrift.ts` when you pick up a new spec. ## Python with OpenAPI Generator [OpenAPI Generator](https://openapi-generator.tech/) emits a full Python client. It needs Java, or Docker. ```bash theme={null} docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate \ -i https://docs.zerodrift.com/openapi.json \ -g python \ -o /local/zerodrift-python \ --additional-properties=packageName=zerodrift ``` Follow the generated README. Set the API key on the `ApiKeyHeader` scheme (the `x-api-key` header) and use host `https://api.zerodrift.ai`. If you do not want Java or Docker, [`openapi-python-client`](https://github.com/openapi-generators/openapi-python-client) generates a typed httpx client from the same URL: ```bash theme={null} pip install openapi-python-client openapi-python-client generate --url https://docs.zerodrift.com/openapi.json ``` That writes a package named from the spec title (`zero_drift_enforcement_api_client`). Pass the API key as a header. Do not use the default `Authorization: Bearer` client unless you override the header name. ```python theme={null} from zero_drift_enforcement_api_client import Client from zero_drift_enforcement_api_client.api.validate import validate_content from zero_drift_enforcement_api_client.models import ( ItemValidateV3Email, ItemValidateV3EmailMode, ItemValidateV3EmailModelEngine, ) client = Client( base_url="https://api.zerodrift.ai", headers={"x-api-key": "YOUR_API_KEY"}, ) result = validate_content.sync( client=client, body=ItemValidateV3Email( content="Our fund guarantees 20% returns with zero risk!", mode=ItemValidateV3EmailMode.SYNC, model_engine=ItemValidateV3EmailModelEngine.ANCHOR_3_0, ), ) ``` Install the generated package into your environment (for example `pip install ./zero-drift-public-api-client`) before you import it. ## Other languages The same spec URL works with any OpenAPI 3.0 generator (`-g typescript-fetch`, Go, and so on). After codegen, check three things: 1. Requests go to `https://api.zerodrift.ai` 2. The API key is sent as `x-api-key`, not `Authorization` 3. You regenerate the client when the spec changes ZeroDrift does not publish `pip install zerodrift` or `npm i @zerodrift/sdk`. Keep the generated client in your repo or private registry. API reference pages also show basic request examples. Use the language selector in the upper right for **cURL**, **Python**, **JavaScript**, **PHP**, **Go**, **Java**, or **Ruby**. # Act on Each Verdict Source: https://docs.zerodrift.com/guides/act-on-verdicts The production handler: Pass, Fix, Block, Escalate, with thresholds and logging. You have a verdict and need code that decides what the user sees. This is the reference handler every other guide builds on. ## The four outcomes | Verdict | API today | Your code | User sees | | - | - | - | - | | **Pass** | `overall_status: "approved"` | Deliver as is. | Original content. | | **Fix** | `overall_status: "needs_changes"`, a suggested **replace**, confidence at or above your bar | Apply each confident `replace`. Deliver. | Compliant content. | | **Escalate** | `needs_changes` with any fix below your bar, grouped-only findings, `insert_after`/`remove`/`warning`, or empty line lists | Hold. Put it in a human queue with the rules cited. | A holding message, then a human follow-up. | | **Block** | `overall_status: "do not send"` | Do not deliver. Return a fallback. Log. | A fallback message. | Read `overall_status` from the nested `result` object on a completed job. Poll until `status` is `completed` or `failed`. Current Anchor responses use `violations[]` with `quote`, `fix.suggested_text`, and string `confidence` (`high` / `medium` / `low`). Treat `high` as auto-fix; escalate `medium` and `low`. If a response includes `violations_by_line[].best_fix` with a numeric confidence, compare it to `0.85`. Tune from Activity: if humans approve almost every escalated fix, auto-apply `medium` as well; if they change auto-fixes often, require review even on `high`. ## Handler Call [Enforce Content](/api-reference/validate/validate-content) with `mode: "sync"`. If the response reports `mode: "async"`, poll [Get Verdict](/api-reference/validate/get-results) until the job is terminal (see [Long-Form Content](/guides/long-form-content)). ```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" }' ``` ```python Python theme={null} import os import time import logging from dataclasses import dataclass import requests API = "https://api.zerodrift.ai" HEADERS = {"x-api-key": os.environ["ZERODRIFT_API_KEY"]} NUMERIC_THRESHOLD = 0.85 TERMINAL = {"completed", "failed"} @dataclass class Outcome: verdict: str # "pass" | "fix" | "block" | "escalate" text: str | None rules: list[str] 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) >= NUMERIC_THRESHOLD except (TypeError, ValueError): return False def wait_for_job(poll_url: str, timeout_s: int = 180) -> dict: delay, waited = 1.0, 0.0 while waited < timeout_s: r = requests.get(f"{API}{poll_url}", headers=HEADERS, timeout=10) r.raise_for_status() job = r.json() if job["status"] in TERMINAL: if job["status"] == "failed": raise RuntimeError(job.get("error", "enforcement failed")) return job time.sleep(delay) waited += delay delay = min(delay * 2, 5.0) raise TimeoutError(poll_url) def violations_of(job: dict) -> list[dict]: body = payload(job) return body.get("violations_by_line") or body.get("violations") or [] def rule_label(entry: dict) -> str | None: return ( entry.get("rule_ref") or entry.get("rule_id") or entry.get("signal_name") or entry.get("rule_name") ) def collect_rules(job: dict) -> list[str]: labels = set() for v in violations_of(job): if v.get("rules_violated"): for rv in v["rules_violated"]: if rule_label(rv): labels.add(rule_label(rv)) elif rule_label(v): labels.add(rule_label(v)) return sorted(labels) def apply_fixes(content: str, job: dict) -> str | None: items = violations_of(job) if not items: return None text = content 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 None if "line_number" in v: lines = text.split("\n") lines[v["line_number"] - 1] = suggested text = "\n".join(lines) else: quote = v.get("quote") or v.get("line_text") if not quote or quote not in text: return None text = text.replace(quote, suggested) return text def enforce( content: str, *, scope: dict | None = None, meta: dict | None = None, ) -> Outcome: body = { "content": content, "model_engine": "anchor_3_0", "mode": "sync", } if scope: body["validation_scope"] = scope if meta: body["metadata"] = meta r = requests.post( f"{API}/api/v3/content/validate", headers=HEADERS, json=body, timeout=30, ) r.raise_for_status() job = r.json() if job.get("mode") == "async": job = wait_for_job(job["poll"]["url"]) rules = collect_rules(job) status = payload(job).get("overall_status") if status == "approved": return Outcome("pass", content, rules) if status == "do not send": logging.warning("blocked job=%s rules=%s", job.get("job_id"), rules) return Outcome("block", None, rules) fixed = apply_fixes(content, job) if fixed is None: return Outcome("escalate", None, rules) return Outcome("fix", fixed, rules) outcome = enforce("Our fund guarantees 20% returns with zero risk!") print(outcome) ``` ```typescript TypeScript theme={null} type Verdict = "pass" | "fix" | "block" | "escalate"; export interface Outcome { verdict: Verdict; text: string | null; rules: string[]; } const API = "https://api.zerodrift.ai"; const HEADERS = { "x-api-key": process.env.ZERODRIFT_API_KEY!, "Content-Type": "application/json", }; const NUMERIC_THRESHOLD = 0.85; const TERMINAL = new Set(["completed", "failed"]); 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 >= NUMERIC_THRESHOLD; } export async function waitForJob(pollUrl: string, timeoutMs = 180_000) { let delay = 1000; let waited = 0; while (waited < timeoutMs) { const r = await fetch(`${API}${pollUrl}`, { headers: HEADERS }); if (!r.ok) throw new Error(`ZeroDrift ${r.status}`); const job = await r.json(); if (job.status === "failed") throw new Error(job.error ?? "enforcement failed"); 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}`); } function violationsOf(job: any): any[] { const body = payload(job); return body.violations_by_line ?? body.violations ?? []; } function ruleLabel(entry: any): string | undefined { return entry?.rule_ref || entry?.rule_id || entry?.signal_name || entry?.rule_name; } function collectRules(job: any): string[] { const labels = new Set(); for (const v of violationsOf(job)) { if (v.rules_violated) { for (const rv of v.rules_violated) { const label = ruleLabel(rv); if (label) labels.add(label); } } else { const label = ruleLabel(v); if (label) labels.add(label); } } return [...labels].sort(); } function applyFixes(content: string, job: any): string | null { const items = violationsOf(job); if (!items.length) return null; let text = content; 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 null; if (v.line_number != null) { 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)) return null; text = text.replaceAll(quote, suggested); } } return text; } export async function enforce( content: string, opts: { scope?: object; meta?: object } = {}, ): Promise { const body: Record = { content, model_engine: "anchor_3_0", mode: "sync", }; if (opts.scope) body.validation_scope = opts.scope; if (opts.meta) body.metadata = opts.meta; const r = await fetch(`${API}/api/v3/content/validate`, { method: "POST", headers: HEADERS, body: JSON.stringify(body), }); if (!r.ok) throw new Error(`ZeroDrift ${r.status}`); let job = await r.json(); if (job.mode === "async") job = await waitForJob(job.poll.url); const rules = collectRules(job); const status = payload(job).overall_status; if (status === "approved") return { verdict: "pass", text: content, rules }; if (status === "do not send") return { verdict: "block", text: null, rules }; const fixed = applyFixes(content, job); if (fixed == null) return { verdict: "escalate", text: null, rules }; return { verdict: "fix", text: fixed, rules }; } const outcome = await enforce("Our fund guarantees 20% returns with zero risk!"); console.log(outcome); ``` ## Showing a reviewer why Each violation includes a rule id (`rule_id` / `signal_name`) and the quoted text. Line-grouped responses add `rules_violated[].rule_ref` (for example `FINRA 2210(d)(1)(B)`). Surface the id, quote, severity, and confidence in your review UI. ## Do not * Do not deliver the original text on Fix. The fix is the compliant version. * Do not retry a Block with the same content. Block means no compliant version exists. * Do not strip the disclosure a fix added to make the message shorter. * Do not fail open silently. If the API is unreachable, log it and apply your fail-open or fail-closed policy on purpose. * Poll until `completed` or `failed`. ## Next Apply this handler on every chatbot reply before delivery. Enforce each hop in a multi-agent workflow. Tune your threshold from real verdicts. # Agent-to-Agent Traffic Source: https://docs.zerodrift.com/guides/agent-to-agent Enforce every message an agent sends, including messages between agents. You run a multi-agent workflow: a planner delegates to workers, workers return results, an outbound agent writes to the customer. A violation introduced at any step propagates downstream. Enforcing only the final message means the final agent inherits the problem and may not be able to fix it. Enforce at every hop. ## Prerequisites * An API key ([Quickstart](/quickstart)). * A place in your orchestrator to wrap each hop (agent output → next agent or customer). ## The pattern Wrap the point where one agent's output becomes another agent's input (or a customer message) in one function. Reuse the [Act on Each Verdict](/guides/act-on-verdicts) outcomes: Pass, Fix, Block, Escalate. ``` agent A output → enforce(sender=A, recipient=B, message) → agent B input │ └─ Block → halt run, open Escalate ticket ``` ## Code Raise a `Blocked` exception on Block or low-confidence Fix so the orchestrator can halt and escalate. ```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": "Tell the client we guarantee 20% returns with no downside.", "model_engine": "anchor_3_0", "mode": "sync" }' ``` ```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"} class Blocked(Exception): pass 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 wait_for_job(poll_url: str) -> dict: while True: r = requests.get(f"{API}{poll_url}", headers=HEADERS, timeout=10) r.raise_for_status() job = r.json() if job["status"] == "failed": raise Blocked(job.get("error", "enforcement failed")) if job["status"] in TERMINAL: return job time.sleep(2) def enforce_hop( message: str, *, sender: str, recipient: str, run_id: str, step: int, ) -> str: r = requests.post( f"{API}/api/v3/content/validate", headers=HEADERS, json={ "content": message, "model_engine": "anchor_3_0", "mode": "sync", }, timeout=30, ) r.raise_for_status() job = r.json() if job.get("mode") == "async": job = wait_for_job(job["poll"]["url"]) body = payload(job) status = body.get("overall_status") if status == "approved": return message if status == "do not send": raise Blocked(f"run {run_id} step {step}: {sender} → {recipient}") text = message items = body.get("violations_by_line") or body.get("violations") or [] if not items: raise Blocked(f"run {run_id} step {step}: no line-level replace") 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): raise Blocked(f"run {run_id} step {step}: escalate") 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: raise Blocked(f"run {run_id} step {step}: escalate") return text try: handoff = enforce_hop( "Tell the client we guarantee 20% returns with no downside.", sender="planner", recipient="writer", run_id="run_456", step=2, ) print(handoff) except Blocked as e: # In your orchestrator: halt_run(run_id); open_escalation(str(e)) print("halt and escalate:", e) ``` ```typescript TypeScript theme={null} const API = "https://api.zerodrift.ai"; const TERMINAL = new Set(["completed", "failed"]); export class Blocked extends Error {} 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; } async function waitForJob(pollUrl: string) { for (;;) { const r = await fetch(`${API}${pollUrl}`, { headers: { "x-api-key": process.env.ZERODRIFT_API_KEY! }, }); if (!r.ok) throw new Blocked(`ZeroDrift ${r.status}`); const job = await r.json(); if (job.status === "failed") { throw new Blocked(job.error ?? "enforcement failed"); } if (TERMINAL.has(job.status)) return job; await new Promise((resolve) => setTimeout(resolve, 2000)); } } export async function enforceHop( message: string, meta: { sender: string; recipient: string; runId: string; step: number; }, ): Promise { 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: message, model_engine: "anchor_3_0", mode: "sync", }), }); if (!r.ok) throw new Error(`ZeroDrift ${r.status}`); let job = await r.json(); if (job.mode === "async") job = await waitForJob(job.poll.url); const body = payload(job); if (body.overall_status === "approved") return message; if (body.overall_status === "do not send") { throw new Blocked(`run ${meta.runId} step ${meta.step}`); } let text = message; const items = body.violations_by_line ?? body.violations ?? []; if (!items.length) { throw new Blocked(`run ${meta.runId} step ${meta.step}: no line-level replace`); } 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)) { throw new Blocked(`run ${meta.runId} step ${meta.step}: escalate`); } if (v.line_number) { const lines = text.split("\n"); lines[v.line_number - 1] = suggested; text = lines.join("\n"); } else if (v.quote && text.includes(v.quote)) { text = text.replaceAll(v.quote, suggested); } else { throw new Blocked(`run ${meta.runId} step ${meta.step}: escalate`); } } return text; } try { const handoff = await enforceHop( "Tell the client we guarantee 20% returns with no downside.", { sender: "planner", recipient: "writer", runId: "run_456", step: 2, }, ); console.log(handoff); } catch (e) { if (e instanceof Blocked) { // In your orchestrator: halt the run and open an escalation console.log("halt and escalate:", e.message); } else { throw e; } } ``` ## Handling Block in a running workflow * Halt the run. Do not let downstream agents proceed on unenforced input. * Open an escalation with your orchestrator’s run/step identifiers and the rules cited (`rule_id` / `signal_name`, or `rules_violated[].rule_ref` when present). * Do not retry the same content. A Block means no compliant version exists. ## Tool results are text too A tool result that an agent will read and act on is still text. Enforce it with the same `POST /api/v3/content/validate` call before the next agent consumes it. Optional `metadata` on that endpoint is a free-form object merged into the job. It is not required for per-hop enforcement, and keys such as `surface` or `run_id` are not a documented Activity filter schema. Use [List Activity](/api-reference/activities/list-activities) filters (`agent_id`, `source`, `rule_id`, `status`, `search`) only as that page defines them. ## Gotchas * **Enforce structured messages as text.** If agents pass JSON, enforce the human-readable fields, not the envelope. * **Sync can fall back to async** on long hops. Check `mode` and poll until `completed` or `failed` ([Long-Form Content](/guides/long-form-content)). ## Next Pull the activity chain for a run. The Pass / Fix / Block / Escalate handler pattern. # Enforce a Chatbot Source: https://docs.zerodrift.com/guides/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. ## 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 { 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](/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.** 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](/guides/act-on-verdicts). ## Next Thresholds, logging, and a human review queue. Different rules per customer when one bot serves many tenants. # Export Activity Source: https://docs.zerodrift.com/guides/export-activity Pull the verdict log for review, reporting, and your own retention. Every verdict is logged in Command. The Activity endpoints read that log for up to **30 days**. Use them for nightly exports, threshold tuning, filters by agent or source, or copying verdicts into your own warehouse for longer retention. ## Endpoints ``` GET /api/activities list, newest first, 30-day window max GET /api/activities/{activity_id} one record with timeline and rule detail ``` List filters (repeatable where noted) match [List Activity](/api-reference/activities/list-activities): | Param | Meaning | | - | - | | `from` / `to` | Window as ISO 8601 date-times (max 30 days) | | `status` | Repeatable: `auto_fixed`, `blocked`, `warned`, `passed` | | `agent_id` | Repeatable agent id | | `source` | Repeatable source | | `rule_id` | Repeatable rule id | | `search` | Free-text search | | `cursor` | Opaque pagination cursor from a previous response | | `limit` | Page size 1–100 | List items include `activity_id`, `occurred_at`, `event_name`, `status`, `source`, `provider`, `model`, `agent` (`id`, `name`), `rules` (`id`, `name`, `severity`, `type`, `policy`), `rule_count`, and `truncated_rule_count`. The detail endpoint adds timeline and fuller rule fields. `status` maps to product outcomes: `passed` ≈ Pass, `auto_fixed` ≈ Fix, `blocked` ≈ Block, `warned` ≈ delivered with caution. ## Nightly paginated export Run once per night. Keep each request inside a 30-day window. Overlap the previous run (lookback) so records that appear after they occurred are not missed; dedupe by `activity_id`. Walk `next_cursor` until it is null. Encode `cursor` as a query value (`--data-urlencode` or an equivalent). Write CSV or NDJSON from the public list fields only. ```bash cURL theme={null} # Last 24 hours plus a 6-hour lookback (keep from/to within 30 days) FROM=$(date -u -d '30 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ || date -u -v-30H +%Y-%m-%dT%H:%M:%SZ) TO=$(date -u +%Y-%m-%dT%H:%M:%SZ) CURSOR="" : > activity.ndjson while true; do ARGS=( --data-urlencode "from=$FROM" --data-urlencode "to=$TO" --data-urlencode "limit=100" --data-urlencode "status=blocked" --data-urlencode "status=warned" --data-urlencode "status=auto_fixed" --data-urlencode "status=passed" ) if [ -n "$CURSOR" ]; then ARGS+=(--data-urlencode "cursor=$CURSOR") fi PAGE=$(curl -sG "https://api.zerodrift.ai/api/activities" \ -H "x-api-key: $ZERODRIFT_API_KEY" \ "${ARGS[@]}") printf '%s' "$PAGE" | jq -c '.items[]' >> activity.ndjson CURSOR=$(printf '%s' "$PAGE" | jq -r '.next_cursor // empty') if [ -z "$CURSOR" ] || [ "$CURSOR" = "null" ]; then break fi done # Dedupe overlapping lookback windows by activity_id jq -sc 'unique_by(.activity_id)[]' activity.ndjson > activity.unique.ndjson wc -l activity.unique.ndjson ``` ```python Python theme={null} import csv import os from datetime import datetime, timedelta, timezone import requests API = "https://api.zerodrift.ai" HEADERS = {"x-api-key": os.environ["ZERODRIFT_API_KEY"]} def export_activity(days: int = 1, path: str = "activity.csv") -> int: if days > 30: raise ValueError("Activity windows are capped at 30 days") to = datetime.now(timezone.utc) # 6-hour lookback so late-visible records overlap yesterday's export # Clamp so from/to never exceeds the 30-day API cap frm = max(to - timedelta(days=days, hours=6), to - timedelta(days=30)) params: dict = { "from": frm.isoformat(), "to": to.isoformat(), "limit": 100, # Repeatable filters — same names as List Activity "status": ["auto_fixed", "blocked", "warned", "passed"], } # Optional: params["agent_id"] = [os.environ["AGENT_ID"]] # Optional: params["source"] = ["api"] # Optional: params["rule_id"] = [os.environ["RULE_ID"]] # Optional: params["search"] = "guarantees" by_id: dict[str, dict] = {} cursor = None while True: if cursor: params["cursor"] = cursor page = requests.get( f"{API}/api/activities", headers=HEADERS, params=params, timeout=30, ) page.raise_for_status() body = page.json() for a in body["items"]: by_id[a["activity_id"]] = { "activity_id": a["activity_id"], "occurred_at": a["occurred_at"], "event_name": a.get("event_name"), "status": a["status"], "source": a.get("source"), "provider": a.get("provider"), "model": a.get("model"), "agent_id": (a.get("agent") or {}).get("id"), "agent_name": (a.get("agent") or {}).get("name"), "rule_count": a.get("rule_count"), "rules": "; ".join( r.get("name") or r.get("id") or "" for r in a.get("rules") or [] ), } cursor = body.get("next_cursor") if not cursor: break rows = list(by_id.values()) fieldnames = [ "activity_id", "occurred_at", "event_name", "status", "source", "provider", "model", "agent_id", "agent_name", "rule_count", "rules", ] with open(path, "w", newline="") as f: w = csv.DictWriter(f, fieldnames=fieldnames) w.writeheader() w.writerows(rows) return len(rows) print(export_activity(days=1)) ``` ```typescript TypeScript theme={null} const API = "https://api.zerodrift.ai"; type ActivityItem = { activity_id: string; occurred_at: string; event_name?: string; status: string; source?: string; provider?: string; model?: string; agent?: { id?: string; name?: string } | null; rule_count?: number; rules?: { id?: string; name?: string }[]; }; export async function exportActivity(days = 1) { if (days > 30) throw new Error("Activity windows are capped at 30 days"); const to = new Date(); // 6-hour lookback so late-visible records overlap yesterday's export // Clamp so from/to never exceeds the 30-day API cap const from = new Date( Math.max( to.getTime() - days * 86_400_000 - 6 * 3_600_000, to.getTime() - 30 * 86_400_000, ), ); const byId = new Map>(); let cursor: string | null = null; do { const url = new URL(`${API}/api/activities`); url.searchParams.set("from", from.toISOString()); url.searchParams.set("to", to.toISOString()); url.searchParams.set("limit", "100"); for (const s of ["auto_fixed", "blocked", "warned", "passed"]) { url.searchParams.append("status", s); } // Optional documented filters: // if (process.env.AGENT_ID) url.searchParams.append("agent_id", process.env.AGENT_ID); // if (process.env.RULE_ID) url.searchParams.append("rule_id", process.env.RULE_ID); // url.searchParams.append("source", "api"); // url.searchParams.set("search", "guarantees"); if (cursor) url.searchParams.set("cursor", cursor); const r = await fetch(url, { headers: { "x-api-key": process.env.ZERODRIFT_API_KEY! }, }); if (!r.ok) throw new Error(`ZeroDrift ${r.status}`); const page = await r.json(); for (const a of page.items as ActivityItem[]) { byId.set(a.activity_id, { activity_id: a.activity_id, occurred_at: a.occurred_at, event_name: a.event_name, status: a.status, source: a.source, provider: a.provider, model: a.model, agent_id: a.agent?.id, agent_name: a.agent?.name, rule_count: a.rule_count, rules: (a.rules ?? []).map((x) => x.name ?? x.id ?? "").join("; "), }); } cursor = page.next_cursor ?? null; } while (cursor); // NDJSON-style lines for warehouses; or convert to CSV in your job runner return [...byId.values()].map((row) => JSON.stringify(row)).join("\n"); } console.log(await exportActivity(1)); ``` ## Gotchas * **30 days.** Ranges longer than 30 days return `400`. Export nightly and keep your own copy for longer retention ([Data Retention](/data-retention)). * **Lookback.** Activity is eventually consistent. Overlap the previous window and dedupe by `activity_id` so late records are not dropped. If the overlap would exceed 30 days, clamp `from` to the cap. * **Filters.** Use only the documented list params above. Optional `metadata` on enforce is merged into the job; do not assume undocumented metadata keys are Activity filters. * **Tune thresholds.** Compare `auto_fixed` volume to human review outcomes; adjust the confidence threshold in [Act on Each Verdict](/guides/act-on-verdicts). ## Next How long ZeroDrift keeps content, verdicts, and activity records. The Pass / Fix / Block / Escalate handler this export helps you tune. # Import a Policy Source: https://docs.zerodrift.com/guides/import-a-policy Turn your own policy document into enforceable rules. You have a rulebook that is not a regulation: a communications policy, a claims manual, a brand guide, an underwriting policy. You want Anchor to enforce it alongside managed rule packs. ## Prerequisites * A full-access API key ([Quickstart](/quickstart)). * The policy as PDF, DOCX, or plain text, or in Notion, Linear, Confluence, or Google Drive. ## The flow ``` import → poll extraction → review → activate → enforce (optional: train) ``` Training an adapter is optional and comes after activation. See [Train an Adapter](/guides/train-an-adapter). ## Step 1: Import Three intake paths. Pick one. Every path yields an `import_id`. ### Inline text or small file `POST /api/policies/import` sends the document in the body (`source: "text"` or base64 `source: "file"`). Payload size is capped; use the presigned flow for large files. Set `POLICY_TEXT` or encode a small file; use the returned `import_id` for later steps. ```bash cURL theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/import" \ -H "x-api-key: $ZERODRIFT_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"source\": \"text\", \"content\": \"Client-facing messages must not promise outcomes. Every recommendation must state the basis for the recommendation.\", \"filename\": \"comms-policy.txt\" }" # Small file (base64): # DOC_BASE64=$(base64 -i "$POLICY_FILE") # curl ... -d "{\"source\":\"file\",\"content\":\"$DOC_BASE64\",\"filename\":\"$(basename "$POLICY_FILE")\"}" ``` ```python Python theme={null} import base64 import os import requests API = "https://api.zerodrift.ai" HEADERS = { "x-api-key": os.environ["ZERODRIFT_API_KEY"], "Content-Type": "application/json", } policy_file = os.environ.get("POLICY_FILE") if policy_file: with open(policy_file, "rb") as f: payload = { "source": "file", "content": base64.b64encode(f.read()).decode(), "filename": os.path.basename(policy_file), } else: payload = { "source": "text", "content": os.environ.get( "POLICY_TEXT", "Client-facing messages must not promise outcomes. " "Every recommendation must state the basis for the recommendation.", ), "filename": "comms-policy.txt", } r = requests.post(f"{API}/api/policies/import", headers=HEADERS, json=payload, timeout=60) r.raise_for_status() import_id = r.json()["import_id"] print("import_id:", import_id) ``` ```typescript TypeScript theme={null} import { readFile } from "node:fs/promises"; import path from "node:path"; const API = "https://api.zerodrift.ai"; const HEADERS = { "x-api-key": process.env.ZERODRIFT_API_KEY!, "Content-Type": "application/json", }; let payload: Record; if (process.env.POLICY_FILE) { const buf = await readFile(process.env.POLICY_FILE); payload = { source: "file", content: buf.toString("base64"), filename: path.basename(process.env.POLICY_FILE), }; } else { payload = { source: "text", content: process.env.POLICY_TEXT ?? "Client-facing messages must not promise outcomes. Every recommendation must state the basis for the recommendation.", filename: "comms-policy.txt", }; } const r = await fetch(`${API}/api/policies/import`, { method: "POST", headers: HEADERS, body: JSON.stringify(payload), }); if (!r.ok) throw new Error(`ZeroDrift ${r.status}`); const { import_id: importId } = await r.json(); console.log("import_id:", importId); ``` ### Large file (presigned upload) 1. `POST /api/policies/import/presigned_url` → `import_id`, `upload_url`, `required_headers` 2. `PUT` the raw bytes to `upload_url` with **every** header in `required_headers` (omitting them causes `SignatureDoesNotMatch`) 3. `POST /api/policies/import/start` with that `import_id`. If status is `scan_pending`, retry until extraction starts (`processing`) Max upload size is **1 GB**. Set `POLICY_FILE` to the path on disk. ```bash cURL theme={null} POLICY_FILE="${POLICY_FILE:?set POLICY_FILE to your PDF path}" RESP=$(curl -s -X POST "https://api.zerodrift.ai/api/policies/import/presigned_url" \ -H "x-api-key: $ZERODRIFT_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"filename\": \"$(basename "$POLICY_FILE")\", \"content_type\": \"application/pdf\"}") IMPORT_ID=$(printf '%s' "$RESP" | jq -r '.import_id') UPLOAD_URL=$(printf '%s' "$RESP" | jq -r '.upload_url') CURL_HEADERS=() while IFS=$'\t' read -r key value; do CURL_HEADERS+=(-H "$key: $value") done < <(printf '%s' "$RESP" | jq -r '.required_headers | to_entries[] | "\(.key)\t\(.value)"') curl -X PUT "$UPLOAD_URL" "${CURL_HEADERS[@]}" --data-binary @"$POLICY_FILE" # Retry while malware scan is still pending while true; do START=$(curl -s -X POST "https://api.zerodrift.ai/api/policies/import/start" \ -H "x-api-key: $ZERODRIFT_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"import_id\": \"$IMPORT_ID\"}") STATUS=$(printf '%s' "$START" | jq -r '.status // empty') if [ "$STATUS" != "scan_pending" ]; then echo "$START" break fi sleep 3 done echo "IMPORT_ID=$IMPORT_ID" ``` ```python Python theme={null} import os import time import requests API = "https://api.zerodrift.ai" API_KEY = os.environ["ZERODRIFT_API_KEY"] HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"} policy_file = os.environ["POLICY_FILE"] presign = requests.post( f"{API}/api/policies/import/presigned_url", headers=HEADERS, json={ "filename": os.path.basename(policy_file), "content_type": "application/pdf", }, timeout=30, ).json() import_id = presign["import_id"] with open(policy_file, "rb") as f: requests.put( presign["upload_url"], headers=presign["required_headers"], data=f, timeout=120, ).raise_for_status() while True: started = requests.post( f"{API}/api/policies/import/start", headers=HEADERS, json={"import_id": import_id}, timeout=30, ).json() if started.get("status") != "scan_pending": break time.sleep(3) if "status" not in started: raise SystemExit(f"Start failed: {started.get('error', started)}") print("import_id:", import_id, "status:", started["status"]) ``` ```typescript TypeScript theme={null} import { readFile } from "node:fs/promises"; import path from "node:path"; const API = "https://api.zerodrift.ai"; const API_KEY = process.env.ZERODRIFT_API_KEY!; const policyFile = process.env.POLICY_FILE!; const presignRes = await fetch(`${API}/api/policies/import/presigned_url`, { method: "POST", headers: { "x-api-key": API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ filename: path.basename(policyFile), content_type: "application/pdf", }), }); if (!presignRes.ok) throw new Error(`presign ${presignRes.status}`); const presign = await presignRes.json(); const fileBytes = await readFile(policyFile); const put = await fetch(presign.upload_url, { method: "PUT", headers: presign.required_headers as Record, body: fileBytes, }); if (!put.ok) throw new Error(`upload ${put.status}`); let started: { status?: string; error?: string }; for (;;) { const startRes = await fetch(`${API}/api/policies/import/start`, { method: "POST", headers: { "x-api-key": API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ import_id: presign.import_id }), }); started = await startRes.json(); if (started.status !== "scan_pending") break; await new Promise((r) => setTimeout(r, 3000)); } if (!started.status) { throw new Error(`Start failed: ${started.error ?? JSON.stringify(started)}`); } console.log("import_id:", presign.import_id, "status:", started.status); ``` ### Connected source In Command, open **Settings → Sources**, connect Notion, Linear, Confluence, or Google Drive, select a document, and sync. The sync creates the same `import_id` as an upload. Details: [MCP Connectors](/connecting-mcp). ## Step 2: Poll extraction Poll `GET /api/policies/import/{import_id}` until `status` is no longer `processing`. Continue when it is `pending_review`. `no_rules_found` and `failed` are not activatable — fix the document and import again. Use `IMPORT_ID` from the previous step (or your environment). ```bash cURL theme={null} : "${IMPORT_ID:?set IMPORT_ID from the previous step}" while true; do DETAILS=$(curl -s "https://api.zerodrift.ai/api/policies/import/$IMPORT_ID" \ -H "x-api-key: $ZERODRIFT_API_KEY") STATUS=$(printf '%s' "$DETAILS" | jq -r '.status') if [ "$STATUS" != "processing" ]; then break fi sleep 3 done echo "$STATUS $(printf '%s' "$DETAILS" | jq -r '.rule_count // empty')" if [ "$STATUS" != "pending_review" ]; then echo "import is not activatable: $STATUS" >&2 exit 1 fi ``` ```python Python theme={null} import os import time import requests API = "https://api.zerodrift.ai" HEADERS = {"x-api-key": os.environ["ZERODRIFT_API_KEY"]} import_id = os.environ["IMPORT_ID"] while True: details = requests.get( f"{API}/api/policies/import/{import_id}", headers=HEADERS, timeout=30, ).json() if details.get("status") != "processing": break time.sleep(3) print(details["status"], details.get("rule_count")) if details.get("status") != "pending_review": raise SystemExit(f"import is not activatable: {details.get('status')}") ``` ```typescript TypeScript theme={null} const API = "https://api.zerodrift.ai"; const importId = process.env.IMPORT_ID!; let details: { status: string; rule_count?: number }; for (;;) { const r = await fetch(`${API}/api/policies/import/${importId}`, { headers: { "x-api-key": process.env.ZERODRIFT_API_KEY! }, }); if (!r.ok) throw new Error(`ZeroDrift ${r.status}`); details = await r.json(); if (details.status !== "processing") break; await new Promise((res) => setTimeout(res, 3000)); } console.log(details.status, details.rule_count); if (details.status !== "pending_review") { throw new Error(`import is not activatable: ${details.status}`); } ``` ## Step 3: Review Inspect extracted rules in the poll response or in Command. Before activation you can PATCH a pending rule's `prompt`, `fix_note`, or `confidence` ([Edit Imported Rule](/api-reference/custom-policies/edit-imported-rule)). Pass `null` to clear `prompt` or `fix_note`. ## Step 4: Activate `POST /api/policies/import/{import_id}/activate` activates every extracted rule (or a `rule_ids` subset). Set `overwrite: true` to replace rules from an earlier import when ids collide. Activated rules run on enforcement for your API key. Use `validation_scope.imports` when you need to narrow to this import (see [Per-Tenant Rulepacks](/guides/per-tenant-rulepacks)). ```bash cURL theme={null} curl -X POST "https://api.zerodrift.ai/api/policies/import/$IMPORT_ID/activate" \ -H "x-api-key: $ZERODRIFT_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```python Python theme={null} import os import requests API = "https://api.zerodrift.ai" HEADERS = { "x-api-key": os.environ["ZERODRIFT_API_KEY"], "Content-Type": "application/json", } import_id = os.environ["IMPORT_ID"] r = requests.post( f"{API}/api/policies/import/{import_id}/activate", headers=HEADERS, json={}, timeout=30, ) r.raise_for_status() print(r.json()) ``` ```typescript TypeScript theme={null} const API = "https://api.zerodrift.ai"; const importId = process.env.IMPORT_ID!; const r = await fetch(`${API}/api/policies/import/${importId}/activate`, { method: "POST", headers: { "x-api-key": process.env.ZERODRIFT_API_KEY!, "Content-Type": "application/json", }, body: "{}", }); if (!r.ok) throw new Error(`ZeroDrift ${r.status}`); console.log(await r.json()); ``` ## Step 5: Enforce against it After activation, untrained custom rules run on Max. Scope with `validation_scope.imports` and `model_engine: "anchor_3_0_max"`. After you [train an adapter](/guides/train-an-adapter), Flagship (`anchor_3_0`) can check that import. ```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\": \"You will definitely be approved and your rate will never change.\", \"model_engine\": \"anchor_3_0_max\", \"mode\": \"sync\", \"validation_scope\": { \"imports\": [\"$IMPORT_ID\"] } }" ``` ```python Python theme={null} import os import requests API = "https://api.zerodrift.ai" HEADERS = { "x-api-key": os.environ["ZERODRIFT_API_KEY"], "Content-Type": "application/json", } import_id = os.environ["IMPORT_ID"] r = requests.post( f"{API}/api/v3/content/validate", headers=HEADERS, json={ "content": "You will definitely be approved and your rate will never change.", "model_engine": "anchor_3_0_max", "mode": "sync", "validation_scope": {"imports": [import_id]}, }, timeout=30, ) r.raise_for_status() print(r.json()) ``` ```typescript TypeScript theme={null} const API = "https://api.zerodrift.ai"; const importId = process.env.IMPORT_ID!; 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: "You will definitely be approved and your rate will never change.", model_engine: "anchor_3_0_max", mode: "sync", validation_scope: { imports: [importId] }, }), }); if (!r.ok) throw new Error(`ZeroDrift ${r.status}`); console.log(await r.json()); ``` ## Gotchas * **Retention.** The original document is kept **7 days** by default ([Data Retention](/data-retention)). Training after that returns `410`; re-import. * **Engine.** Untrained custom rules run on `anchor_3_0_max`. Flagship (`anchor_3_0`) can check the import after you train an adapter. See [Enforce Content](/api-reference/validate/validate-content). * **Re-sync replaces.** Syncing a connected document again updates the import; it does not create a duplicate. * **Read-only connections.** ZeroDrift does not write back to Notion, Linear, Confluence, or Google Drive. * **Presigned headers.** Always send `required_headers` on the S3 `PUT`. Retry `scan_pending` on start. ## Next Teach Anchor your policy's judgment, not just its extracted rules. Give each customer their own packs and imports. # Long-Form Content Source: https://docs.zerodrift.com/guides/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 | 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](/guides/act-on-verdicts). ## 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 { 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 Pass / Fix / Block / Escalate after the job completes. Pull verdicts for every document you have enforced. # Per-Tenant Rulepacks Source: https://docs.zerodrift.com/guides/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](/api-reference/rulepacks) (`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](/guides/import-a-policy) 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](/guides/import-a-policy), 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](/api-reference/validate/validate-content)). Activity list filters are the documented query params on [List Activity](/api-reference/activities/list-activities) (`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 = { "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](/guides/import-a-policy). 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](/api-reference/validate/validate-content). * **`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 List managed and custom packs; use returned ids in `validation_scope.rulepacks`. Pull the verdict log for reporting and threshold tuning. # Train an Adapter Source: https://docs.zerodrift.com/guides/train-an-adapter Train a policy-specific adapter in Training Studio, then enforce against it. You have imported and activated a policy. Extraction gives Anchor the letter of the policy. Training Studio trains a policy-specific adapter on ZeroDrift's base so enforcement can apply that policy's judgment, not only the extracted rule list. Anchor handles regulations. Your adapter handles your policies. Train when the policy is nuanced, when extracted rules alone over-escalate, or when you want a single model path for regulations plus your policy. You do **not** have to train for custom rules to run — [Activate Rules](/api-reference/custom-policies/activate-rules) is enough for extracted rules. Untrained custom rules run on **Max** (`anchor_3_0_max`). Training lets Flagship (`anchor_3_0`) check that import. At launch, adapters are available only for **Anchor 3.0 (Flagship)**. Max evaluates uploaded policies at inference and has no training charge. See [Choose a model](/models) and [Pricing](/pricing). ## Terms | Term | Meaning | | - | - | | **Policy** | The imported document and the rules extracted from it. | | **Adapter** | The policy-specific model trained from that import. After a successful training poll, scoped enforcement can use it. | | **Training run** | One Training Studio job for an import, identified by `training_run_id`. | ## Prerequisites * Full-access API key (read-only returns `403`). * An import whose rules are **activated** (pending review returns `409`). * The original document still within retention — **7 days** by default (`410` if expired). * No other training run in progress for this import (`409`). * Training enabled for your environment (`503` if not). Set `IMPORT_ID` to the activated import id from [Import a Policy](/guides/import-a-policy). ## Flow ``` train → poll until succeeded|failed → enforce with validation_scope.imports ``` ## Start, poll, enforce Each sample starts training, polls to a terminal status, then enforces with `validation_scope.imports`. Omit the POST body to use Training Studio defaults, or set `cost_cap_usd` and `examples_per_rule`. Do not enforce against a still-training run. ZeroDrift promotes the adapter only after a successful status poll. Until then, scoped enforcement follows its configured fallback path. A failed retrain does not replace an earlier working adapter. ```bash cURL theme={null} IMPORT_ID="${IMPORT_ID:?set IMPORT_ID to an activated import}" curl -fsS -X POST "https://api.zerodrift.ai/api/policies/import/$IMPORT_ID/train" \ -H "x-api-key: $ZERODRIFT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"cost_cap_usd": 25, "examples_per_rule": 8}' # Poll until training_status or status is succeeded or failed while true; do RESP=$(curl -fsS "https://api.zerodrift.ai/api/policies/import/$IMPORT_ID/train" \ -H "x-api-key: $ZERODRIFT_API_KEY") STATUS=$(printf '%s' "$RESP" | jq -r '.training_status // .status') echo "$STATUS" case "$STATUS" in succeeded|failed) break ;; queued) ;; *) printf '%s\n' "$RESP" >&2; exit 1 ;; esac sleep 10 done printf '%s\n' "$RESP" test "$STATUS" = "succeeded" 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\", \"validation_scope\": {\"imports\": [\"$IMPORT_ID\"]} }" ``` ```python Python theme={null} import os import time import requests API = "https://api.zerodrift.ai" HEADERS = { "x-api-key": os.environ["ZERODRIFT_API_KEY"], "Content-Type": "application/json", } IMPORT_ID = os.environ["IMPORT_ID"] URL = f"{API}/api/policies/import/{IMPORT_ID}/train" started = requests.post( URL, headers=HEADERS, json={"cost_cap_usd": 25, "examples_per_rule": 8}, timeout=30, ) started.raise_for_status() print(started.json()) while True: training = requests.get(URL, headers=HEADERS, timeout=30) training.raise_for_status() body = training.json() status = body.get("training_status") or body.get("status") print(status, body.get("stage")) if status in ("succeeded", "failed"): break time.sleep(10) if (body.get("training_status") or body.get("status")) == "failed": raise SystemExit(body.get("error", "Training failed")) enforced = 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", "mode": "sync", "validation_scope": {"imports": [IMPORT_ID]}, }, timeout=30, ) enforced.raise_for_status() job = enforced.json() if job.get("mode") == "async": poll = job["poll"]["url"] while True: job = requests.get(f"{API}{poll}", headers=HEADERS, timeout=30).json() if job.get("status") in ("completed", "failed"): break time.sleep(2) print(job.get("status"), (job.get("result") or job).get("overall_status"), job.get("served_by_adapter")) ``` ```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 IMPORT_ID = process.env.IMPORT_ID!; const URL = `${API}/api/policies/import/${IMPORT_ID}/train`; const started = await fetch(URL, { method: "POST", headers: HEADERS, body: JSON.stringify({ cost_cap_usd: 25, examples_per_rule: 8 }), }); if (!started.ok) throw new Error(`train ${started.status}`); console.log(await started.json()); let body: { status?: string; training_status?: string; stage?: string; error?: string | null; }; for (;;) { const r = await fetch(URL, { headers: HEADERS }); if (!r.ok) throw new Error(`status ${r.status}`); body = await r.json(); const status = body.training_status ?? body.status; console.log(status, body.stage); if (status === "succeeded" || status === "failed") break; await new Promise((res) => setTimeout(res, 10_000)); } if ((body.training_status ?? body.status) === "failed") { throw new Error(body.error ?? "Training failed"); } const enforced = 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", mode: "sync", validation_scope: { imports: [IMPORT_ID] }, }), }); if (!enforced.ok) throw new Error(`enforce ${enforced.status}`); let job = await enforced.json(); if (job.mode === "async") { const pollUrl = job.poll.url; for (;;) { const polled = await fetch(`${API}${pollUrl}`, { headers: HEADERS }); job = await polled.json(); if (["completed", "failed"].includes(job.status)) break; await new Promise((res) => setTimeout(res, 2000)); } } const verdict = job.result ?? job; console.log(job.status, verdict.overall_status, job.served_by_adapter); ``` ## Errors | Status | Cause | Fix | | - | - | - | | `400` | Invalid training options | Check `cost_cap_usd` and `examples_per_rule`. | | `403` | Read-only key, or import belongs to another customer | Use a full-access key for the owning account. | | `404` | Import not found, or no run started (GET) | Confirm `IMPORT_ID`; POST before you poll. | | `409` | Import not activated, or a run already in progress | Activate rules; wait for the current run. | | `410` | Original document expired | Re-import, activate, then train ([Data Retention](/data-retention)). | | `422` | Document too short or rejected | Provide a fuller policy document. | | `502` | Training Studio request failed | Retry; contact support if it persists. | | `503` | Training not configured | Contact [support@zerodrift.ai](mailto:support@zerodrift.ai). | ## Gotchas * One training run per import at a time. A second POST while a run is active returns `409`. * Retrain after you materially change the policy (re-sync or re-import, activate, train again). * Adapters apply when enforcement includes that import in `validation_scope.imports`. * Live GET responses report `training_status` (`queued`, `succeeded`, `failed`). Some responses also include `status`. * A failed retrain does not replace an earlier working adapter. ## FAQ No. [Activate Rules](/api-reference/custom-policies/activate-rules) puts extracted rules into enforcement immediately. Untrained custom rules run on `anchor_3_0_max`. Training Studio adds a policy-specific adapter so Flagship (`anchor_3_0`) can check that import. See [Enforce Content](/api-reference/validate/validate-content). At launch, Training Studio adapters are Flagship-only. Max has no training step — upload the policy and it is evaluated at inference. Training and inference rates are on [Pricing](/pricing). Model choice is on [Choose a model](/models); quality and latency are on the [Anchor 3.0 benchmark](https://zerodrift.com/model/anchor-3-benchmark). Yes. A connected-source import is the same `import_id` as a file upload. Activate it, then call the train endpoints. See [MCP Connectors](/connecting-mcp). Training needs the original imported file or text. After default [content retention](/data-retention) (7 days unless your contract says otherwise), that document is deleted. Import again, activate the new rules, then train. No training run has been started for that import. POST first, then poll. ## Next Get an activated `IMPORT_ID` before you train. POST `/api/policies/import/{import_id}/train` Flagship training rates and per-enforcement custom-rule billing. # Introduction Source: https://docs.zerodrift.com/index ZeroDrift Enforcement API - Enforce regulations and policies on AI content and communications ## Welcome to ZeroDrift ZeroDrift is the Enforcement Runtime for AI. The Enforcement API checks content and communications against regulations, company policies, and security controls, and returns a verdict: pass, fix, block, or escalate. Enforce your first piece of content in minutes. Pick Mini, Flagship (`anchor_3_0`), or Max on the one enforce endpoint. Precision, recall, F1, and latency for Anchor 3.0. Produce a typed Python or TypeScript client from the public OpenAPI spec. Connect Notion, Linear, Confluence, or Google Drive and turn a source document into an enforceable policy. Import a custom policy, train a policy-specific adapter, and enforce against it. Per-enforcement rates for Mini, Flagship, and Max, including custom rules and training. ## What you can enforce * [Emails and chat messages](/api-reference/validate/validate-content) * Marketing and client communications * LLM and agent outputs # Choose a model Source: https://docs.zerodrift.com/models When to use Anchor 3.0 Mini, Flagship, or Max on the one enforce endpoint. All three models share [Enforce Content](/api-reference/validate/validate-content) (`POST /api/v3/content/validate`). You pick a tier with `model_engine` in the request body. There is no separate Mini or Max route. The public name for the default tier is **Flagship**. The body value is `anchor_3_0`. Older internal notes called this **Base**; that name is not used in these docs. Pass `anchor_3_0` whenever another document says Base or Anchor 3.0. | Public name | `model_engine` | Use it when | | - | - | - | | Mini | `anchor_3_0_mini` | You need a fast whole-message verdict against Mini's frozen rulebook. No scope or rewrite. | | Flagship | `anchor_3_0` | You need per-line violations and a rewrite against managed rules or a trained adapter. This is the default. | | Max | `anchor_3_0_max` | You need per-line violations against an uploaded policy evaluated at inference, with nothing to train. No rewrite. | ## Mini `model_engine: "anchor_3_0_mini"` Detection only. The response has `result.flagged_rules` plus `result.any_violation_detected` and `result.any_violation_p`. There are no per-line `violations`, no `fix.suggested_text`, and no rewrite. Mini always runs unscoped against its frozen rulebook. It cannot honor workspace pack toggles or switched-off rules. Sending `validation_scope` returns `422 unsupported_scope_for_model`; scope a Flagship or Max call instead. **When to use.** High-volume pre-checks, a first-pass flag before a Flagship or Max call, or any path that only needs "is this clean?" Mini skips the rewrite pass, so it returns sooner than Flagship or Max on the same input. **When not to use.** You need a compliant rewrite, per-line quotes, or a scoped rule set. ## Flagship `model_engine: "anchor_3_0"` (default) Per-line violations plus rewrite. With no `validation_scope`, Flagship stays on Flagship and checks the workspace's active managed catalog only. Activated custom rules are not evaluated by that scope-less call. To evaluate an uploaded policy on Flagship, train an [adapter](/guides/train-an-adapter) and scope the request to its import. Training Studio is Flagship-only at launch. **When to use.** Chat, email, and agent replies where you will apply `fix.suggested_text` or block. Start here unless you have a reason to pick Mini or Max. **When not to use.** You only need a detection flag (use Mini), or you want uploaded policies evaluated at inference on Max. ## Max `model_engine: "anchor_3_0_max"` Per-line violations in the same shape as Flagship, but no rewrite. Max evaluates the workspace's active managed and custom rules at inference. There is no training step and no Training Studio charge. Ask for Max explicitly; merely activating custom rules does not move a scope-less Flagship request to Max. Max surfaces a rule's own fix note when that rule has one, so `fix.suggested_text` is empty for most rules and custom rules never return a rewritten line. Escalate on an empty `suggested_text`, or run the same content through Flagship for a rewrite. To run Max against a policy you uploaded: 1. [Import](/guides/import-a-policy) the document and wait for extraction. 2. [Activate](/api-reference/custom-policies/activate-rules) the rules. 3. Call enforce with `model_engine: "anchor_3_0_max"` and the import id in `validation_scope.imports`. **When to use.** Custom policies you do not want to train, or a mix of managed rulepacks and uploaded rule text on one call. **When not to use.** You only need detection (use Mini), you need a compliant rewrite, or you already have a Flagship adapter and want that adapter's judgment (use Flagship). ## Latency Typical Flagship (Anchor 3.0) latency is 1.63 seconds. Mini is detection only, so it skips the rewrite pass and returns sooner. Flagship runs detection and then rewrite; Max detects without a rewrite pass. Quality, precision, recall, F1, and latency are on the [Anchor 3.0 benchmark](https://zerodrift.com/model/anchor-3-benchmark). These are not SLOs; long content, cold starts, and fallback can take longer. Use `mode: "sync"` for short content and `mode: "async"` when you expect to wait. ## Request shape Same endpoint, same fields, different `model_engine`: ```json theme={null} { "content": "Our fund guarantees 20% returns with zero risk!", "mode": "sync", "model_engine": "anchor_3_0" } ``` Replace `anchor_3_0` with `anchor_3_0_mini` or `anchor_3_0_max`. Read the verdict from `result` on a completed job. See [Act on Each Verdict](/guides/act-on-verdicts) for Pass / Fix / Block / Escalate. To evaluate one custom rule on Max, pass its workspace-specific `cust_*` ID in `validation_scope.rules`: ```json theme={null} { "content": "A. As your financial advisor, I am no longer in charge of the requirements for your investment account if you lose money that is completely on you.", "mode": "sync", "model_engine": "anchor_3_0_max", "validation_scope": { "rules": ["cust_abc123_require_fiduciary_duties"] } } ``` Get the rule ID from [List Rules](/api-reference/custom-policies/list-rules). See [Enforce Content](/api-reference/validate/validate-content#example) for runnable cURL, Python, and JavaScript examples. Mini responses omit per-line `violations` and rewrite fields, and Max responses carry violations without a rewrite. Handlers that read `violations[].fix.suggested_text` must branch on those tiers or skip them. Public responses report billed `result.units` and `result.cost`; they do not return raw input/output token counts. ## Next * [Enforce Content](/api-reference/validate/validate-content) — request and response fields * [Anchor 3.0 benchmark](https://zerodrift.com/model/anchor-3-benchmark) — quality and latency * [Pricing](/pricing) — per-validation rates for Mini, Flagship, and Max * [Quickstart](/quickstart) — first call * [Train an Adapter](/guides/train-an-adapter) — optional Flagship adapter * [Import a Policy](/guides/import-a-policy) — custom rules on Flagship or Max # Pricing Source: https://docs.zerodrift.com/pricing What ZeroDrift bills for: validations, custom rules, and Training Studio, across Anchor 3.0 Mini, Flagship, and Max. ZeroDrift bills three things, and nothing else. You pay per **validation** at your model's rate, you pay for **custom rules** in groups of 10, and on Flagship you pay once for **Training Studio**. New accounts start with **\$10** in free credits. A validation is one enforcement call on a message of up to 2,000 tokens, checked against your active rules. If the model rewrites, the rewrite and its verification are part of that same validation. ## What you're billed for Per validation, at your model's rate. On Flagship, rewrite is included, never a separate charge. Every 10 rules applied to a message counts as one validation at that same rate. Optional and one-time, per rule trained. Flagship only — Max has nothing to train. | Resource | How it's billed | Mini | Flagship | Max | | - | - | - | - | - | | **Validation** | Per message, up to 2,000 tokens | \$0.0020 | \$0.0100 | \$0.0500 | | **Rewrite** | Included in the validation | Not available | Included | Not available | | **Custom rules** | Per 10 rules applied to a message | — | \$0.0100 | \$0.0500 | | **Training Studio** | Optional. One time, per rule trained or retrained | Not available | First 5 free, then \$10 | Not applicable | | **Policy upload** | — | Free | Free | Free | ## Validation | Model | Per Validation | Input / 1M tokens | Output / 1M tokens | Cost per 2M tokens (input/output) | Cost per 1,000 emails | | - | - | - | - | - | - | | **Anchor 3.0 Mini** | \$0.0020 | 1.00 | 0.00 | 1.00 | 2.00 | | **Anchor 3.0** (Flagship) | \$0.0100 | 5.00 | 0.00 | 5.00 | 10.00 | | **Anchor 3.0 Max** | \$0.0500 | 25.00 | 0.00 | 25.00 | 50.00 | Mini and Max are detection-only, so they never rewrite. Flagship includes rewrite at no extra charge. Output tokens are not billed separately. See [Choose a model](/models) for when to use each tier, and the [Anchor 3.0 benchmark](https://zerodrift.com/model/anchor-3-benchmark) for quality and latency. ## Custom rules Custom rules are billed in groups of 10, as validations at your model's rate. The group charge is the cost of the message, not an addition to it: a message with 1 to 10 rules applied costs exactly one validation. | Rules applied to a message | Billed as | Flagship | Max | | - | - | - | - | | 1–10 | 1 validation | \$0.01 | \$0.05 | | 11–20 | 2 validations | \$0.02 | \$0.10 | | 21–30 | 3 validations | \$0.03 | \$0.15 | This price is the same however the rules got there. Untrained custom rules run on **Max** (`anchor_3_0_max`). Training Studio is optional and lets Flagship check that import — see [Enforce Content](/api-reference/validate/validate-content) and [Train an Adapter](/guides/train-an-adapter). Either way, every rule runs against every line, which is why the cost lands on the validation call. ## Training Studio First **5** rules trained are free. Each rule after that is **\$10** to train or retrain, billed once for that run. Nothing to train. Upload your policy document and Max evaluates it at inference, free and unlimited. Training is optional. Activated rules enforce without it, and training does not change what a validation costs — see [Train an Adapter](/guides/train-an-adapter) for when an adapter is worth it. At launch, adapters are available only for Flagship, and training is a one-time charge per run: running those rules afterward still bills as custom-rule validations above. ## Examples Each example separates the three charges: what a validation costs, what custom rules add, and what Training Studio costs up front. ### Flagship | Charge | Price | Notes | | - | - | - | | Validation | **\$0.01** | Per message, up to 2,000 tokens | | Validation + rewrite | **\$0.01** | Rewrite is included, so the price does not change | | Validation + custom rules | **\$0.01** per 10 rules applied | Same tiers as [Custom rules](#custom-rules) above, trained or not | | Training Studio | **\$10** per rule trained or retrained | Optional. First 5 rules free. One time, not per message | ### Max | Charge | Price | Notes | | - | - | - | | Validation | **\$0.05** | Per message, up to 2,000 tokens | | Validation + custom rules (prompt) | **\$0.05** per 10 rules applied | Same tiers as [Custom rules](#custom-rules) above | | Training Studio | **Free** | Nothing to train. Uploading policies to Max costs nothing | ### A 12-rule policy, side by side | | Flagship | Max | | - | - | - | | Training Studio, one time (optional) | **\$70** for the 7 chargeable rules | Free — nothing to train | | Per message, 12 rules applied | 2 validations, **\$0.02** | 2 validations, **\$0.10** | Max costs nothing to set up. Flagship costs less per message either way — skip training and those 12 rules still enforce at the same two-validation price, so training buys an adapter's judgment, not the ability to run the rules. ## Next When to use Mini, Flagship, or Max. Precision, recall, F1, and latency. Flagship-only Training Studio flow. Upload a policy for Flagship training or Max inference. # Quickstart Source: https://docs.zerodrift.com/quickstart Send your first piece of content through the Enforcement API in minutes ## Get started in three steps Every result is a verdict. Pass. Fix. Block. Escalate. Enforce your first piece of content with the ZeroDrift Enforcement API. ### Step 1: Get your API key API keys live in ZeroDrift Command. Sign in, open **Settings**, then under **API** select **Keys**. Command Settings → API → Keys empty state with Create first key 1. Select **Create first key** (or **New API key** if you already have keys). 2. In the **Create API key** dialog, enter a **Key name**, choose **Permissions** (Full access or Read only), and set **Rulepacks**. 3. Select **Create**, then **Copy** the key immediately — Command shows the full value only once. Export it once and every sample on this site works as written: ```bash theme={null} export ZERODRIFT_API_KEY="zd_..." ``` The key goes in the `x-api-key` header on all API requests. ### Step 2: Send content for enforcement The quickest way to test the API is with a text snippet: ```bash 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!", "mode": "async", "model_engine": "anchor_3_0" }' ``` You'll receive a `job_id` in the response: ```json theme={null} { "api_version": "v3", "job_id": "abc123def456", "status": "queued", "mode": "async", "model_engine": "anchor_3_0", "poll": { "method": "GET", "url": "/api/v3/jobs/abc123def456" } } ``` `mode` is required. Enforcement runs on Anchor. `"model_engine": "anchor_3_0"` is Flagship (the default; older notes called this Base). Mini is `anchor_3_0_mini`; Max is `anchor_3_0_max`. Custom rules require Max — see [Enforce Content](/api-reference/validate/validate-content). See [Choose a model](/models). Use `"mode": "async"` for a job you poll; for short content, `"mode": "sync"` returns the completed verdict in a single response. The same endpoint can enforce policies in other regulated industries: ```bash 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": "Send Jane Doe'\''s diagnosis and treatment plan to the entire partner mailing list.", "mode": "sync", "model_engine": "anchor_3_0" }' ``` ### Step 3: Get the results Use the `job_id` to retrieve the results: ```bash theme={null} curl "https://api.zerodrift.ai/api/v3/jobs/abc123def456" \ -H "x-api-key: $ZERODRIFT_API_KEY" ``` Poll until `status` is `completed` or `failed`. A completed response includes: ```json theme={null} { "api_version": "v3", "job_id": "abc123def456", "status": "completed", "mode": "async", "submitted_at": "2026-09-23T04:44:13.879781+00:00", "model_engine": "anchor_3_0", "result": { "overall_status": "needs_changes", "summary": { "do_not_send": 0, "send_with_caution": 1, "document_pages": 1 }, "revalidation_recommended": false, "units": 1, "cost": { "currency": "USD", "amount": "0.01" }, "violations": [ { "rule_id": "guarantee_of_returns", "signal_name": "Guarantees of returns", "rule_type": "SLM_SIGNAL", "severity": "send_with_caution", "confidence": "high", "quote": "Our fund guarantees 20% returns with zero risk!", "fix": { "action": "replace", "suggested_text": "Returns are not guaranteed and involve risk." } } ] }, "extracted_text": null } ``` Read `result.overall_status`: `approved` is Pass, `needs_changes` is Fix, and `do not send` is Block. Anything you would not apply automatically, escalate to a human. See [Get Verdict](/api-reference/validate/get-results). ## Next steps Explore the full API capabilities: What teams enforce with the API, and which guide to follow for each. Verdicts, rulepacks, policies, and adapters in one page. Full request and response details for the enforcement endpoint used above. Browse the managed rule pack catalog. Import a policy, train a policy-specific adapter, and enforce against it. Produce a typed Python or TypeScript client from the OpenAPI spec. Import a policy from Notion, Linear, Confluence, or Google Drive instead of uploading a file. **Need help?** Contact us at **[support@zerodrift.ai](mailto:support@zerodrift.ai)** # Security Source: https://docs.zerodrift.com/security Certifications, deployment, data handling, and how to request reports. ## Certifications and compliance ZeroDrift is SOC 2 Type II and ISO 27001 certified, and supports customers' GDPR and HIPAA compliance requirements. ## Deployment ZeroDrift supports multi-tenant cloud and private-cloud deployments. Customer- managed VPC and on-premises deployments are not generally available. Contact [support@zerodrift.ai](mailto:support@zerodrift.ai) to discuss deployment and data-residency requirements. ## Data handling * Content and verdict retention windows are documented in [Data Retention](/data-retention). * Customer data is not used to train ZeroDrift or third-party AI models. * Connected Notion, Linear, Confluence, and Google Drive sources are read-only. ## Access * API keys are shown once when created. * Keys can have read-only or full-access permissions. * Rulepacks can be scoped per key. ## Reports and subprocessors Review ZeroDrift's security posture, request SOC 2 and ISO 27001 reports, and see the current subprocessor list in the [ZeroDrift Trust Center](https://app.vanta.com/zerodrift.ai/trust/8dwp517ay48r0q3abhwy09). # Use Cases Source: https://docs.zerodrift.com/use-cases What teams enforce with the ZeroDrift Enforcement API today. Each use case starts with `POST /api/v3/content/validate`. Sync returns the verdict in that response. Async returns a `job_id`; poll [Get Verdict](/api-reference/validate/get-results) until `status` is `completed` or `failed`. Stop on `failed` and handle the error — do not keep polling. ## Customer support chatbots and virtual assistants Every reply your chatbot generates is enforced before the customer sees it. A promise your policy does not allow, a missing disclosure, or a regulated claim gets fixed or blocked in the response path. **Send:** the model's reply, `mode: "sync"`. **Get back:** Pass, a compliant fix, or Block. Start with [Quickstart](/quickstart). ## AI-drafted client communications Advisors, bankers, insurance agents, and account reps draft email and chat with AI. Enforce the draft before it sends, inside your own app or CRM. (In Gmail, Outlook, Slack, and Teams, Guard for People does this with no code.) **Send:** the draft. **Get back:** the verdict and a fix per finding. See the handler in [Quickstart](/quickstart). ## Marketing and content generation Campaign copy, social posts, landing pages, and newsletters generated by AI, enforced before publish against FINRA 2210, the SEC Marketing Rule, UDAAP, NAIC advertising rules, TCPA, CAN-SPAM, and the rest of the catalog. **Send:** the asset text, `mode: "async"` for long pieces. **Get back:** findings to poll. See [Get Verdict](/api-reference/validate/get-results). ## Autonomous agents and multi-agent workflows Every message an agent sends on your behalf, and every message passed between agents, so a violation introduced in step 2 never reaches the customer in step 5. Enforce each hop with the same validate call; Block means halt the run. **Send:** each outbound or inter-agent message. **Get back:** the verdict. Start with [Quickstart](/quickstart). ## AI-generated documents Reports, letters, proposals, disclosures, and statements. Submit the document as text and poll for the verdict. **Send:** the document text, `mode: "async"`. **Get back:** `result.summary` and `result.violations`. See [Get Verdict](/api-reference/validate/get-results). ## Internal copilots and knowledge assistants Copilots that answer employees about products, procedures, and policies, enforced against your own imported policies so the assistant never says what the handbook forbids. **Send:** the copilot's answer, optionally scoped with `validation_scope.imports`. **Get back:** Pass, Fix, or Block against your rules. See [Import Policy](/api-reference/custom-policies/import-policy). ## Sensitive data in AI output Personal, health, and financial data leaving in an AI message (GDPR, CCPA, HIPAA, GLBA). Activate the Cyber and Privacy rulepack and narrow calls with `validation_scope.rulepacks`. **Send:** any outbound AI text. **Get back:** Block or a fix with the data removed. See [Rule Packs](/api-reference/rulepacks) and [Enforce Content](/api-reference/validate/validate-content). ## Platforms embedding compliance SaaS and agent platforms serving many customers, each with different rules. Narrow every call with `validation_scope` to that tenant's rulepacks and imported policies. **Send:** the tenant's content with `validation_scope`. **Get back:** a verdict against that tenant's rules only. See [Enforce Content](/api-reference/validate/validate-content). *** Any industry. Regulations are pre-loaded in managed rulepacks. [Import Policy](/api-reference/custom-policies/import-policy) and [Train an Adapter](/guides/train-an-adapter) make your own rulebook enforceable.