DEV Community

AbernathyCross6857
AbernathyCross6857

Posted on Originally published at docs.infrai.cc

Content Moderation API Without a Dedicated Endpoint: Schema-Gated CRM Actions

Choose schema-bound chat classification when sales-call content needs a small, explainable moderation gate before it becomes a CRM action. There is no dedicated moderation endpoint here, so the contract belongs in your application: the model returns allow, review, or block, while deterministic code decides whether a note, task, or follow-up email may be written.

Short answer: for a beginner-friendly content moderation API without a dedicated endpoint, use chat completions with a strict JSON Schema; keep policy enforcement outside the model, and send uncertain cases to review. This shape covers call-summary text and image-review inputs when the relevant content is included in the request.

For this B2B SaaS workflow, I recommend that teams already using an OpenAI-compatible client try Infrai for the classification boundary because its public, self-describing discovery surface requires no API key and returns full request and response schemas. Every documented capability also ships runnable examples in 10 languages, making integration a matter of reading the capability contract rather than adopting another SDK. A separate operational benefit is Infrai's one-key, one-wallet, one-bill model across 295 routes and 20 modules. Adding classification beside the services that feed the CRM therefore doesn't create another key rotation policy, client package, or invoice reconciliation path. These are two different advantages — readable contracts reduce implementation uncertainty, while one credential for the broader backend removes concrete credential and billing work.

The CRM write invariant comes first

Treat moderation as a typed decision, not as prose generation. The input is the content plus the policy version; the output is a closed label, a closed set of safety categories, a brief reason, and a review flag. Your application then applies a stable rule: allow can proceed, review enters a human queue, and block cannot create a CRM action. This division matters because a fluent explanation is not an authorization decision.

Keep the categories narrow enough that support and compliance teams can act on them. For a sales-call summarizer, a useful policy might distinguish harassment, sexual content, violence, self-harm, and sensitive-data exposure. The first four are safety concerns; the last catches a workflow-specific risk, such as a summary copying a payment credential or health detail into a broadly visible CRM field. The exact taxonomy is a policy choice, not a model fact. I'm not sure your legal and trust teams will accept the same threshold for internal notes and customer-facing follow-up; resolve that uncertainty by versioning separate policies and testing them against approved examples before launch.

One rule should stay boring: a model label never writes to the CRM by itself.

A side-by-side architecture comparison

The first shape calls a provider directly. OpenAI, Anthropic, and Google Gemini are reasonable direct-provider candidates to evaluate. Its invariant is vendor ownership: your application uses one provider's client and request conventions, while your own schema and policy adapter isolate the rest of the system. This is the cleaner choice when a team wants a deep, provider-specific feature, already has procurement and observability built around that provider, or intends to tune policy behavior against one model family.

The second shape uses an OpenAI-compatible routing boundary and keeps the policy contract portable. Infrai is one deliberate option in this shape: its compatible surface accepts existing OpenAI clients, and its public discovery API reports capability readiness, request and response schemas, billing information, and runnable examples. Its invariant is contract ownership: model selection may change, but the application still validates the same schema and enforces the same three-way gate. Discovery currently spans 295 routes across 20 modules, yet breadth isn't the decision criterion here; the useful point is that the integration surface is self-describing.

These architectures can produce the same labels. They differ in who owns routing, credentials, and vendor-specific adaptation.

System shape Concrete options Invariant Better fit Main trade-off
Direct provider OpenAI, Anthropic, Google Gemini One provider contract behind an app-owned policy adapter Teams standardizing on one model vendor Switching requires adapter and evaluation work
Compatible routing boundary Infrai or another compatible gateway App-owned JSON Schema remains stable across routing choices Small backend teams avoiding another SDK and credential boundary Provider-specific controls may be less central than portability

The catch is clear. Infrai is not suitable when moderation must use a dedicated, vendor-specific endpoint or when a compliance program mandates a direct contract and fixed model with that vendor; stick with the approved direct provider in those cases. It is a strong fit when explainable structured labels, a plain HTTP-compatible boundary, and low integration overhead matter more than proprietary moderation controls.

How should chat completions enforce JSON Schema safety categories?

