Skip to main content
A dataset’s sample 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. Once a redaction is stored, both sample endpoints apply it. GET /datasets/{dataset_id}/sample returns the masked rows by default; pass redacted=false to get the unmasked sample instead. GET /datasets/{dataset_id}/sample/redacted always returns the masked rows and requires a stored redaction. The underlying sample itself is not modified — deleting the redaction leaves the sample untouched, and GET /datasets/{dataset_id}/sample then returns unmasked rows either way.
Writing or deleting a redaction requires an API key with write access on datasets. See API key permissions.

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 endpoints

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.
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.
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.

Masking strategies

Seven strategies are available. Each is a JSON object tagged with type. keep_last_digits requires a count between 1 and 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

Two endpoints return the masked rows. GET /datasets/{dataset_id}/sample applies the stored redaction by default, masking every named column and leaving every other column untouched. Pass redacted=false to get the unmasked sample; a dataset with no stored redaction returns the unmasked sample either way.
GET /datasets/{dataset_id}/sample/redacted always returns the masked sample and requires both a stored sample and a stored redaction.
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.
GET /sample/redacted returns 400 when no sample or no redaction is stored for the dataset. GET /sample returns 400 when no sample is stored, whatever the redacted value. 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.

View the sample in the app

Wherever a dataset’s sample is shown in the app — its own sample section, the mapping editor, and query results — the rows arrive masked by the stored redaction, the same default GET /datasets/{dataset_id}/sample applies. Turn on Show unredacted values below the table to reload the sample unmasked. The switch starts off each time the sample is opened, and a dataset with no stored redaction shows the same rows either way. Update sample in a dataset’s Actions menu, and Request new sample on an empty sample card, both take the POST /v2/datasets/{dataset_id}/request-sample path: they sample the dataset and have the privacy model write the redaction for that sample in one run.
Requesting a new sample from the app replaces the stored redaction with one the privacy model writes. If you maintain a redaction of your own, upload it again with PUT /datasets/{dataset_id}/redaction afterwards, or refresh the sample with POST /datasets/{dataset_id}/request-sample, which leaves the stored redaction alone.

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:
Any read path that consults the stored redaction (GET /redaction, GET /sample with its default, and GET /sample/redacted) returns 500 if the stored redaction cannot be decoded. This happens, for example, when the stored masking_strategy is one 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.

Sample Data

How samples move from data planes to the control plane

Hashing PII for Upload

Prepare identifiers before uploading them