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

# Consent-aware CDP ingest: bring a consented contact base in with lawful basis attached

> Push an existing opted-in user list or warehouse table into the Orbit CDP so every record lands with its lawful basis, disclosure version, and capture timestamp, and downstream sends respect it.

# Consent-aware CDP ingest: bring a consented contact base in with lawful basis attached

You already own a consented audience: a CSV export, a warehouse table, a legacy ESP list. Getting the identifiers into the Orbit CDP is the easy half. The half that makes those people sendable is the consent metadata that travels with each record: which disclosure version the person accepted, on what lawful basis, captured when, through which source, and what happens when they change their mind.

This guide walks that loop end to end: configure the tenant-owned controls, map your ledger to the consent envelope, wire the source with signed ingest, prove enforcement in the sandbox, and package the evidence. It sits between three existing guides that each cover one depth, not the migration shape: the console tour ([CDP integration console](/guides/cdp-integration-console-overview)), the identity resolver ([anonymous identity stitching](/guides/anonymous-identity-stitching)), and the exit path ([erasure propagation](/guides/cdp-erasure-propagation)).

## 1. The task, and the five ways ingest without consent fails

Ingesting identifiers alone takes an afternoon. Ingesting them without consent metadata creates five gaps that surface later, always at the worst moment:

| # | Failure mode | What lands in Orbit | Where it bites |
| - | - | - | - |
| 1 | Missing lawful-basis flag | Profiles with no legal ground recorded | Neither the [evidence binder](/guides/compliance-evidence-binder) nor a regulator can see why you may contact each person. |
| 2 | Unversioned disclosure | Opt-ins with no disclosure version | You cannot tell which opt-in text the person accepted, and a v1 opt-in does not cover v3 wording. |
| 3 | No capture timestamp | Grants with no `granted_at` | You cannot prove consent predates the first send, and re-consent windows have no start date. |
| 4 | No revocation path | Opt-outs stranded in the warehouse | Withdrawals never reach the ledger, so suppression and [erasure propagation](/guides/cdp-erasure-propagation) never fire. |
| 5 | PII crossing regions | Residency-pinned profiles in the wrong pipeline | EU-pinned rows flow through a region your [data-residency posture](/compliance/data-residency-overview) forbids. |

Consent posture is tenant-owned in Orbit: the platform records, gates, and proves; you set the posture. Everything in this guide is a control you configure, not a platform mandate.

## 2. Configure tenant-owned controls before ingest

Do this before the first row moves, so every ingested record lands already governed:

1. **Pick a compliance profile.** Run the [compliance profile wizard](/guides/compliance-profile-wizard) and choose the template that matches your market. The template you pick becomes the `policy_template` on every ledger row this source writes.
2. **Declare lawful basis per source.** For each list or table you migrate, record the basis your counsel determined for it (`consent` for opted-in marketing lists, another basis only where counsel says so). You will attach it to every row from that source in section 3.
3. **Pin quiet hours per country.** Set the sending windows your markets require, per the [country rules posture playbook](/guides/country-rules-posture-playbook). Ingested contacts become sendable immediately; the windows must exist before they do.
4. **Pin data residency where required.** If part of the base is region-pinned, pin it before ingest per [data residency pin, lock, migrate](/guides/data-residency-pin-lock-migrate) so no row ever lands outside its pinned region (failure mode 5).

## 3. Map your warehouse columns to the consent envelope

Start from the shape your ledger actually holds. A typical three-column consent ledger export:

```csv theme={null}
email,consent_version,captured_at
jane@globex.com,v3,2026-06-14T09:32:11Z
liu@globex.com,v3,2026-06-20T18:05:47Z
mara@globex.com,v2,2025-11-02T11:21:09Z
```

The envelope Orbit's consent ledger stores has four fields: `{consent_version, lawful_basis, source, captured_at}`. Two of them (`lawful_basis`, `source`) are declared once per source in section 2; two (`consent_version`, `captured_at`) ride on every row. A dbt model that emits the envelope:

```sql theme={null}
-- models/mart/cdp_consent_envelope.sql
-- One row per opted-in identity, envelope complete.
select
    email                                            as identity_value,
    consent_version                                  as consent_version,
    'consent'                                        as lawful_basis,  -- declared per source, step 2
    'warehouse_migration_2026_06'                      as source,        -- declared per source, step 2
    captured_at                                      as captured_at
from {{ ref('stg_consent_ledger') }}
where opted_in = true
  and consent_version is not null
  and captured_at is not null
```

The `where` clause is the point: a row with a missing version or timestamp is failure mode 2 or 3, and it must stop here, not in Orbit.

The warehouse-to-Orbit mapping:

| Warehouse model field | Orbit consent ledger field | Notes |
| - | - | - |
| `identity_value` (email) | `traits.email` on `/identify`, then `identity_value` on the consent call | `/identify` returns the `contact_id` the ledger row keys on |
| `consent_version` | `consent_text_version` | The disclosure version the row's opt-in was captured under |
| `lawful_basis` | `lawful_basis` | Declared per source in section 2 |
| `source` | `source` | Declared per source in section 2 |
| `captured_at` | `granted_at` | The proof that consent predates the first send |

