Developer Guides
·IdenticAPI

Prompt Injection Detection with TypeScript

Integrate prompt injection detection in TypeScript and Next.js — server-side screening, API calls, error handling, and where to place guards in your request flow.

Integrate prompt injection detection in TypeScript by calling IdenticAPI's Prompt Injection Shield from server-side code — before user messages, retrieved chunks, or agent fetch results reach your LLM. Client-side-only checks are bypassable; place guards in Next.js Route Handlers, Server Actions, or Node.js middleware that runs on every untrusted input path.

This guide covers types, fetch wrappers, error handling, verdict policies, and placement in a Next.js App Router chat flow.

API contract

POST /api/v1/security/prompt-injection
Authorization: Bearer idapi_test_your_key_here
Content-Type: application/json

{"text": "string (1–32,000 chars)", "source": "optional", "context": "optional"}

Response fields:

FieldTypeDescription
request_idstringCorrelation ID for logs
apistring"prompt-injection-shield"
verdict"safe" | "suspicious" | "unsafe"Primary decision input
riskstring"low", "medium", "high"
findingsarray{ category, reason, confidence?, start?, end? }
reasonsstring[]Summary strings
usage_unitsnumberBilling meter

Full reference: Prompt Injection Shield documentation. Product overview: Prompt Injection Shield.

Types and client module

Create lib/identicapi/prompt-injection.ts:

export type InjectionVerdict = "safe" | "suspicious" | "unsafe";

export type InjectionFinding = {
  category: string;
  reason: string;
  confidence?: number;
  start?: number;
  end?: number;
};

export type PromptInjectionResponse = {
  request_id: string;
  api: string;
  verdict: InjectionVerdict;
  risk: string;
  findings: InjectionFinding[];
  reasons: string[];
  usage_units: number;
  processing_time_ms?: number;
  detector_version?: string;
};

export class PromptInjectionApiError extends Error {
  constructor(
    message: string,
    public readonly status: number,
    public readonly code?: string
  ) {
    super(message);
    this.name = "PromptInjectionApiError";
  }
}

export type ScreenPromptOptions = {
  source?: string;
  context?: string;
  baseUrl?: string;
  apiKey?: string;
  signal?: AbortSignal;
};

const DEFAULT_BASE_URL =
  process.env.IDENTICAPI_BASE_URL ?? "https://www.identicapi.com";

export async function screenPromptInjection(
  text: string,
  options: ScreenPromptOptions = {}
): Promise<PromptInjectionResponse> {
  const apiKey = options.apiKey ?? process.env.IDENTICAPI_API_KEY;
  if (!apiKey) {
    throw new PromptInjectionApiError(
      "IDENTICAPI_API_KEY is not configured",
      500,
      "missing_api_key"
    );
  }

  if (!text || text.length > 32_000) {
    throw new PromptInjectionApiError(
      "text must be between 1 and 32,000 characters",
      400,
      "invalid_text_length"
    );
  }

  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
  const url = `${baseUrl}/api/v1/security/prompt-injection`;

  let response: Response;
  try {
    response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        text,
        ...(options.source ? { source: options.source } : {}),
        ...(options.context ? { context: options.context } : {})
      }),
      signal: options.signal
    });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Network request failed";
    throw new PromptInjectionApiError(message, 503, "network_error");
  }

  const body = (await response.json().catch(() => null)) as
    | (PromptInjectionResponse & { error?: { code?: string; message?: string } })
    | null;

  if (!response.ok || !body) {
    throw new PromptInjectionApiError(
      body?.error?.message ?? `Prompt injection API returned ${response.status}`,
      response.status,
      body?.error?.code
    );
  }

  return body;
}

Verdict policy helper

Map API verdicts to application actions:

export type GuardAction = "allow" | "review" | "block";

export function verdictToAction(verdict: InjectionVerdict): GuardAction {
  switch (verdict) {
    case "unsafe":
      return "block";
    case "suspicious":
      return "review";
    default:
      return "allow";
  }
}

Adjust suspicious handling per product: high-risk flows should treat review as block.

Next.js Route Handler example

app/api/chat/route.ts:

import { NextRequest, NextResponse } from "next/server";
import {
  screenPromptInjection,
  verdictToAction,
  PromptInjectionApiError
} from "@/lib/identicapi/prompt-injection";