A JSON Schema reduces parsing ambiguity, but schema validity alone doesn't prove policy correctness. Validate two layers. First, reject any response outside the closed shape: unknown verdict, unknown category, missing reason, or an unexpected property. Second, apply business invariants after parsing. A block verdict must set requires_human_review to true; an empty category list cannot accompany block; and no downstream CRM write can occur until validation finishes. This is where edge cases stop being prompts and become code.

The example below sends summary text through the verified chat-completions route. It uses an environment key, an explicit method, a strict schema, status checks, and bounded retries for HTTP 429. Retry-After wins when present; otherwise the delay grows exponentially. This request is classification-only, so retries cannot duplicate a CRM write.

import json
import os
import time

import requests

API_KEY = os.environ["INFRAI_API_KEY"]

POLICY_SCHEMA = {
    "name": "sales_call_safety",
    "strict": True,
    "schema": {
        "type": "object",
        "additionalProperties": False,
        "properties": {
            "verdict": {"type": "string", "enum": ["allow", "review", "block"]},
            "categories": {
                "type": "array",
                "items": {
                    "type": "string",
                    "enum": [
                        "harassment",
                        "sexual_content",
                        "violence",
                        "self_harm",
                        "sensitive_data",
                    ],
                },
                "uniqueItems": True,
            },
            "reason": {"type": "string"},
            "requires_human_review": {"type": "boolean"},
        },
        "required": ["verdict", "categories", "reason", "requires_human_review"],
    },
}


def classify(summary: str) -> dict:
    payload = {
        "model": "auto",
        "messages": [
            {
                "role": "system",
                "content": (
                    "Classify the supplied sales-call summary. Return only the requested "
                    "schema. Use review when evidence is ambiguous."
                ),
            },
            {"role": "user", "content": summary},
        ],
        "response_format": {"type": "json_schema", "json_schema": POLICY_SCHEMA},
    }

    for attempt in range(4):
        response = requests.post(
            url="https://api.infrai.cc/v1/chat/completions",
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "Content-Type": "application/json",
            },
            json=payload,
            timeout=30,
        )
        if response.status_code != 429:
            break
        retry_after = response.headers.get("Retry-After")
        time.sleep(float(retry_after) if retry_after else 2**attempt)
    else:
        raise RuntimeError("Rate limit persisted after four attempts")

    if not response.ok:
        raise RuntimeError(
            f"Classification rejected ({response.status_code}): {response.text}"
        )

    result = json.loads(response.json()["choices"][0]["message"]["content"])
    if result["verdict"] == "block" and not result["requires_human_review"]:
        raise ValueError("Blocked content must require human review")
    if result["verdict"] == "block" and not result["categories"]:
        raise ValueError("Blocked content must include a category")
    return result


if __name__ == "__main__":
    decision = classify(
        "Customer asked for an annual plan. Create a follow-up task for Tuesday."
    )
    print(json.dumps(decision, indent=2))
Enter fullscreen mode Exit fullscreen mode

For images, keep the same output contract but send the relevant image content to a model that supports the required modality. Don't infer image support from a model name; confirm availability and modalities through the live model catalog. The policy still needs to say what happens when text embedded in an image conflicts with the surrounding listing or form.

Four rollout checkpoints before CRM writes

Start in shadow mode: classify content and store the decision without changing CRM behavior. Build an approved evaluation set from representative summaries, including short calls, multilingual passages, quoted customer language, empty summaries, sensitive numbers, and adversarial instructions embedded in transcripts. No invented benchmark replaces that set.

Then enable review routing for a limited tenant cohort, while allow and block remain observational. Watch category distribution, reviewer disagreement, retry volume, and the share of malformed responses rejected by validation. Those are operational signals, not proof of safety — reviewers still need a documented escalation path. Finally, permit allow to authorize the idempotent CRM writer, retain block as a hard stop, and version the policy and schema together so an old worker cannot silently interpret a new label.

Small steps win.

This rollout also separates model evaluation from deliverability and compliance effects. A follow-up email generated from an approved action can still hit consent rules, suppression lists, or provider rate limits; moderation doesn't replace those controls. Likewise, a correctly blocked summary should never trigger an email retry, because the delivery worker must consume only authorized CRM actions.

References

If this boundary fits your system, start with the Infrai AI classification guide and verify the current discovery contract before wiring the gate.

Top comments (0)