## 4. Wire the source, and sign every event

1. **Create the source.** In the dashboard, open **Integrations → CDP → Sources**, create a source for this migration, and copy the ingest id and the secret shown once. The API equivalent is `POST /api/v1/cdp/secrets`. The full signing contract (the three headers, the canonical string, the replay rules) is [CDP ingest signing](/guides/cdp-ingest-signing); every request below carries those headers.
2. **Identify each row.** Send one signed `/identify` per row to `/cdp/v1/:ingest_id/identify` with the email in `traits`. The 200 response returns the `contact_id` the consent ledger keys on.
3. **Record the grant.** For each row, append the durable consent decision with the mapped envelope:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/consent \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "identity_kind": "contact_id",
    "identity_value": "cnt_01H9XABC1234",
    "action": "grant",
    "purpose": "marketing",
    "lawful_basis": "consent",
    "consent_text_version": "v3",
    "source": "warehouse_migration_2026_06",
    "granted_at": "2026-06-14T09:32:11Z",
    "idempotency_key": "ledger-v3-cnt_01H9XABC1234"
  }'
```

The `idempotency_key` makes the migration re-runnable: a replayed row is deduped, not double-granted.
4\. **Make the envelope mandatory on ingest events.** In your [tracking plan](/guides/cdp-tracking-plan), declare `consent_text_version`, `lawful_basis`, `source`, and `captured_at` required on the events this source produces, and set the plan to strict enforcement. From now on, an event that arrives without its envelope is rejected at the gateway instead of silently persisting.

Every event this source emits now either carries its consent envelope or does not persist at all.

## 5. Verify enforcement in the sandbox

Run both captures in [sandbox mode](/guides/sandbox-test-mode) before the production cutover.

**Capture 1: an event without consent is rejected.** Send one signed `track` for a contact whose envelope fields you deliberately omitted. With the tracking plan in strict mode, expect a 422:

```json theme={null}
{
  "error": {
    "code": "TRACKING_PLAN_VIOLATION",
    "message": "Event rejected by tracking plan",
    "status": 422,
    "details": {
      "violations": [
        { "type": "required_field_missing", "path": "properties.consent_text_version", "expected": "string", "actual": "undefined" },
        { "type": "required_field_missing", "path": "properties.lawful_basis", "expected": "string", "actual": "undefined" }
      ]
    }
  },
  "meta": { "request_id": "req_01J9SBXQA1" }
}
```

If that send returns 200, the tracking plan is still in soft mode; fix step 4 of section 4 before migrating real rows.

**Capture 2: a revocation propagates.** Record a withdrawal for one sandbox contact:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/consent \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "identity_kind": "contact_id",
    "identity_value": "cnt_01H9XABC1234",
    "action": "revoke",
    "purpose": "marketing",
    "idempotency_key": "revoke-cnt_01H9XABC1234-2026-10-01"
  }'
```

Then verify the chain: `POST /api/v1/cdp/consent/check` for that identity and purpose now returns the withdrawn state; a send to the contact is blocked by your tenant's consent gate (see [consent-required blocked sends](/troubleshooting/consent-required-blocked-sends)); and `POST /api/v1/cdp/consent/{contactId}/{channel}/propagate-revocation` pushes the withdrawal to your connected destinations with a per-destination audit row. An `rtbf` action goes further: it revokes every purpose and feeds the erasure-propagation pipeline, fanning the deletion out to every downstream store per [CDP erasure propagation](/guides/cdp-erasure-propagation).

## 6. Prove it with an evidence binder

When counsel or a regulator asks "show me the basis for contacting these people", the [compliance evidence binder](/guides/compliance-evidence-binder) assembles the answer from the artifacts this guide created:

* The per-identity ledger scroll, `GET /api/v1/cdp/consent/{contactId}`: every grant and revoke with `lawful_basis`, `consent_text_version`, `source`, `granted_at`, and `revoked_at`.
* The re-consent surface, `GET /api/v1/cdp/consent/renewals`: identities whose grants are expiring under your configured retention window, before they lapse.
* The revocation trail from capture 2: per-destination propagation outcomes with timestamps.
* The per-message proof-of-consent receipt that binds each send to the consent record it relied on.

The migration is done when a sample of those receipts, pulled at random, each trace back to a ledger row whose `captured_at` predates the send and whose `consent_text_version` matches the disclosure in force at capture.

## See also

* [CDP ingest signing](/guides/cdp-ingest-signing) — the signing contract every request in this guide carries
* [CDP tracking plan](/guides/cdp-tracking-plan) — the strict-mode contract that rejects envelope-less events
* [CDP erasure propagation](/guides/cdp-erasure-propagation) — the destination fan-out a revocation or rtbf triggers
* [Compliance profile wizard](/guides/compliance-profile-wizard) — pick the template before ingest
* [Country rules posture playbook](/guides/country-rules-posture-playbook) — pin quiet hours per country
* [Consent and suppression model](/concepts/consent-and-suppression-model) — how grants, suppression, and sends interact


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.