DROP scrubbing API for California data brokers
Check records against your California DROP deletion list from your own pipelines. Send a record, a batch, or a CSV file. Get back exactly what to delete.
What is the Purgepath API?
The Purgepath API is a REST API for California DROP compliance. Purgepath keeps your DROP deletion list current by syncing it daily. You send records from your CRM, data warehouse, ingestion pipeline or internal tools, and the API tells you which ones match a deletion request.
Use it to screen new data before it lands, re-check your full database after each sync, and keep a timestamped record of every check.
Quickstart
Send records with any identifiers you hold. Purgepath standardizes them to CalPrivacy's formatting rules, hashes them, and matches against your list.
curl https://api.purgepath.com/v1/scrub \ -H "Authorization: Bearer pp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "records": [ { "id": "cust_1001", "email": "jane.doe@example.com" }, { "id": "cust_1002", "phone": "(916) 555-0142" }, { "id": "cust_1003", "first_name": "Kim", "last_name": "Nguyen", "dob": "1971-04-09", "zip": "90012" } ] }'
const res = await fetch("https://api.purgepath.com/v1/scrub", { method: "POST", headers: { Authorization: `Bearer ${process.env.PURGEPATH_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ records: [ { id: "cust_1001", email: "jane.doe@example.com" }, { id: "cust_1002", phone: "(916) 555-0142" }, ], }), }); const { results } = await res.json(); const toDelete = results.filter(r => r.action === "delete").map(r => r.id);
import os, requests res = requests.post( "https://api.purgepath.com/v1/scrub", headers={"Authorization": f"Bearer {os.environ['PURGEPATH_KEY']}"}, json={"records": [ {"id": "cust_1001", "email": "jane.doe@example.com"}, {"id": "cust_1002", "phone": "(916) 555-0142"}, ]}, timeout=30, ) res.raise_for_status() to_delete = [r["id"] for r in res.json()["results"] if r["action"] == "delete"]
Response:
{
"scrub_id": "scr_7Hq2kLx9",
"list_synced_at": "2026-10-04T06:00:00Z",
"summary": { "checked": 3, "delete": 1, "clear": 2 },
"results": [
{ "id": "cust_1001", "action": "delete", "matched_on": ["email"] },
{ "id": "cust_1002", "action": "clear", "matched_on": [] },
{ "id": "cust_1003", "action": "clear", "matched_on": [] }
]
}
How do I authenticate with the Purgepath API?
Send your API key in the Authorization header using the Bearer scheme. Live keys start with pp_live_. Test keys start with pp_test_ and run against a sample list, so you can build before your DROP list is linked.
Authorization: Bearer pp_live_YOUR_KEY
Keep keys server-side. Every request is logged against the key that made it, and keys can be rotated from your dashboard at any time.
How do I check records in real time?
Send a JSON body with a records array of up to 1,000 records. Each record needs an id and at least one identifier set. Include every identifier you hold; Purgepath checks each one against the matching DROP list.
Request fields
| Field | Type | Description |
|---|---|---|
| recordsrequired | array | Up to 1,000 record objects. |
| records[].idrequired | string | Your own reference for the record. Returned unchanged so you can act on results. |
| records[].email | string | Email address. Matched against the email list. |
| records[].phone | string | U.S. phone number in any common format. Matched against the phone list. |
| records[].first_name | string | Used with last_name, dob and zip as one identifier set. All four are needed to match on name. |
| records[].last_name | string | See first_name. |
| records[].dob | string | Date of birth, YYYY-MM-DD. |
| records[].zip | string | ZIP code. ZIP+4 is accepted. |
| tags | object | Optional. Free-form labels stored with the scrub record, e.g. {"source": "vendor_feed_q4"}. |
What response fields does the API return?
| Field | Type | Description |
|---|---|---|
| scrub_id | string | Unique ID for this check. Stored with a timestamp for your audit trail. |
| list_synced_at | timestamp | When your DROP list was last synced. Records were checked against this version. |
| summary | object | Counts of checked, delete and clear. |
| results[].id | string | Your record ID. |
| results[].action | string | delete if any identifier matches an active deletion request, otherwise clear. |
| results[].matched_on | array | Which identifier sets matched: email, phone, name_dob_zip. |
| results[].errors | array | Present only if an identifier couldn't be used, e.g. an invalid date. |
How do I upload a CSV file?
For full-database checks, upload a file as multipart/form-data. CSV, TSV and plain text are accepted. Map your columns to identifier fields, and optionally pass a webhook URL to be notified when results are ready.
curl https://api.purgepath.com/v1/scrub/file \ -H "Authorization: Bearer pp_live_YOUR_KEY" \ -F "file=@customers.csv" \ -F 'columns={"id":"customer_id","email":"email_address","phone":"mobile"}' \ -F "webhook_url=https://example.com/hooks/purgepath"
import json, os, requests with open("customers.csv", "rb") as f: res = requests.post( "https://api.purgepath.com/v1/scrub/file", headers={"Authorization": f"Bearer {os.environ['PURGEPATH_KEY']}"}, files={"file": f}, data={ "columns": json.dumps({"id": "customer_id", "email": "email_address"}), "webhook_url": "https://example.com/hooks/purgepath", }, ) job = res.json() # {"job_id": "job_4Fz81", "status": "queued"}
| Field | Type | Description |
|---|---|---|
| filerequired | file | CSV, TSV or plain text, with a header row. |
| columnsrequired | JSON string | Maps Purgepath fields (id, email, phone, first_name, last_name, dob, zip) to your column names. |
| webhook_url | string | Optional. Called with the job result when processing finishes. |
| output | string | delete_only (default) returns only records to delete. all returns every record with its action. |
How do I check a file job?
{
"job_id": "job_4Fz81",
"status": "complete",
"list_synced_at": "2026-10-04T06:00:00Z",
"summary": { "checked": 2104882, "delete": 1932, "clear": 2102950 },
"result_url": "https://api.purgepath.com/v1/scrub/file/job_4Fz81/result.csv",
"expires_at": "2026-10-11T06:00:00Z"
}status is one of queued, processing, complete or failed. Result files are available for 7 days; the scrub record itself is kept for your audit trail.
How do I check my DROP list status?
{
"state": "CA",
"last_synced_at": "2026-10-04T06:00:00Z",
"next_sync_at": "2026-10-05T06:00:00Z",
"lists": ["email", "phone", "name_dob_zip"],
"active_requests": 352118
}Use this to confirm your list is current before a large job, or to surface sync status in your own monitoring.
Can I send hashes instead of raw data?
Yes. If you'd rather raw identifiers never leave your systems, standardize and hash them yourself (SHA-256, per CalPrivacy's formatting rules) and send the hash fields instead. Matching works the same way.
{
"records": [
{ "id": "cust_1001", "email_sha256": "8f2c…e41a" },
{ "id": "cust_1003", "name_dob_zip_sha256": "b19d…07c3" }
]
}Accepted hash fields: email_sha256, phone_sha256, name_dob_zip_sha256.
Webhooks
Purgepath sends a POST to your webhook_url when a file job finishes, and can notify you after each daily DROP sync. Each webhook is signed with an X-Purgepath-Signature header so you can verify it came from us.
| Event | Description |
|---|---|
| file.complete | A file job finished. Payload matches GET /v1/scrub/file/:id. |
| file.failed | A file job couldn't be processed. Includes an error message. |
| list.synced | Your DROP list synced. Includes the count of new requests, so you can trigger a re-check. |
What errors can the API return?
| Status | Meaning |
|---|---|
| 400 | Malformed JSON or multipart body. |
| 401 | Missing, invalid or revoked API key. |
| 403 | The key is valid but your DROP list isn't linked yet. |
| 413 | More than 1,000 records in one request. Split the batch or use file upload. |
| 422 | Required fields are missing, or no record has a usable identifier. |
| 429 | Too many requests per second. Retry after the time in the Retry-After header. |
{
"error": {
"code": "no_identifiers",
"message": "Record cust_1004 has no usable identifier set."
}
}What are the API limits?
Scrubs are unlimited on the $500/month plan. To keep latency predictable, each /v1/scrub request takes up to 1,000 records, and keys are limited to 20 requests per second. For larger jobs, use file upload, which has no record limit.
Frequently asked questions
What is the Purgepath API?
Which identifiers can I match on?
Do I have to send raw personal data?
How current is the list I'm checking against?
list_synced_at so you know exactly which version a record was checked against.Is there a test environment?
pp_test_) run against a sample list with known matches, so you can build and test before going live.How much does API access cost?
Does the API handle compliance for us?
Get an API key
We're at capacity. Keys are issued to waitlist members as spots open.