> ## 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 connector catalogs: sources, destinations, enrichment, BI, CRM/ERP, offers, and the clean room

> Operate the provider-aware Discovery tabs on Integrations → CDP — browse the source, destination, streaming, file, event-bus, enrichment, and BI catalogs, validate a config before you wire it, run CRM/ERP object syncs, manage the persisted offer catalog, and run privacy-safe two-party overlap in the clean room.

# CDP connector catalogs: sources, destinations, enrichment, BI, CRM/ERP, offers, and the clean room

Integrations → **CDP** is more than the two tabs that mint your ingest secret and forward outbound events. A further set of tabs gives you **provider-aware connector discovery** — a static catalog of every SaaS, warehouse, file, event-bus, enrichment, and BI connector Orbit supports — plus the operational levers on top: validate a connect form before any credential call, check activation status, kick a sync run, and read back the bounded run history.

Under each catalog tab the same four-step shape repeats:

1. **Discover** — list connectors and filter by `category` / `status` / `search`. Each row is a typed, honest-maturity record: `ga` (wired to a live backend today), `beta` (transport live, gaps in metadata), or `coming_soon` (no activatable provider yet).
2. **Validate a config** — every catalog ships a `POST .../validate-config` route that checks an operator-submitted connect form against the connector's field spec **without persisting anything and without the redirect-credentialed call**, and echoes back a credential-redacted copy.
3. **Activate** — on the source catalog, mint the OAuth connect URL (`POST /connect`), kick a managed pull (`POST /sync`), and watch live pull state (`GET /sync-status`).
4. **Audit** — CRM/ERP sync, decisioning, and history surfaces keep a capped sync-run ledger; every mutation lands in the tamper-evident audit chain.

Role gate: **owner**, **admin**, or **developer** — either a Clerk JWT or an API key that carries that role. Discovery payloads never include tenant data or secrets, so they need no `contacts:*` scope.

## 1. Source catalog — SaaS/cloud-app pull connectors

`GET /api/v1/cdp/source-catalog` lists the cloud-app source connectors (Salesforce, HubSpot, Stripe, Zendesk, and more) and `GET /api/v1/cdp/source-catalog/:connector_id` returns one connector's full metadata — the objects it pulls, auth shape, and each config field the connect form renders.

On top of discovery, three self-service routes close the "wire it" loop:

* `POST /api/v1/cdp/source-catalog/:connector_id/connect` — mint the OAuth/authorize URL so you link the upstream provider via Nango. A `coming_soon` connector returns `409` (no activatable backend yet).
* `POST /api/v1/cdp/source-catalog/:connector_id/sync` — trigger a managed pull on the connection you just wired. `409` when the provider is not connected.
* `GET /api/v1/cdp/source-catalog/:connector_id/sync-status` — the live view: connected? errored? last synced at? per-sync row counts.
* `GET /api/v1/cdp/source-catalog/:connector_id/activation` — the read the connect surface uses to render a *Connect* button vs a *Request access* CTA: `activatable`, the wired managed-sync provider and schedule, the connect URL, and which advertised objects are `live_objects` vs `roadmap_objects`.
* `POST /api/v1/cdp/source-catalog/:connector_id/validate-config` — structural connect-form validation. Check an operator-submitted connect form against the connector's field spec without persisting and without reaching the provider; the echo is credential-redacted.

### Worked example — connect HubSpot

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/source-catalog/hubspot/connect \
  -H "X-API-Key: dv_live_sk_your_key_here"
# → { "connectUrl": "https://...nango.dev/oauth...&connection_id=hubspot__org_abc" }
```

Open the returned URL, complete the OAuth handshake, then poke a pull:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/source-catalog/hubspot/sync \
  -H "X-API-Key: dv_live_sk_your_key_here"
curl -s https://api.orbit.devotel.io/api/v1/cdp/source-catalog/hubspot/sync-status \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The response reports per-sync rows ingested and the honest `activation` payload tells you which advertised objects are live now vs still on the roadmap.

## 2. Destination catalog — outbound connectors

`GET /api/v1/cdp/destination-catalog` lists the outbound destinations (Segment, Salesforce, Braze, Iterable, Customer.io, Klaviyo, and the full Nango-routed catalog) with `?category=`, `?status=`, `?routing=`, and `?search=` filters plus per-category facet counts.

Validate a connect form without persisting a row and without an upstream `triggerAction` call:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/destination-catalog/customer_io/validate-config \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "api_key": "sk-acme-123",
    "site_id": "12345-ab"
  }'
```

→ `200 { "normalized": { "api_key": "***redacted", "site_id": "12345-ab" } }` — the echo is credential-redacted, but the field-spec check (missing key? wrong shape?) runs before any credential lives anywhere.

