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

# CDP erasure propagation: fan a DSAR deletion into every downstream store

> Operate the erasure-propagation surface on /integrations/cdp — set the per-destination delete-vs-suppress policy, fan a completed GDPR Article-17 erasure out to HubSpot, Salesforce, Braze, Iterable, Customer.io, Klaviyo, and the CRM/ESP connectors, read the per-destination status trail, and re-run only the failed leg.

# CDP erasure propagation

A GDPR Article-17 (or CCPA §1798.105) erasure removes the contact inside Orbit. If you also stream CDP profile and event data into your own CRM or marketing tools — HubSpot, Salesforce, Braze, Iterable, Customer.io, Klaviyo, Pipedrive, Zoho CRM, Intercom, Freshsales, SendGrid, Mailchimp, Brevo, ActiveCampaign, Drip, or one of the ERP/finance connectors — that copy has to go too. Inside mature CDPs this is called *destination delete* (Segment) or *user suppression* (RudderStack, Hightouch suppression syncs). Orbit's equivalent is the **erasure-propagation surface**: one per-destination policy and one per-erasure fan-out action, both driven from the dashboard.

This page is the operator guide for that surface: what propagation covers, where the per-target status rows live, how to re-run a stuck leg, and how the whole thing composes with the tenant-owned DSAR posture. The intake side — filing a request, verifying the subject, the SLA clock — is covered by [DSAR](/compliance/dsar); this guide is about the operational console over it, not the intake itself.

<Warning>
  Propagation issues an irreversible delete or suppression in every connected
  destination. Confirm the identity of the data subject and the jurisdiction
  of the request with qualified counsel before you propagate — Orbit fans out
  whatever you have already decided to erase internally.
</Warning>

All endpoints below are rooted at `https://api.orbit.devotel.io/api/v1/cdp`. Every erasure-propagation route is owner/admin only.

## 1. What erasure propagation covers

When a contact is erased in Orbit, the hard-delete cascade removes every linked record in your tenant: messages, opt-outs, segment and list memberships, agent conversations and memory, call logs, recordings and transcripts, conversations, SDK events, verifications, the CDP event history, and the contact row itself. That cascade is the internal half of an erasure.

Propagation is the external half. It fans the same erasure OUT to the destinations the contact's data has flowed to, in three store classes:

| Store class                                  | What it holds                                                                                                                                                                                                         | How propagation handles it                                                                                                                                                                                                                                     |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CRM / engagement destinations (Nango-backed) | Synced contact/profile copies in HubSpot, Salesforce, Braze, Iterable, Customer.io, Klaviyo, and the CRM/ESP connectors (Pipedrive, Zoho CRM, Intercom, Freshsales, SendGrid, Mailchimp, Brevo, ActiveCampaign, Drip) | One `delete` or `suppress` action per destination, matched on the best identifier it indexes (see the table in section 3).                                                                                                                                     |
| Warehouse / reverse-ETL export sinks         | Mirror tables you pushed CDP contacts and events into (Snowflake, BigQuery, Redshift, self-managed Postgres, Databricks, ClickHouse via [reverse ETL](/guides/reverse-etl-warehouse-exports))                         | The internal cascade deletes the source rows, so the next reverse-ETL run already ships deletes downstream. The propagation surface covers the Nango destination leg; treat warehouse sinks as satisfied by delta delete-sync, not by the destination fan-out. |
| Event store / CDP analytics (internal)       | Ingested event history and the contact row itself                                                                                                                                                                     | Removed by the hard-delete cascade directly — no destination action required.                                                                                                                                                                                  |

Think of the fan-out as a console over the [DSAR](/compliance/dsar) surface: the DSAR decides *that* a contact must be erased; propagation makes the decision stick at every provider the contact reached.

## 2. Set the policy: the Erasure propagation tab on /integrations/cdp

The **Erasure propagation** tab on **Integrations → CDP** is the per-tenant policy surface. It renders one row per wired destination with three columns:

* **Destination** — the provider label (HubSpot, Salesforce, Braze, …).
* **Native default** — the mode Orbit uses when you have not overridden the row.
* **Erasure mode** — your override: `Default`, `Delete`, or `Suppress`.

