Skip to main content

Erase user identities for GDPR requests

Snowplow Identities provides an API to erase a user's entries from the identity graph to fulfill GDPR right-to-erasure requests. Use the dry-run mode first to preview what will be deleted.

What erasure removes

A successful erasure call removes the following from the Identities database:

  • The user's identifiers matching the inputs you supply
  • The Snowplow IDs those identifiers resolve to, plus any others connected through merges
  • The internal links between those identifiers and Snowplow IDs

What erasure does not remove

Erasure is limited to the Identities graph database. The user's data in other parts of your pipeline is unaffected.

Erasure removes graph data only

The Erasure API deletes rows from the Identities graph database. It does not delete events from your warehouse, downstream destinations, or any other system that has received the data. To fulfill the request, you must also delete the user's data from your warehouse and any downstream destinations.

Make an erasure request

Call the API to fulfill a right-to-erasure request for a specific user. The same endpoint handles both the dry-run preview and the actual deletion.

Authenticate

Follow the instructions in the Account management section to obtain an access token, then export it:

bash
export TOKEN=<your-token>

You also need the API URL for your Identities deployment.

ValueDescriptionWhere to get itFormat
Identities API URLThe API URL for your Identities deploymentConsole > Identities > Overviewhttps://{{123abc}}.identity.snowplowanalytics.com
bash
export IDENTITIES_URL=https://{{123abc}}.identity.snowplowanalytics.com

The token carries your organization, so requests do not include an organization ID in the path.

Preview a deletion (dry run)

Set "dry_run": true to resolve inputs and preview what would be deleted, without touching the database.

bash
curl -s -X POST "$IDENTITIES_URL/erasure" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identifiers": [
{"type": "user_id", "value": "alice@example.com"},
{"type": "user_id", "value": "nobody@example.com"}
],
"snowplow_ids": ["sp_5ntwgrjc3kdm7vhq2bzxf4pyla"],
"dry_run": true
}'

Response (200 OK):

json
{
"operation_id": "019577a3-...",
"successful": [
{
"request": {"type": "user_id", "value": "alice@example.com"},
"result": {
"snowplow_ids": ["sp_2bzxf4pyla5ntwgrjc3kdm7vhq", "sp_kdm7vhq2bzxf4pyla5ntwgrjc3"],
"identifiers_affected": 5
}
},
{
"request": {"snowplow_id": "sp_5ntwgrjc3kdm7vhq2bzxf4pyla"},
"result": {
"snowplow_ids": ["sp_5ntwgrjc3kdm7vhq2bzxf4pyla"],
"identifiers_affected": 2
}
}
],
"not_found": [
{"request": {"type": "user_id", "value": "nobody@example.com"}}
],
"failed": [],
"totals": {
"snowplow_ids_affected": 3,
"identifiers_affected": 7
},
"dry_run": true
}

Each input lands in exactly one of successful, not_found, or failed. A single identifier can resolve to multiple Snowplow IDs through merges, so successful[].result.snowplow_ids lists every one affected. not_found means the identifier or snowplow_id is not in the graph. totals aggregates across all successful items. With dry_run: true, nothing is deleted.

Erase identities

Omit dry_run (or set it to false) to perform the erasure.

bash
curl -s -X POST "$IDENTITIES_URL/erasure" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identifiers": [
{"type": "user_id", "value": "alice@example.com"},
{"type": "user_id", "value": "nobody@example.com"}
],
"snowplow_ids": ["sp_5ntwgrjc3kdm7vhq2bzxf4pyla"]
}'

Response (200 OK):

json
{
"operation_id": "019577b1-...",
"successful": [
{
"request": {"type": "user_id", "value": "alice@example.com"},
"result": {
"snowplow_ids": ["sp_2bzxf4pyla5ntwgrjc3kdm7vhq", "sp_kdm7vhq2bzxf4pyla5ntwgrjc3"],
"identifiers_affected": 5
}
},
{
"request": {"snowplow_id": "sp_5ntwgrjc3kdm7vhq2bzxf4pyla"},
"result": {
"snowplow_ids": ["sp_5ntwgrjc3kdm7vhq2bzxf4pyla"],
"identifiers_affected": 2
}
}
],
"not_found": [
{"request": {"type": "user_id", "value": "nobody@example.com"}}
],
"failed": [],
"totals": {
"snowplow_ids_affected": 3,
"identifiers_affected": 7
},
"dry_run": false
}

The response shape is the same as the dry run, with dry_run: false. Not-found inputs do not fail the request; found inputs are still deleted. The operation_id is your audit reference for this erasure. Keep it with your records.

Limits and errors

Each request is limited to:

  • 100 combined inputs (identifiers plus snowplow_ids)
  • 256 KB request body

Errors use a single envelope:

json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "too many inputs, maximum 100 total identifiers and snowplow IDs",
"timestamp": "2026-03-24T12:00:00.000Z"
}
}

The HTTP status code tells you which kind of error occurred:

StatusWhen
200Request succeeded. Per-input validation errors appear in failed[].
400Request-level validation failed: over the input limit, body too large, or malformed JSON.
401Missing, invalid, or unauthorized token.
500Server error.

Input-level validation errors (for example, an empty type or value) return 200 with the invalid input listed in failed[]:

json
{
"failed": [
{
"request": {"value": "test"},
"error": {"code": "validation_error", "message": "missing type or value"}
}
]
}