Develop / Guides

Configure & use Shield

How to set up Noirdoc Shield in the portal, control it per request, API key or tag, inspect text directly via /v1/shield/inspect and migrate from legacy masking.

Noirdoc Shield inspects inbound prompts statelessly across /v1/chat/completions, /v1/responses, /v1/messages, /v1/embeddings and /v1/rerank, as well as standalone via POST /v1/shield/inspect (+€0.40 per 1M inspected input tokens). For an overview of how the five protection filters and policy precedence work, see Noirdoc Shield.

  1. Configure the default setting and protection filters in the portal

    Admins of your organisation manage Noirdoc Shield under Models → Shield. First choose the organisation’s Default Setting (mode):

    Portal labelAPI valueBehaviour without key, tag or header override
    On demanddefault_offShield is off (default for new organisations)
    Active by defaultdefault_onShield inspects every supported request
    EnforcedenforcedShield is always active and cannot be turned off

    Below the mode selector, configure each of the five protection filters:

    • Secrets & API Keys (secrets, default: block, placeholder [SECRET])
    • Financial & Contact Data (pii, default: redact, placeholders such as [IBAN], [EMAIL], [PHONE])
    • Names & Addresses (ner, default: redact, placeholders [PERSON], [ADDRESS])
    • Prompt Injection (injection, off or block only, default: block, evaluated on user and tool segments)
    • Custom Blocklist (custom, default: redact, placeholder [CUSTOM]): up to 100 entries of 2 to 256 characters each. Plain strings such as "Projekt Helios" match case-insensitive literal substrings; entries prefixed with re: such as "re:AZ-[0-9]{4}-[0-9]{5}" compile as linear-time regular expressions (no look-around or backreferences, must not match the empty string).

    Result: your organisation has a baseline Shield policy and active protection filters. You can also manage these settings programmatically via /v1/shield/policy (requires Manage Shield policy via API on the API key, see Shield (/v1/shield)).

  2. Control Shield per request with X-Noirdoc-Shield

    Pass X-Noirdoc-Shield: on or off (case-insensitive) to turn Shield on or off for a single request whenever the organisation is not set to enforced. The gateway strips the header before forwarding the request to the provider.

    On organisations running Noirdoc Shield, X-Noirdoc-Mask continues to work as a legacy alias. On pre-existing organisations during the transition period, X-Noirdoc-Shield: on explicitly selects Noirdoc Shield while X-Noirdoc-Mask controls legacy masking.

    Result: the model receives redacted placeholders such as [PERSON] or [IBAN], and the response streams straight back with zero hold-back latency. In the response headers, X-Noirdoc-Shield: redacted=<n> reports the number of redactions alongside X-Noirdoc-Masked: true.

    Shell
    curl -si https://api.noirdoc.de/v1/chat/completions \
      -H "Authorization: Bearer $NOIRDOC_API_KEY" \
      -H "Content-Type: application/json" \
      -H "X-Noirdoc-Shield: on" \
      -d '{
        "model": "qwen3.8-27b",
        "messages": [{
          "role": "user",
          "content": "Anna Schmidt, DE89 3704 0044 0532 0130 00"
        }]
      }'
    Python
    import os
    from openai import OpenAI
    
    client = OpenAI(
        base_url="https://api.noirdoc.de/v1",
        api_key=os.environ["NOIRDOC_API_KEY"],
    )
    
    prompt = "Anna Schmidt, DE89 3704 0044 0532 0130 00"
    raw = client.chat.completions.with_raw_response.create(
        model="qwen3.8-27b",
        messages=[{"role": "user", "content": prompt}],
        extra_headers={"X-Noirdoc-Shield": "on"},
    )
    print(raw.headers.get("x-noirdoc-shield"))  # "redacted=2"
    completion = raw.parse()
    TypeScript
    import OpenAI from "openai";
    
    const client = new OpenAI({
      baseURL: "https://api.noirdoc.de/v1",
      apiKey: process.env.NOIRDOC_API_KEY,
    });
    
    const prompt = "Anna Schmidt, DE89 3704 0044 0532 0130 00";
    const { data, response } = await client.chat.completions
      .create(
        {
          model: "qwen3.8-27b",
          messages: [{ role: "user", content: prompt }],
        },
        { headers: { "X-Noirdoc-Shield": "on" } },
      )
      .withResponse();
    
    console.log(response.headers.get("x-noirdoc-shield"));
  3. Pin Shield per API key, request tag or SDK client

    Instead of passing a header on every call, you can bind Shield to a specific application, matter or client instance:

    • Per API key: under Models → API keys, set Noirdoc Shield (shield_mode) to On (on) or Off (off). Inherit (inherit) defers to request tags, headers and the organisation default.
    • Per request tag: under Models → Tags or via /v1/tags, set shield_mode per tag (see Request tags (Preview)). When multiple tags appear in X-Noirdoc-Tags, any on wins over off.
    • On an SDK client: pass default_headers (Python) or defaultHeaders (TypeScript) when constructing the client. Because Shield inspects /v1/embeddings and /v1/rerank in addition to /v1/chat/completions, /v1/responses and /v1/messages, a single client works across chat and retrieval pipelines. Use a separate client without the header for image generation (/v1/images/generations, where X-Noirdoc-Shield: on returns 403 masking_not_supported_for_endpoint); audio and /v1/files are not inspected by Shield.

    Result: every supported call made with that key, tag or client is automatically inspected by Shield.

    Python
    shield_client = OpenAI(
        base_url="https://api.noirdoc.de/v1",
        api_key=os.environ["NOIRDOC_API_KEY"],
        default_headers={"X-Noirdoc-Shield": "on"},
    )
    
    # Works across Chat, Responses, Messages, Embeddings and Rerank
    emb = shield_client.embeddings.create(
        model="text-embedding-3-small",
        input="Note on Anna Schmidt, file AZ-2026-00142",
    )
    TypeScript
    const shieldClient = new OpenAI({
      baseURL: "https://api.noirdoc.de/v1",
      apiKey: process.env.NOIRDOC_API_KEY,
      defaultHeaders: { "X-Noirdoc-Shield": "on" },
    });
    
    // Works across Chat, Responses, Messages, Embeddings and Rerank
    const emb = await shieldClient.embeddings.create({
      model: "text-embedding-3-small",
      input: "Note on Anna Schmidt, file AZ-2026-00142",
    });
  4. Inspect text standalone via POST /v1/shield/inspect

    Before storing user-supplied text in an external vector database, search index or third-party tool, call POST /v1/shield/inspect directly without invoking a model (1 to 64 non-empty strings, up to 256 KB UTF-8 in total, billed at €0.40 per 1M input tokens).

    The endpoint returns 200 OK even when action is "block", providing action ("pass", "redact" or "block"), the sanitised output and every match in findings. You can pass an optional per-call policy with rules and custom_patterns – combining literal terms and re: regular expressions – provided it does not weaken an enforced organisation policy (403 shield_policy_enforced).

    Result: you obtain sanitised text and structured findings before handing data to external tools or RAG stores.

    Shell
    curl https://api.noirdoc.de/v1/shield/inspect \
      -H "Authorization: Bearer $NOIRDOC_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "input": "Projekt Helios (AZ-2026-00418), Anna Schmidt",
        "role": "user",
        "policy": {
          "rules": { "custom": "redact" },
          "custom_patterns": [
            "Projekt Helios",
            "re:AZ-[0-9]{4}-[0-9]{5}"
          ]
        }
      }'
    Python
    import os
    import httpx
    
    key = os.environ["NOIRDOC_API_KEY"]
    patterns = ["Projekt Helios", "re:AZ-[0-9]{4}-[0-9]{5}"]
    
    resp = httpx.post(
        "https://api.noirdoc.de/v1/shield/inspect",
        headers={"Authorization": f"Bearer {key}"},
        json={
            "input": "Projekt Helios (AZ-2026-00418), Anna Schmidt",
            "role": "user",
            "policy": {
                "rules": {"custom": "redact"},
                "custom_patterns": patterns,
            },
        },
    )
    resp.raise_for_status()
    data = resp.json()
    if data["action"] == "block":
        raise RuntimeError("Input blocked by Shield")
    clean_text = data["output"]
    TypeScript
    const url = "https://api.noirdoc.de/v1/shield/inspect";
    const patterns = ["Projekt Helios", "re:AZ-[0-9]{4}-[0-9]{5}"];
    
    const resp = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.NOIRDOC_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        input: "Projekt Helios (AZ-2026-00418), Anna Schmidt",
        role: "user",
        policy: {
          rules: { custom: "redact" },
          custom_patterns: patterns,
        },
      }),
    });
    
    const data = await resp.json();
    if (data.action === "block") {
      throw new Error("Input blocked by Shield");
    }
    const cleanText: string = data.output;
  5. Handle blocks and uninspectable attachments in code

    On model endpoints, Noirdoc Shield raises two specific pre-forwarding errors:

    • 403 shield_violation: an active filter configured with block (by default secrets or injection) matched; error.message names the triggered rule. Do not retry unchanged – prompt the user or remove the triggering content.
    • 422 shield_uninspectable_content: the organisation policy is enforced and the request carries content that Shield cannot read as text, such as PDF, DOCX or XLSX files, images, audio, file_id/URL references or inline text of 4 MiB or larger. Extract text from binary documents client-side first and pass it as message text or an inline UTF-8 text attachment under 4 MiB (see Files).

    Under default_on or selective activation without enforced, uninspectable attachments are forwarded alongside inspected text, and the response carries X-Noirdoc-Shield: partial; redacted=<n> with X-Noirdoc-Masked: false.

    Result: your application cleanly separates policy blocks from uninspectable attachment formats.

    Python
    from openai import (
        PermissionDeniedError,
        UnprocessableEntityError,
    )
    
    try:
        response = client.chat.completions.create(
            model="qwen3.8-27b",
            messages=[{"role": "user", "content": user_prompt}],
            extra_headers={"X-Noirdoc-Shield": "on"},
        )
    except PermissionDeniedError as err:
        if getattr(err, "code", None) == "shield_violation":
            # Triggered rule is named in err.message
            handle_blocked_prompt(err.message)
        else:
            raise
    except UnprocessableEntityError as err:
        code = getattr(err, "code", None)
        if code == "shield_uninspectable_content":
            # Under enforced: extract text from PDF/Office first
            handle_uninspectable_attachment()
        else:
            raise
    TypeScript
    import OpenAI from "openai";
    
    try {
      const response = await client.chat.completions.create(
        {
          model: "qwen3.8-27b",
          messages: [{ role: "user", content: userPrompt }],
        },
        { headers: { "X-Noirdoc-Shield": "on" } },
      );
    } catch (err) {
      const code = err instanceof OpenAI.APIError ? err.code : null;
      if (code === "shield_violation") {
        handleBlockedPrompt((err as Error).message);
      } else if (code === "shield_uninspectable_content") {
        handleUninspectableAttachment();
      } else {
        throw err;
      }
    }