`Delete` hard-removes the record at the destination (each provider's delete / data-privacy endpoint). All destinations wired today natively support delete. `Suppress` is the fallback posture — the provider adds the identifier to a do-not-contact/suppression list instead of deleting the record. A provider on `Default` follows its native mode; anything you change counts toward the **N overridden** badge and the row gains a reset (trash) control to revert it.

Save with **Save policy**; **Discard changes** returns the editor to the saved map. The same surface is API-driven:

```bash theme={null}
# Read the resolved per-destination policy view
curl https://api.orbit.devotel.io/api/v1/cdp/erasure/destination-modes \
  -H "X-API-Key: dv_live_sk_your_key_here"

# Persist overrides — the PUT fully replaces the map, a destination you
# omit reverts to its native default server-side
curl -X PUT https://api.orbit.devotel.io/api/v1/cdp/erasure/destination-modes \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "destination_modes": { "hubspot": "suppress" } }'
```

The GET response echoes each destination's `destination`, resolved `mode`, `source` (`configured` when you overrode it, else `default`), and `default_mode` for reference — the dashboard renders this map verbatim, so any destination the catalog adds later appears without a client release.

## 3. Trigger the fan-out and watch the per-target status rows

The trigger and the status trail live one level down, on the contact that was erased. Open a contact page in **Audience → Contacts** and the **Right to erasure** card carries a **Downstream propagation** panel:

1. **File the erasure** — *File right-to-erasure request* opens a confirm dialog. Filing creates a pending erasure with a 7-day cooling-off window, cancellable any time before it expires. (The [DSAR](/compliance/dsar) page documents statuses, SLA, and the org-wide erasure queue.)
2. **Propagate** — the panel's **Propagate** button (confirm-gated) fans the erasure out to every active destination, applying each destination's resolved mode. Propagating before the cooling-off ends is fine — the destination leg is independent of the internal hard delete. Run it while the contact still exists: after the hard-delete cascade purges the contact row, identifiers can no longer be resolved to look the record up downstream.
3. **Read the trail** — after the first run the panel renders a summary row (`N dispatched` / `N failed` / `N skipped`) and one row per destination: the label, the applied mode, the latest outcome badge, and the timestamp of the last attempt.

The API equivalents:

```bash theme={null}
# Fan the erasure out — returns the inline per-destination outcomes
curl -X POST \
  https://api.orbit.devotel.io/api/v1/cdp/erasure/<erasureRequestId>/propagate \
  -H "X-API-Key: dv_live_sk_your_key_here"

# Read the per-destination trail back — latest outcome per destination
curl https://api.orbit.devotel.io/api/v1/cdp/erasure/<erasureRequestId>/propagate \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Each destination resolves its own outcome independently:

| Outcome                 | Meaning                                                                                                                                | What to do                                                                                                                                           |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dispatched`            | The destination acknowledged Orbit's `delete` / `suppress` action.                                                                     | Nothing — the leg is done.                                                                                                                           |
| `failed`                | The destination returned an error (credential expired, provider outage, identifier not found). The response includes the `error_code`. | Fix the connection (re-auth the provider in Integrations) or confirm the record held, then re-run — only this destination re-dispatches (section 4). |
| `skipped_no_identifier` | The contact had no identifier the destination indexes on (no email for an email-first provider, for example).                          | Nothing to re-run — the destination never held the contact.                                                                                          |
| `skipped_no_connection` | The destination's connection is inactive in your tenant.                                                                               | Re-activate the connection if the copy matters, then re-run.                                                                                         |

The status response is the same `total` / `dispatched` / `failed` / `skipped` roll-up plus the per-destination rows, so a regulator's "prove the deletion reached Salesforce" question is answered by the row, not by a support ticket.

## 4. Fan-out targets per store type

Propagation walks the destination catalog server-side — the same catalog the dashboard renders — and resolves one action per destination, matched on the first identifier the provider indexes:

| Destination       | Class (sync source) | Native mode | Lookup preference                             |
| ----------------- | ------------------- | ----------- | --------------------------------------------- |
| HubSpot           | CRM                 | `delete`    | `email` → `external_id` → `user_id`           |
| Salesforce        | CRM                 | `delete`    | `external_id` → `user_id` → `email`           |
| Braze             | Engagement          | `delete`    | `external_id` → `user_id` → `email` → `phone` |
| Iterable          | Engagement          | `delete`    | `email` → `user_id` → `external_id`           |
| Customer.io       | Engagement          | `delete`    | `user_id` → `external_id` → `email`           |
| Klaviyo           | Marketing           | `delete`    | `email` → `phone` → `external_id` → `user_id` |
| Pipedrive         | CRM                 | `delete`    | `email` → `external_id` → `user_id`           |
| Zoho CRM          | CRM                 | `delete`    | `email` → `external_id` → `user_id`           |
| Intercom          | CRM                 | `delete`    | `email` → `user_id` → `external_id`           |
| Freshsales        | CRM                 | `delete`    | `email` → `external_id` → `user_id`           |
| SendGrid          | ESP                 | `delete`    | `email` → `external_id` → `user_id`           |
| Mailchimp         | ESP                 | `delete`    | `email`                                       |
| Brevo             | ESP                 | `delete`    | `email` → `phone` → `external_id` → `user_id` |
| ActiveCampaign    | ESP                 | `delete`    | `email` → `external_id` → `user_id`           |
| Drip              | ESP                 | `delete`    | `email` → `external_id` → `user_id`           |
| Oracle NetSuite   | ERP / finance       | `delete`    | `email` → `external_id` → `user_id`           |
| SAP S/4HANA       | ERP / finance       | `delete`    | `email` → `external_id` → `user_id`           |
| Workday           | ERP / HCM           | `delete`    | `email` → `external_id` → `user_id`           |
| QuickBooks Online | ERP / finance       | `delete`    | `email` → `external_id` → `user_id`           |

Every wired destination natively supports `delete`; `suppress` exists so a tenant whose provider's native default is the wrong posture (suppression-only in practice, or your counsel's call) can override per destination from the Erasure propagation tab.

