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.
-
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 label API value Behaviour without key, tag or header override On demand default_offShield is off (default for new organisations) Active by default default_onShield inspects every supported request Enforced enforcedShield 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,offorblockonly, default:block, evaluated onuserandtoolsegments) - 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 withre: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)). - Secrets & API Keys (
-
Control Shield per request with X-Noirdoc-Shield
Pass
X-Noirdoc-Shield: onoroff(case-insensitive) to turn Shield on or off for a single request whenever the organisation is not set toenforced. The gateway strips the header before forwarding the request to the provider.On organisations running Noirdoc Shield,
X-Noirdoc-Maskcontinues to work as a legacy alias. On pre-existing organisations during the transition period,X-Noirdoc-Shield: onexplicitly selects Noirdoc Shield whileX-Noirdoc-Maskcontrols 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 alongsideX-Noirdoc-Masked: true.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" }] }'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()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")); -
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, setshield_modeper tag (see Request tags (Preview)). When multiple tags appear inX-Noirdoc-Tags, anyonwins overoff. - On an SDK client: pass
default_headers(Python) ordefaultHeaders(TypeScript) when constructing the client. Because Shield inspects/v1/embeddingsand/v1/rerankin addition to/v1/chat/completions,/v1/responsesand/v1/messages, a single client works across chat and retrieval pipelines. Use a separate client without the header for image generation (/v1/images/generations, whereX-Noirdoc-Shield: onreturns 403masking_not_supported_for_endpoint); audio and/v1/filesare not inspected by Shield.
Result: every supported call made with that key, tag or client is automatically inspected by Shield.
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", )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", }); - Per API key: under Models → API keys, set Noirdoc Shield (
-
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/inspectdirectly 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 OKeven whenactionis"block", providingaction("pass","redact"or"block"), the sanitisedoutputand every match infindings. You can pass an optional per-callpolicywithrulesandcustom_patterns– combining literal terms andre:regular expressions – provided it does not weaken anenforcedorganisation policy (403 shield_policy_enforced).Result: you obtain sanitised text and structured findings before handing data to external tools or RAG stores.
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}" ] } }'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"]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; -
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 withblock(by defaultsecretsorinjection) matched;error.messagenames the triggered rule. Do not retry unchanged – prompt the user or remove the triggering content.422 shield_uninspectable_content: the organisation policy isenforcedand 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_onor selective activation withoutenforced, uninspectable attachments are forwarded alongside inspected text, and the response carriesX-Noirdoc-Shield: partial; redacted=<n>withX-Noirdoc-Masked: false.Result: your application cleanly separates policy blocks from uninspectable attachment formats.
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: raiseimport 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:
- 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. - Direct streaming: with reverse substitution gone, sentence-level stream buffering disappears completely.
- 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). - Expanded endpoints & pricing: Shield also inspects
/v1/embeddingsand/v1/rerank, and is billed at +€0.40 per 1M inspected input tokens (including BYOK routes and403 shield_violationblocks).
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.
# 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. |