> ## Documentation Index
> Fetch the complete documentation index at: https://docs.narrative.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Redacting Dataset Samples

> Mask personal data in a dataset's stored sample by uploading a redaction that names each sensitive column and the strategy that hides its value

A dataset's [sample](/concepts/architecture/sample-data) is up to 1,000 rows that Narrative copies from your data plane into the control plane so you can preview it. When those rows contain personal data, you can store a **redaction** for the sample: a record of which columns hold personal data, what kind of personal data each column holds, and how each value is masked when the sample is read.

The redaction only affects `GET /datasets/{dataset_id}/sample/redacted`. The underlying sample is not modified — deleting the redaction leaves the sample untouched, and `GET /datasets/{dataset_id}/sample` continues to return unmasked rows.

<Info>
  Writing or deleting a redaction requires an API key with write access on datasets. See [API key permissions](/reference/security/permissions).
</Info>

## Prerequisites

* A dataset with a stored sample. Call `POST /datasets/{dataset_id}/request-sample` first if one does not exist.
* An API key with write access on datasets.

## The four endpoints

| Method   | Path                                     | Purpose                                      |
| -------- | ---------------------------------------- | -------------------------------------------- |
| `GET`    | `/datasets/{dataset_id}/redaction`       | Read the stored redaction                    |
| `PUT`    | `/datasets/{dataset_id}/redaction`       | Store or replace the redaction               |
| `DELETE` | `/datasets/{dataset_id}/redaction`       | Delete the stored redaction                  |
| `GET`    | `/datasets/{dataset_id}/sample/redacted` | Return the sample with the redaction applied |

## Upload a redaction

Send `PUT /datasets/{dataset_id}/redaction` with one entry per column that holds personal data. Each entry names the column's dot-separated `path` in the sample, an optional `category`, and a `masking_strategy`.

```bash theme={null}
curl -X PUT "https://api.narrative.io/datasets/1234/redaction" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "columns": [
      {
        "path": "email_address",
        "category": "private_email",
        "masking_strategy": { "type": "keep_email_domain" }
      },
      {
        "path": "payment.card_number",
        "category": "account_number",
        "masking_strategy": { "type": "keep_last_digits", "count": 4 }
      },
      {
        "path": "notes",
        "category": null,
        "masking_strategy": { "type": "mask_everything" }
      }
    ]
  }'
```

A successful `PUT` returns `200` and replaces whatever was stored before. Uploading also brings back a redaction that was previously deleted. The stored redaction records who wrote it from your access token, so a `written_by` field in the request body is ignored.

<Note>
  A single redaction may name at most 20,480 columns. A path that does not resolve in a row leaves that row untouched, so a redaction naming a since-renamed column quietly stops masking it.
</Note>

## Masking strategies

Seven strategies are available. Each is a JSON object tagged with `type`.

| Strategy                  | Behavior                                                                                            | Example (`John Doe` / `john@example.com`)                     |
| ------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `keep_email_domain`       | Keeps the domain plus the local part's first and last two characters                                | `jo****hn@example.com`                                        |
| `keep_initials`           | Replaces each whitespace-separated token with its first letter and three mask characters            | `J*** D***`                                                   |
| `keep_network_prefix`     | Keeps an IPv4 address's `/24` and masks the rest. IPv6 is masked completely                         | `192.168.1.***`                                               |
| `keep_two_decimal_places` | Rounds a coordinate to two decimal places                                                           | `40.71`                                                       |
| `keep_year`               | Returns the first four-digit run and drops the rest                                                 | `1985-03-12` → `1985`                                         |
| `keep_last_digits`        | Keeps the last `count` digits (1 to 4), masks other letters and digits, and leaves separators alone | `4111-1111-1111-1234` with `count: 4` → `****-****-****-1234` |
| `mask_everything`         | Keeps nothing                                                                                       | `************`                                                |

`keep_last_digits` requires a `count` between 1 and 4:

```json theme={null}
{ "type": "keep_last_digits", "count": 4 }
```

A value the strategy can't parse (an email with no `@`, a non-IPv4 address, a non-coordinate, a value with no four-digit run) is masked completely.

## Personal data categories

`category` classifies what kind of personal data the column holds. It does not affect masking — only `masking_strategy` does. Nine values are accepted:

* `account_number`
* `other`
* `private_address`
* `private_date`
* `private_email`
* `private_person`
* `private_phone`
* `private_url`
* `secret`

`category` is optional. Send `null` or leave the key out for a column you don't want to classify.

## Read the redacted sample

`GET /datasets/{dataset_id}/sample/redacted` returns the sample with every named column masked and every other column untouched.

```bash theme={null}
curl -X GET "https://api.narrative.io/datasets/1234/sample/redacted?size=5" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={null}
{
  "records": [
    {
      "email_address": "ja****oe@example.com",
      "full_name": "J*** D***",
      "payment": { "card_number": "****-****-****-4321" },
      "city": "Calgary"
    }
  ]
}
```

<Warning>
  Masking a number or a boolean returns the text it renders as, so a masked numeric column comes back as a JSON string. Redacted rows do not match the dataset's declared schema for those columns.
</Warning>

The endpoint returns `400` when no sample or no redaction is stored for the dataset. Use `POST /v2/datasets/{dataset_id}/request-sample` to sample the dataset and have Narrative's privacy model write a redaction in a single call.

## Delete a redaction

`DELETE /datasets/{dataset_id}/redaction` clears the stored redaction. `GET /datasets/{dataset_id}/redaction` then reports it as missing, and `GET /datasets/{dataset_id}/sample/redacted` stops returning rows until a new redaction is stored. Deleting when there is no redaction succeeds and does nothing. The underlying sample is not affected.

## Errors

`PUT /datasets/{dataset_id}/redaction` returns `400` when a request body is malformed. `error_description` points at the offending column by its position in `columns`:

```
DecodingFailure at .columns[0].masking_strategy: unexpected type 'keep_emial_domain'. expected one of keep_email_domain, keep_initials, keep_last_digits, keep_network_prefix, keep_two_decimal_places, keep_year, mask_everything
```

```
DecodingFailure at .columns[0].category: unexpected category 'national_id'. expected one of account_number, other, private_address, private_date, private_email, private_person, private_phone, private_url, secret
```

```
DecodingFailure at .columns[0].masking_strategy.count: 999 digits must be at most 4
```

Both read endpoints return `500` if the stored redaction cannot be decoded — for example, a stored `masking_strategy` this API no longer recognises. The body is an RFC 7807 problem document with `title: "Stored Redaction Unreadable"`. Quote the `log_id` when reporting the failure to support.

## Related content

<CardGroup cols={2}>
  <Card title="Sample Data" icon="flask" href="/concepts/architecture/sample-data">
    How samples move from data planes to the control plane
  </Card>

  <Card title="Hashing PII for Upload" icon="hashtag" href="/guides/ingestion/hashing-pii">
    Prepare identifiers before uploading them
  </Card>
</CardGroup>
