# Erase a contact or a message

Two operations remove personal data. Erase a contact when a person asks to be forgotten or when you decide it as the data controller. Redact a message when one line of a thread holds something that must not stay, such as an ID number or a password.

Both are permanent. There is no undo and no soft delete.


**You will:** Preview an erasure, erase a contact, follow the job until it ends and redact a message.

**You need:**
- A key with contacts:erase and contacts:read, issued by an admin or the owner
- A webhook endpoint subscribed to contact.suppressed

**Erasing a contact**

Preview first, then erase. The call answers 202 and the contact is gone for readers at once. The job runs in phases and ends with a webhook that has only ids.

1. Send dryRun=true first. It changes nothing.
2. The answer is a suppression with status preview and an inventory per store. Never content.
3. The real call. It is permanent: there is no undo and no soft delete.
4. Accepted, not finished. The contact answers 404 from this moment, so nobody can read it while the job runs.
5. The job runs in the background in short batches and can resume if a process dies.
6. status goes queued, running, done. phase says where it is: inventory, rows, objects, rest.
7. Follow the job whenever you want. counts holds numbers only.
8. When the job ends the webhook contact.suppressed arrives with the ids and nothing else. It is your signal to erase the person from your own systems.

## Preview what would be erased

Send `dryRun=true` first. It changes nothing and returns counters per store, never content.

```bash title="cURL"
curl -X DELETE "https://api.lelemon.cl/v2/contacts/ctc_7Hc2LmQ0x8bVw1TqS9mYp3?dryRun=true" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```


The answer is a suppression with `status: "preview"` and an `inventory`: cases, open cases, files and the rows per store.

## Erase a contact

```bash title="cURL"
curl -X DELETE "https://api.lelemon.cl/v2/contacts/ctc_7Hc2LmQ0x8bVw1TqS9mYp3?reason=data_subject_request&closeOpenCases=true" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
const suppression = await lelemon.contacts.delete('ctc_7Hc2LmQ0x8bVw1TqS9mYp3', {
  query: { reason: 'data_subject_request', closeOpenCases: true },
});
```


The call answers `202` with a `suppression`. The contact answers `404` from that moment, so nobody can read it while the job runs.

The `suppression` is this object. Open it to see every field:

**Suppression**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `suppression` |
| `id` | string or null | yes | null on a dryRun: nothing was created. |
| `contactId` | string | yes |  |
| `status` | enum | yes | `queued`, `running`, `done`, `failed`, `held`, `preview` |
| `phase` | enum | yes | `inventory`, `rows`, `objects`, `rest`, `done`, `null` |
| `reason` | enum | yes | `data_subject_request`, `controller_instruction` |
| `legalHold` | boolean | yes |  |
| `closedOpenCases` | number | yes |  |
| `counts` | map of integer | yes | Rows erased or anonymized per store. Numbers only. |
| `errorCode` | string or null | yes |  |
| `inventory` | SuppressionInventory or null | yes | Only on dryRun: what would be erased, by store, without content. |
| `createdAt` | string or null | yes |  |
| `completedAt` | string or null | yes |  |
| `dryRun` | boolean | no | Present and true only on a dry run: nothing was erased and the inventory shows what would be. |

| Parameter | Meaning |
| --- | --- |
| `reason` | `data_subject_request` when the person asked, `controller_instruction` when you decided. Kept in the suppression. |
| `closeOpenCases` | Without it, a contact with open cases is a `409 resource_in_use`. With it, the open cases are closed and erased. |
| `legalHold` | `true` when you must keep the data by law, for example a debt, a credit or a refund. The request is recorded as `held` and nothing is erased. Repeat the call without `legalHold` when the obligation ends. |
| `dryRun` | `true` returns the inventory and changes nothing. |

### What is erased

The contact is anonymized in place, not deleted, so your statistics keep working. Its cases, messages, files, fields, agent memory, logs and queued jobs lose their content.

What remains of each case is an aggregate shell. It has a new id with no table that maps it to the old one, no contact, and dates cut to the day. It is aggregate data, not anonymous data: do not present it as anonymous.

Files are deleted from storage with all their versions and replicas.

The responses stored for `Idempotency-Key` are erased too. A retry with the key of a request that described the person answers `409 idempotency_key_reused`: send it again with a new key. The queues that hold the person's messages (webhook deliveries, sends, incoming messages, WhatsApp history and pipeline jobs) are purged of their jobs.

Opt-outs are the exception. The contact is anonymized but its opt-out stays, with a keyed hash of the phone and the email instead of the values, so nobody writes to that number again.

What waited for a person about the contact is closed first: approvals that were `pending` or `approved` become `canceled` (event `approval.canceled`) and lose their arguments, and the team questions still open are discarded. They never run and nobody is told. See [Approve agent actions](/guides/approvals#when-the-person-asks-to-be-erased).

### Follow the job

The erasure runs in the background in short batches and can resume if a process dies.

```bash title="cURL"
curl "https://api.lelemon.cl/v2/suppressions/sup_3PqA0x8bVw1TqS9mYp3RkL" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```


`status` goes `queued`, `running`, `done`. `phase` says where it is: `inventory`, `rows`, `objects`, `rest`. `counts` holds numbers only. When it ends, the webhook `contact.suppressed` arrives with the ids and nothing else.

### Deadlines

| Where | When |
| --- | --- |
| Our database and file storage | When the job ends |
| Our processors (WhatsApp provider, Meta, logging) | Within 30 days |
| Backups | When each backup expires. If we restore one, we erase the person again from the list of suppressions |

Your own systems are yours. The data processing agreement obliges you to erase the person wherever you copied them. The `contact.suppressed` webhook is the signal to do it.

## Redact a message

```bash title="cURL"
curl -X POST "https://api.lelemon.cl/v2/cases/cse_7Hc2LmQ0x8bVw1TqS9mYp3/messages/msg_3PqA0x8bVw1TqS9mYp3RkL/redact" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```


The text becomes `[redacted]`. The file, the transcription and the copies of the message in logs, events and agent memory are erased. The rest of the thread stays.

It is idempotent. Redacting a redacted message answers `200` with the same message and publishes nothing.

The webhook `message.redacted` carries the case id and the message id. The webhooks you already delivered with `message.created` carried the text, so redact the message in your own systems when it arrives.

## Agents and the API key

The MCP server of the API does not offer erasure by default. Give an agent the key only when a person approves each erasure.