The internal look-up is one lookup per destination, most-precise-first: an identifier set with nothing the destination can match (`skipped_no_identifier`) never attempts a delete it cannot target.

## 5. Idempotency and partial failure

Propagation is deliberately per-destination. Each run resolves every active destination, attempts its action with `Promise.allSettled`-style independence — one failure never blocks the others — and records each destination's own outcome. The status trail keeps **the latest outcome per destination**: re-running updates only the row for whatever you re-dispatched, and a destination that already succeeded and was not re-dispatched keeps its acknowledged timestamp.

The practical recovery loop:

1. Read the trail and isolate the `failed` row (the response lists the `error_code` per row).
2. Fix the provider leg — most failures are an expired Nango connection, a provider-side outage, or a `lookup_key` the provider no longer holds because you partially removed the record by hand.
3. Re-run **Propagate** (or the POST). Destinations whose latest outcome is already `dispatched` are idempotent against re-dispatch — the provider either re-applies a delete it already holds (typically acknowledged as success a second time) or reports the record is gone, but the action is still safe: a re-run never re-adds a record.
4. Already-deleted replicas never come back. A destination that acknowledged the erasure and then reads the re-run as "record not found" simply resolves that destination to the same outcome it had.

A destination whose connection you disconnected *between* the erasure and the re-run surfaces as `skipped_no_connection` on the next attempt — the trail reflects what actually happened, so a partial re-run never silently loses a leg.

## 6. Compose with the DSAR posture and the certificate

The propagation surface is the operational console; the tenant-owned posture lives on the **Compliance → DSAR** pages:

* [DSAR](/compliance/dsar) — intake (operator-filed or public portal with two-factor email+SMS verification), status tracking, SLA per jurisdiction (30 days GDPR, 45 CCPA/CPRA), cancellation, and the erasure certificate. An executed erasure's proof-of-deletion certificate (`GET /compliance/dsar/erasure-requests/:id/certificate`) cites the internal resource counts **and** folds in the per-destination propagation trail, so the downstream leg of the erasure is provable from the same artifact.
* [Deactivation (churn) scrub](/compliance/deactivation-scrub) — the complementary scrub surface: carrier deactivation and DNC hygiene against *future* sends. Erasure propagation removes a record downstream; the deactivation scrub prevents re-contacting a number after the relationship lapses. The two compose: an erased contact is also suppressed from re-entry so it is never re-created by a stale import.

## 7. End-to-end: one contact, one GDPR erasure, converging statuses

Walk one erasure from intake to audit-ready:

```bash theme={null}
CONTACT=contact_abc123
ERASURE=dsar_abc123

# 1. Intake — file the Article-17 request on the contact (7-day cooling-off starts)
curl -X POST \
  "https://api.orbit.devotel.io/api/v1/contacts/${CONTACT}/gdpr/erasure-request" \
  -H "X-API-Key: dv_live_sk_your_key_here"
# → { "id": "dsar_abc123", "status": "pending", "cooling_off_ends_at": ... }

# 2. Policy — confirm the destination map matches your counsel's posture
curl https://api.orbit.devotel.io/api/v1/cdp/erasure/destination-modes \
  -H "X-API-Key: dv_live_sk_your_key_here"

# 3. Propagate while the contact still resolves — fan the erasure out
curl -X POST \
  "https://api.orbit.devotel.io/api/v1/cdp/erasure/${ERASURE}/propagate" \
  -H "X-API-Key: dv_live_sk_your_key_here"
# → { "dispatched": 3, "failed": 1, "destinations": 4, ... }

# 4. Watch the trail converge — re-fetch until every destination is
#    dispatched or explicitly skipped
curl "https://api.orbit.devotel.io/api/v1/cdp/erasure/${ERASURE}/propagate" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The same sequence in the dashboard: file from the contact page's **Right to erasure** card, confirm the **Erasure propagation** tab on **Integrations → CDP**, click **Propagate** on the same card, and watch the per-destination badges flip to **Dispatched**. The **Compliance → DSAR** page shows the erasure row move from `pending` to `executed` once the cooling-off expires, and its certificate folds the propagation rows into the proof.

## Triage a stuck leg

| Symptom                                                           | Likely cause                                                                             | Fix                                                                                                         |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Every destination reports `skipped_no_connection`                 | No active destinations                                                                   | Connect at least one destination (Integrations → Connected apps), then propagate.                           |
| One destination returns `failed` with an auth-shaped `error_code` | Expired Nango connection                                                                 | Re-auth the provider in Integrations and re-run Propagate.                                                  |
| One destination returns `skipped_no_identifier`                   | Contact lacks the identifier the provider indexes (no email for an email-first provider) | Expected — the provider never held the record.                                                              |
| The **Propagate** button never appears on the contact page        | No erasure request on the contact                                                        | File an erasure first; propagation only runs on a contact with an active (or recent non-cancelled) request. |
| The trail is empty after propagation                              | The response had `destinations: 0` and nothing to dispatch                               | Check the destination-modes map — an empty map means there is nothing to fan out to.                        |
