Developer Guides
·IdenticAPI

How to Add AI Security to a SaaS API Route

Add AI security to SaaS API routes — middleware control flow, input guards, model calls, output checks, and tenant-aware policies.

SaaS products expose AI features through HTTP API routes — chat completions, document Q&A, summarization, and agent workflows. Security belongs inside those routes, after authentication and tenant resolution, as explicit middleware control flow — not as an optional client-side check or a single bolt-on at the edge.

This guide maps middleware placement for multi-tenant SaaS API routes: input guards, model calls, output checks, failure modes, and tenant-aware policy. For the full SaaS architecture picture, see AI SaaS Guardrails. For middleware concepts, see LLM Security Middleware.

Request lifecycle control flow

Every AI-enabled route should follow a predictable sequence:

HTTP request
  → AuthN (session / API key)
  → AuthZ + tenant resolution
  → Schema validation + rate limits
  → Assemble LLM context (history, RAG, system vars)
  → INPUT GUARD (Unified Guard)
  → Branch on decision: block | review | allow
  → LLM / RAG / tools (if allowed)
  → OUTPUT GUARD (Unified Guard)
  → Branch on decision
  → Response + metadata audit log

Guardrails execute after you know tenant_id and before side effects. WAF and CDN rules complement but do not replace in-route guards.

Middleware layers

LayerResponsibilityFails if skipped
Edge (WAF, CDN)DDoS, coarse IP blocksApp-layer injection, PII
API gatewayAuth, rate limit, request sizeTenant-scoped policy
Route handlerAssemble context, call guardsWrong text scanned
Guard APIClassify riskN/A — this is the classifier
Policy routerMap verdict → actionInconsistent UX
Model clientInferenceN/A
Render/storageSanitize, persistXSS, data leaks

IdenticAPI is a caller-side guardrails API. Your middleware invokes POST /api/v1/guard; you retain direct model provider clients (middleware vs proxy).

Tenant-aware policy

Multi-tenant SaaS needs policy that varies by customer tier without forking code paths:

type TenantPolicy = {
  failClosed: boolean;
  inputChecks: ("prompt_injection" | "pii_secrets")[];
  outputChecks: ("output_safety" | "pii_secrets")[];
  redactPii: boolean;
  reviewWebhook?: string;
};

const policies: Record<string, TenantPolicy> = {
  enterprise: {
    failClosed: true,
    inputChecks: ["prompt_injection", "pii_secrets"],
    outputChecks: ["output_safety", "pii_secrets"],
    redactPii: true,
    reviewWebhook: "https://internal.example/hooks/review"
  },
  standard: {
    failClosed: false,
    inputChecks: ["prompt_injection", "pii_secrets"],
    outputChecks: ["output_safety"],
    redactPii: true
  }
};

Resolve policy from authenticated tenant record — never from client JSON bodies.

Enterprise tenants often require fail closed when guard APIs are unavailable (fail open vs fail closed).

Input middleware implementation

Express example

import type { Request, Response, NextFunction } from "express";

async function inputGuardMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const tenant = req.tenant; // set by prior auth middleware
  const policy = getPolicy(tenant.id);
  const assembled = buildPrompt(req.body, tenant);

  try {
    const guard = await callUnifiedGuard({
      text: assembled,
      checks: policy.inputChecks,
      redact: policy.redactPii
    });

    req.guardInput = {
      requestId: guard.request_id,
      decision: guard.decision,
      textForModel: extractRedactedText(guard) ?? assembled
    };

    if (guard.decision === "block") {
      return res.status(400).json({
        error: "request_blocked",
        message: SAFE_FALLBACK,
        request_id: guard.request_id
      });
    }
    if (guard.decision === "review") {
      await enqueueReview(tenant.id, guard.request_id);
      return res.status(202).json({
        error: "pending_review",
        message: REVIEW_HOLD_MESSAGE,
        request_id: guard.request_id
      });
    }
    next();
  } catch (err) {
    if (policy.failClosed) {
      return res.status(503).json({ error: "guard_unavailable" });
    }
    req.guardInput = { bypassed: true };
    next();
  }
}

Next.js Route Handler pattern

Place guards in Route Handlers or Server Actions — never in Client Components with exposed API keys (output safety in Next.js).

Sequence:

  1. POST /api/chat authenticates session
  2. Input guard middleware runs on assembled prompt
  3. Handler calls OpenAI/Anthropic with textForModel
  4. Output guard runs on completion
  5. Returns JSON to client

Output middleware

Mirror input flow after inference:

async function outputGuard(
  completion: string,
  tenantId: string
): Promise<{ text: string; blocked: boolean; requestId: string }> {
  const policy = getPolicy(tenantId);
  const guard = await callUnifiedGuard({
    text: completion,
    checks: policy.outputChecks
  });

  if (guard.decision === "block") {
    return { text: SAFE_FALLBACK, blocked: true, requestId: guard.request_id };
  }
  if (guard.decision === "review") {
    await enqueueReview(tenantId, guard.request_id);
    return { text: REVIEW_HOLD_MESSAGE, blocked: true, requestId: guard.request_id };
  }
  return { text: completion, blocked: false, requestId: guard.request_id };
}

Do not echo blocked completions in error payloads or client-visible fields.

RAG routes

Document Q&A routes add retrieve hooks before input guard:

retrieve → per-chunk scan → assemble → input guard → LLM → output guard

See secure RAG chatbot production. Authorization filters are mandatory at retrieve — shared indexes are a top SaaS failure mode.

Agent routes

Routes that execute tools need an additional gate:

model proposes tool → agent_action guard → execute or block

Submit tool proposals to Unified Guard with checks: ["agent_action"] and the agent_action payload (Agent Action Guard). Text guards do not validate SQL, email recipients, or delete operations.

Unified Guard call helper

type GuardCheck =
  | "prompt_injection"
  | "pii_secrets"
  | "output_safety"
  | "agent_action";

async function callUnifiedGuard(body: {
  text?: string;
  checks: GuardCheck[];
  redact?: boolean;
  agent_action?: Record<string, unknown>;
}) {
  const res = await fetch("https://www.identicapi.com/api/v1/guard", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.IDENTICAPI_API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(5000)
  });
  if (!res.ok) throw new Error(`Guard HTTP ${res.status}`);
  return res.json();
}

Reuse HTTP connections (keep-alive) in high-throughput routes to reduce latency (guardrails latency).

Error handling matrix

ConditionEnterprise policyStandard policy
decision: block400 + safe message400 + safe message
decision: review202 + queue202 or block per config
Guard timeout503 fail closed200 with monitoring alert
Guard 5xx503 fail closedAllow with alert (document risk)
Invalid API key (your bug)500 internal500 internal

Document policies in version-controlled config reviewed with security stakeholders.

Audit logging per route

Emit structured events after each guard call:

{
  "event": "ai_guard_input",
  "tenant_id": "tenant_acme",
  "user_id": "usr_123",
  "route": "POST /v1/chat",
  "decision": "allow",
  "request_id": "req_guard_abc",
  "checks": ["prompt_injection", "pii_secrets"],
  "finding_categories": [],
  "latency_ms": 42
}

Do not log assembled prompts or completions in production. Log request_id for support correlation (privacy-safe logging).

API key hygiene

SaaS customers may embed your API keys in their integrations. Protect your guard and model keys:

  • Server-side only — never bundle IdenticAPI keys in frontend JavaScript
  • Per-environment keys with rotation runbooks
  • Separate keys for CI vs production

Testing routes in CI

For each route, assert:

  • Blocked input never reaches mocked LLM client
  • Output guard replaces unsafe completions
  • Tenant policy selects correct checks array
  • Fail-closed tenant gets 503 on guard outage

See test AI guardrails in CI.

Summary

Add AI security to SaaS API routes with explicit middleware control flow: authenticate, resolve tenant, validate input, run Unified Guard on assembled context, branch on decision, call the model, run output guard, then respond. Vary policy by tenant tier, fail closed for enterprise when guards are unavailable, and keep authorization and rendering as separate layers.

Add guards to your API routes · AI SaaS guardrails architecture

Frequently asked questions

Where do AI guardrails belong in a SaaS API route?

After authentication and tenant resolution, before the LLM provider call (input guard), and again after inference before the HTTP response (output guard). Guards belong inside your API boundary, not only at the CDN or WAF edge.

How should multi-tenant SaaS vary guardrail policy?

Load policy from the authenticated tenant record — checks arrays, redact behavior, and fail-closed vs fail-open on guard outages. Never trust client-supplied tenant_id or policy fields without server verification.

What should happen when Unified Guard returns block?

Do not call the LLM on input block. Return a static safe error with request_id. On output block, replace the completion with a pre-approved fallback — never echo flagged model text in errors or logs visible to broad staff.

Do RAG API routes need different middleware?

Yes. Add per-chunk screening and authorization filters before assembling the prompt, then run the same input and output guard pattern as chat-only routes on the full assembled string.

How do agent-enabled SaaS routes differ?

Add a pre-execution gate: submit each proposed tool call to Unified Guard with checks [agent_action] and the agent_action payload before any side effect runs. Text guards do not validate SQL, email recipients, or delete operations.

Related reading