Migrating from legacy masking

Pre-existing organisations on legacy masking continue running unchanged during the transition period until 30 November 2026 (2026-11-30T23:59:59Z). Until your organisation switches, every legacy-masked response carries the RFC 8594 headers Deprecation: true and Sunset: Mon, 30 Nov 2026 23:59:59 GMT.

Review four areas before switching your organisation:

  1. One-way placeholders instead of reverse substitution: Noirdoc Shield replaces matches permanently with [PERSON], [IBAN] or [CUSTOM] rather than <<PERSON_1>>, and does not substitute original values back into responses or tool call arguments. If local tools need to resolve records, pass internal database IDs rather than raw personal names to the model.
  2. Direct streaming: with reverse substitution gone, sentence-level stream buffering disappears completely.
  3. File attachments: Shield inspects inline UTF-8 text attachments under 4 MiB directly. If you previously relied on Base64 PDF, DOCX, XLSX or OCR file analysis, extract text client-side before sending – especially before enabling enforced (422 shield_uninspectable_content).
  4. Expanded endpoints & pricing: Shield also inspects /v1/embeddings and /v1/rerank, and is billed at +€0.40 per 1M inspected input tokens (including BYOK routes and 403 shield_violation blocks).

To test Noirdoc Shield side by side during the transition without switching the entire organisation, send X-Noirdoc-Shield: on on individual requests, set shield_mode: "on" on a dedicated test key or tag, or call POST /v1/shield/inspect, while X-Noirdoc-Mask continues controlling legacy masking for existing traffic.