## 3. Enrichment — Clearbit, ZoomInfo, Apollo, FullContact

`GET /api/v1/cdp/enrichment-catalog` lists the data-enrichment providers and `GET /api/v1/cdp/enrichment-catalog/:provider_id` a single provider's appended attributes and auth fields. Two extras sit next to discovery:

* `GET /api/v1/cdp/enrichment-catalog/:provider_id/scoring-signals` — which firmographic account-scoring inputs the provider would supply, replacing the contact-count proxy default.
* `POST /api/v1/cdp/enrichment-catalog/:provider_id/validate-config` — structural validation of a connect form, credential-redacted echo.

For a vendor with no live adapter, `GET .../activation` returns `coming_soon`, and `POST .../broker-preview` prices a lookup and normalizes a supplied sample vendor response onto the canonical attribute vocabulary — a pure preview, no live call, no credentials.

### Worked example — preview an enrichment lookup

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/enrichment-catalog/clearbit/broker-preview \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "raw": { "company_name": "Acme Corp", "employee_count": 450 },
    "margin_pct": 12,
    "units": 1
  }'
```

→ the preview returns a `billing` breakdown (vendor list cost + your margin at the supplied `margin_pct`) plus a `patch` showing which canonical fields the vendor's adapter would map off the sample `raw` payload. No live vendor call ever leaves this route — omit `raw` to price without field-mapping.

## 4. BI tools — Tableau, Power BI, Looker, Superset, Metabase

`GET /api/v1/cdp/bi-connectors` lists the supported BI tools. `GET /api/v1/cdp/bi-connectors/:tool_id` returns one tool's metadata (vendor, warehouses it can read, docs), and `POST /api/v1/cdp/bi-connectors/:tool_id/connection-profile` generates a per-tool, per-warehouse profile.

The profile carries driver, connection fields, schema-qualified datasets, and step-by-step setup. It contains **placeholders, never a credential** — Orbit never proxies warehouse credentials, and the generated file is meant for the analyst to hand their own key to their own warehouse.

### Worked example — generate a Tableau / BigQuery profile

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/bi-connectors/tableau/connection-profile \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "warehouse": "bigquery" }'
```

→ `200 { "driver": "com.google.bigquery.jdbc.Driver", "datasets": [...], "setup_steps": [...] }` — hand the generated file to a Tableau Desktop workspace. The plumbing then runs entirely inside your own warehouse: reverse-ETL loads rows into BigQuery; the BI tool reads them from there.

## 5. CRM sync — segment → SaaS object writes

CRM sync is the **config + run + runs** triangle used to ship first-party segments into Salesforce, HubSpot, Braze, Iterable, Customer.io, or Klaviyo as native object writes (not a per-event webhook).

* `GET /api/v1/cdp/crm-sync/config` — every destination's mapping: `enabled`, Nango connection id, `object_type`, upsert `identifier_field`, `field_map`, `segment_id`, `schedule_minutes`, and the last-run status.
* `PATCH /api/v1/cdp/crm-sync/config/:destination` — wire or adjust one destination. Only the supplied keys are touched; sibling destinations and other settings keys are preserved. `object_type` is checked against the destination's whitelist.
* `POST /api/v1/cdp/crm-sync/run/:destination` — run one sync now from a supplied batch of profiles.
* `GET /api/v1/cdp/crm-sync/runs` — bounded sync-run history, newest first.

### Worked example — wire HubSpot, then kick a run

```bash theme={null}
curl -X PATCH https://api.orbit.devotel.io/api/v1/cdp/crm-sync/config/hubspot \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "object_type": "contact",
    "identifier_field": "email",
    "segment_id": "seg_champions",
    "field_map": {
      "email": "email",
      "first_name": "firstname",
      "last_name": "lastname",
      "company": "company"
    },
    "schedule_minutes": 360
  }'
```

A profile with no value for `identifier_field` is dropped before batching — never a blind insert — and the run row lands in `GET /crm-sync/runs` with per-run counts. A scheduled cadence follows the same mapping; the on-demand `/run` route is also what the console's **Run now** button hits.

## 6. ERP sync — segment → finance-system object writes

The ERP sibling of the CRM surface carries the same triangle. Destinations: `netsuite`, `sap_erp`, `workday`, `quickbooks`. Every note about `object_type` whitelists, identifier-field skip, and the bounded sync history is identical; the only difference is which system the upsert lands in.

```bash theme={null}
curl -X PATCH https://api.orbit.devotel.io/api/v1/cdp/erp-sync/config/netsuite \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "object_type": "Customer",
    "identifier_field": "entityId",
    "segment_id": "seg_active_customers",
    "field_map": { "entity_id": "entityId", "email": "email" }
  }'
```

No secret travels on this surface: `nango_connection_id` is a connection identifier, and the upstream OAuth token stays inside Nango.

