Skip to main content

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

Open the returned URL, complete the OAuth handshake, then poke a pull:
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:
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

→ 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

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

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

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

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 SDKsGET /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 sourcesGET /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 sourcesGET /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.