Once ready, admins can switch permanently in one click under Models → Shield (Switch to Noirdoc Shield now) or by writing PUT / PATCH /v1/shield/policy (legacy_mask_active: false). After 2026-11-30T23:59:59Z, all remaining legacy organisations automatically run on Noirdoc Shield.

Shell
# Test Noirdoc Shield on a single request
curl https://api.noirdoc.de/v1/chat/completions \
  -H "Authorization: Bearer $NOIRDOC_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Noirdoc-Shield: on" \
  -d '{
    "model": "qwen3.8-27b",
    "messages": [{
      "role": "user",
      "content": "Call Anna Schmidt back on +49 30 1234567."
    }]
  }'

Errors

Status Code Meaning
403 shield_violation Noirdoc Shield blocked the request before forwarding to the provider because an active protection filter configured with block matched; message names the triggered rule (secrets, pii, ner, injection or custom).
403 shield_policy_enforced The organisation's Shield policy is enforced, and the request to /v1/shield/inspect tries to weaken an active rule or omit custom patterns via policy.
403 masking_not_supported_for_endpoint The endpoint supports neither Noirdoc Shield nor legacy masking, yet the policy is enforced or the request sends X-Noirdoc-Mask: on (and on /v1/images/generations also X-Noirdoc-Shield: on). During the legacy masking transition until 30.11.2026, this also affects /v1/embeddings and /v1/rerank on pre-existing organisations that have not switched to Shield yet.
403 insufficient_key_permissions The API key lacks permission for /v1/tags (can_manage_tags) or /v1/shield/policy (can_manage_shield), a key pinned to shield_mode: on tries to weaken the Shield policy or a tag's shield_mode, or the tag requests broader models, providers or compliance rules than the calling key itself holds.
413 shield_input_too_large The text to inspect in this request exceeds the Shield service limit of 4 MiB of encoded text in a single call.
422 shield_uninspectable_content The Shield policy is enforced, and the request carries content that Shield cannot inspect as text (binary_document such as PDF or Office, media_not_inspectable such as images, audio or video, file_reference by ID or URL, file_too_large at 4 MiB or above, file_not_decodable or unrecognised_content). The gateway did not forward the request.
503 shield_unavailable The Noirdoc Shield inspection service is temporarily unreachable; the gateway never forwards a request that requires inspection uninspected (fail-closed). The response contains Retry-After with 5 seconds.