## 7. Offer catalog — next-best-offer constraints, persisted

Until this surface, `POST /cdp/offers/decide` forced the caller to re-transmit the entire offer catalog and each offer's eligibility constraints on every decision request — no durable server-side catalog record. The persisted row gives your workspace one canonical, auditable place for the constraints; the `decide` API still evaluates whatever candidates it receives inline. The workflow today: upsert the constraint rows here, then GET them and send the same constraints through to `decide` from your campaign code — removed from the catalog, an offer stops being deliverable next time a caller takes the catalog as its source.

* `GET /api/v1/cdp/offers` — keyset-paginated list, `?active_only=true` filters to the partial index.
* `POST /api/v1/cdp/offers` — upsert keyed on `offer_id`.
* `DELETE /api/v1/cdp/offers/:id` — remove a row.

### Worked example — upsert a restricted offer

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/offers \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "offer_id": "offer_summer_champions",
    "value": 25,
    "priority": 10,
    "constraints": {
      "starts_at": 1754102400000,
      "expires_at": 1756799999999,
      "max_per_contact": 1,
      "budget_remaining": 5000,
      "cost_per_redemption": 25,
      "exclusivity_group": "summer-2025",
      "required_segments": ["seg_champions"],
      "channels": ["sms"],
      "min_cart_value": 50
    }
  }'
```

→ `200 { "ok": true }`. The row now lives in the workspace's catalog — read it back with `GET /offers?active_only=true` and verify the constraint shape travelled, then pass the same constraints into any `POST /cdp/offers/decide` call your campaign code makes.

## 8. Clean room — two-party hashed overlap

`POST /api/v1/cdp/clean-room/match` runs a privacy-safe two-party overlap **without either side exchanging cleartext PII**: each side supplies a batch of member identifiers (`records` for cleartext, or `hashed_identifiers` pre-hashed with the documented local contract), and the match runs on digests only.

Role gate is owner/admin/developer **plus `contacts:read`** scope — a clean-room match processes your members' identifiers, so a scoped key that excludes contacts bounces before any hashing starts.

### Worked example — overlap with a media partner

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/cdp/clean-room/match \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "party": { "records": [{ "identifier": "alice@acme.com", "consent": true }] },
    "partner": { "hashed_identifiers": ["<hashed-digest>", "<hashed-digest>"] },
    "label": "partner overlap run",
    "min_match_threshold": 20
  }'
```

→ `200 { "overlap_size": 47, "party_match_rate": 0.42, "partner_match_rate": 0.61, "jaccard": 0.32, "matched_digests": [...] }`.

`consent: false` records are dropped before hashing — the opt-out is honoured at the source. When the overlap is below `min_match_threshold`, the matched digest list is suppressed (k-anonymity), and only the counts in the audit log persist.

## 9. Streaming, file, and event-bus source catalogs

Three further discovery tabs mirror the side channels the dashboard already lists beside the SaaS pull sources.

* **Streaming SDKs** — `GET /api/v1/cdp/streaming-sources` registers first-class real-time sources for server, mobile, and web SDKs plus the raw HTTP ingest endpoints (`/cdp/v1/:ingest_id/{track,identify,page,screen,group,alias,batch}`). Status is honest here: `ga` only when the packaged SDK ships today.
* **File sources** — `GET /api/v1/cdp/object-storage-sources` (and the sibling `file-source` catalog) for object-storage file-drop ingestion — drop CSV / JSON / Parquet events or profiles in your own S3 / GCS / Azure Blob bucket on a schedule. Each connector ships its own `validate-config` sibling.
* **Event bus sources** — `GET /api/v1/cdp/event-bus-sources` for Kafka topic / Kinesis stream / Pub/Sub subscription sources (the consume-side mirror of the already-shipped event-bus **destinations**). Also ships `POST .../preview-message` and `POST .../consume-batch` for a non-persistent soak test before wiring the continuous consumer.

Each catalog ships with the same discovery → validate → activate → audit pattern from the top of this guide — browse the catalog, validate the connect form, then wire the live pull/ingest.

## 10. Permissions and audit

Every catalog mutation sits behind `requireRole("owner","admin","developer")` on a Clerk JWT or API key. All writes (`PATCH`, `POST /connect`, `POST /sync`, `POST /offers`, `POST /crm-sync/run`, `POST /erp-sync/run`, `DELETE /offers/:id`) append to the tamper-evident audit chain with the actor, the mutation action, and the target id. Discovery payloads are static product metadata — no tenant data, no secrets — so the discovery surface carries no `contacts:*` scope.

A scoped API key works fine for the read and validate steps; strip its `contacts:read` scope and the clean-room match route starts rejecting with a scope error, since it processes member identifiers from the workspace.
