---
title: Privacy with Veil
description: Turn on Veil for a project so the AI Gateway finds personal details in prompts and masks, redacts, tokenises or blocks them before they leave for a provider.
section: AI
order: 3.5
updated: 2026-11-04
---

Veil is Dekka's engine for finding personal information in text. When a project turns it on, the AI Gateway sends the text of every request to Veil before any provider sees it, and applies the project's policy to what Veil finds. A Ghana Card PIN can be refused outright, a phone number masked to `024 *** 4567`, and an email address replaced with a placeholder that's put back in the model's reply. The provider receives only what the policy allows.

> [!NOTE]
> Veil is in development and runs in the gateway for pilot projects that turn it on. Today it finds structured details, such as Ghana Card PINs, phone and mobile money numbers, digital addresses, taxpayer numbers, email and IP addresses, payment cards and credentials. Names and free-text addresses aren't caught until its model ships, and it doesn't read images or audio. No detector finds everything, so treat Veil as a layer that reduces what leaves, alongside choosing which providers a project may use.

## Turn it on

Open **AI Gateway**, then **Routing**, choose the project, and find **Privacy with Veil** below the routing rules. Choose a mode and save.

| Mode | What happens |
| --- | --- |
| Off | Prompts reach the provider as your app sends them. This is the default. |
| Detect | Veil reports what it finds in the request log and the text goes unchanged. Use it to see what your prompts contain before you change anything. |
| Protect | Veil applies the policy before the text leaves. A request the policy blocks never reaches a provider. |

Veil scans every message in a request, including your system prompt and earlier replies, because a previous reply can contain values Veil restored. **Try it** runs sample text through your settings, saved or not, and shows what the provider would receive. Nothing you try is stored.

## Choose a policy

The built-in `default-protection` policy blocks credentials and Ghana Card PINs, masks phone numbers and redacts every other detail Veil finds as a typed placeholder such as `[EMAIL_ADDRESS]`.

For anything else, write a custom policy in YAML. Each rule matches entity types or categories and chooses an action, and the default action covers everything no rule matches. The name and revision are recorded with every request, so you can tell which version of a policy handled it.

```yaml title="policy.yaml"
version: 1
name: support-desk
revision: "1"
default_action: redact
rules:
  - match: { entity_types: [GH_GHANA_CARD_PIN] }
    action: block
  - match: { entity_types: [GH_MOBILE_NUMBER] }
    action: mask
  - match: { entity_types: [EMAIL_ADDRESS] }
    action: tokenize
  - match: { categories: [CREDENTIAL] }
    action: block
```

The actions are `detect` (report and leave it), `allow`, `mask`, `redact`, `tokenize` (a numbered placeholder such as `[EMAIL_ADDRESS_1]`) and `block`. A block always wins over any other rule. Veil checks a custom policy when you save it, and any error appears under the editor with its path, such as `rules.0.action`.

## Stricter rules for some providers

A provider can have its own policy and its own choice of what happens when Veil can't scan. Use it to block more before prompts reach a provider outside Ghana, for example, while keeping a lighter policy for others. When a request falls back to another provider, the gateway applies that provider's policy, and reuses the first scan when the policies are the same.

## When Veil can't scan

If Veil is unreachable or doesn't answer within two seconds, the project's enforcement decides:

- **Refuse the request** answers 503 `privacy_scan_unavailable`, and nothing reaches a provider. This is the default.
- **Send it with a warning** sends the request as your app wrote it and adds `X-Dekka-Veil-Warning: scan_unavailable` to the response.

Detect mode never refuses a request, since it only reports.

## Restoring tokenised values

With **Restore tokenised values in replies** on, placeholders the policy creates are put back in the model's reply, so your app sees `ama@example.com` where the model wrote `[EMAIL_ADDRESS_1]`. The mapping between placeholders and values exists in memory for that one request and is never logged or stored.

Streamed replies are restored as they arrive. The gateway holds back at most one placeholder's length of text, so a placeholder split across two chunks is still replaced.

## Scanning replies

With **Scan replies too** on, Veil also scans each reply and the request log records how many personal details it found. A non-streamed reply's count is in the `X-Dekka-Veil-Response-Findings` header. A streamed reply is scanned after it finishes, so the count appears only in the log, and the reply itself is never changed.

## Images and audio

Veil reads text only, so a project that uses Veil refuses images in chat and audio for transcription with 422 `privacy_media_blocked`, unless you choose **Allow with a warning**. Allowed requests carry `X-Dekka-Veil-Warning: media_not_scanned`. Image prompts and text to speak are scanned like any other text, and transcripts can be scanned as replies.

## Headers

| Header | What it says |
| --- | --- |
| `X-Dekka-Veil` | `protected`, `detected` or `off` |
| `X-Dekka-Veil-Findings` | How many personal details Veil found in the request |
| `X-Dekka-Veil-Response-Findings` | How many it found in the reply, when replies are scanned |
| `X-Dekka-Veil-Warning` | `scan_unavailable` or `media_not_scanned` |

## What a refusal looks like

```json
{
  "error": {
    "code": "blocked_by_privacy_policy",
    "message": "This project's privacy policy blocks the request, because it contains details the policy doesn't allow to leave.",
    "details": {
      "policy": "default-protection@2026-11-04",
      "by_entity": { "GH_GHANA_CARD_PIN": 1 },
      "by_action": { "block": 1 }
    }
  }
}
```

A refusal names the kinds of detail and how many, never the values.

## What the request log keeps

Each request on the **Requests** page shows whether Veil protected, detected or blocked it, and which kinds of detail it found. The log keeps the mode, the policy and its revision, counts by entity type and by action, and how long the scan took. It never keeps the prompt, the reply, the values Veil found or the restore map.

## With the API

The settings are part of a project's routing rules, and have their own endpoints too.

```bash tab="cURL"
curl -X PUT https://api.dekka.cloud/v1/projects/$PROJECT_ID/ai/privacy \
  -H "Authorization: Bearer $DEKKA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode": "protect", "preset": "default-protection", "enforcement": "closed", "restore_in_responses": true}'
```

`POST /v1/projects/{id}/ai/privacy/preview` with `{"text": "…", "provider": "openai"}` returns what the provider would receive, with the findings, and stores nothing.