export async function POST(request: NextRequest) {
  let message: string;
  try {
    const body = await request.json();
    message = typeof body.message === "string" ? body.message.trim() : "";
  } catch {
    return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
  }

  if (!message) {
    return NextResponse.json({ error: "message is required" }, { status: 400 });
  }

  let screen;
  try {
    screen = await screenPromptInjection(message, {
      source: "chat_input",
      context: "route=/api/chat"
    });
  } catch (err) {
    if (err instanceof PromptInjectionApiError) {
      // Fail closed for security-sensitive deployments; fail open only with explicit product decision
      console.error("injection_screen_failed", {
        status: err.status,
        code: err.code,
        message: err.message
      });
      return NextResponse.json(
        { error: "Security screening temporarily unavailable" },
        { status: 503 }
      );
    }
    throw err;
  }

  const action = verdictToAction(screen.verdict);

  console.info("prompt_injection_screen", {
    request_id: screen.request_id,
    verdict: screen.verdict,
    risk: screen.risk,
    categories: screen.findings.map((f) => f.category),
    action
  });

  if (action === "block") {
    return NextResponse.json(
      {
        error: "Message blocked by security policy",
        request_id: screen.request_id
      },
      { status: 400 }
    );
  }

  if (action === "review") {
    // Queue for human review or require rephrase — do not forward to LLM in strict mode
    return NextResponse.json(
      {
        error: "Message flagged for review",
        request_id: screen.request_id
      },
      { status: 422 }
    );
  }

  // action === "allow" — proceed to your LLM provider call
  const llmReply = await callYourLlmProvider(message);
  return NextResponse.json({ reply: llmReply, request_id: screen.request_id });
}

async function callYourLlmProvider(message: string): Promise<string> {
  // Replace with your provider integration
  return `Echo: ${message.slice(0, 100)}`;
}

Screening RAG chunks

Loop retrieved chunks before prompt assembly:

export async function filterSafeChunks(
  chunks: { id: string; text: string }[]
): Promise<{ id: string; text: string }[]> {
  const safe: { id: string; text: string }[] = [];

  for (const chunk of chunks) {
    const result = await screenPromptInjection(chunk.text, {
      source: "rag_chunk",
      context: `chunk_id=${chunk.id}`
    });

    if (verdictToAction(result.verdict) === "allow") {
      safe.push(chunk);
      continue;
    }

    console.warn("rag_chunk_blocked", {
      chunk_id: chunk.id,
      request_id: result.request_id,
      verdict: result.verdict,
      reasons: result.reasons
    });
  }

  return safe;
}

Parallelize with Promise.all only if rate limits allow — sequential is safer for quota control.

Environment variables

IDENTICAPI_API_KEY=idapi_test_your_key_here
IDENTICAPI_BASE_URL=https://www.identicapi.com   # optional override

Never expose API keys in client bundles (NEXT_PUBLIC_*).

Unit testing with mocks

import { describe, it, expect, vi } from "vitest";
import { verdictToAction } from "@/lib/identicapi/prompt-injection";

describe("verdictToAction", () => {
  it("blocks unsafe", () => {
    expect(verdictToAction("unsafe")).toBe("block");
  });
  it("reviews suspicious", () => {
    expect(verdictToAction("suspicious")).toBe("review");
  });
});

Add fixture integration tests per Prompt Injection Testing.

Where to place guards

LocationScreen
Chat Route HandlerUser message
Document upload APIExtracted full text
RAG serviceEach chunk at retrieve
Agent toolWeb fetch text

See Detect and Prevent guides for architecture.

Limitations

  • 32,000 character limit — split large documents
  • Latency — add timeout via AbortSignal; budget in UX
  • Fail-open vs fail-closed — API outages trade availability for security; document your choice
  • Not a jailbreak solution alone — add output moderation where needed (injection vs jailbreak)
  • Verdicts are risk signals — not guarantees of safe model behavior

Prototype payloads in Prompt Injection Checker.

Practical checklist

  • Implement server-side screenPromptInjection wrapper with typed responses
  • Map verdict to allow / review / block consistently
  • Log request_id, categories, and action — not raw secrets
  • Screen RAG chunks separately from user messages
  • Keep IDENTICAPI_API_KEY server-only
  • Handle API errors explicitly (503 fail-closed recommended)
  • Add CI fixture tests against live or mocked API
  • Review security checklist before production

TypeScript integration is straightforward: one API call per untrusted string, explicit verdict handling, and guards placed where text enters your server — not where it leaves the browser.

Frequently asked questions

Should detection run on the client or server?

Always server-side. Never expose API keys in browser code; call IdenticAPI from your backend or a Route Handler.

What HTTP status indicates a flagged prompt?

The API returns 200 with a verdict field. Your application decides whether to block based on safe, suspicious, or unsafe — and your own policy thresholds.

How do I handle API failures?

Fail closed for high-risk flows when possible: reject the request or queue for review rather than skipping screening silently.

Where is the official schema documented?

See /docs/prompt-injection-shield for request and response fields matching the production endpoint.

Related reading