# Play 1: Contact LinkedIn CTD Enrichment — Drips (step-by-step)

**Client:** Drips  
**Salesforce admin:** Miller (`miller@drips.com`)  
**CTD implementation reference:** Same flow as CTD’s internal “Play 1” (Contact record-triggered enrichment + warm-path email)  
**What stays the same:** Apex, CTD API endpoints, six Contact enrichment fields, permission set, Flow logic, email format  
**What Drips must configure:** **Enterprise API key** (not Personal), Named Credential, LinkedIn field API name on Contact, email recipient, page layout / Lightning page (org-specific)

---

## What you are building

When a **Contact** is created or updated with a non-blank **LinkedIn URL**, Salesforce will (asynchronously, after commit):

1. Call CTD **global paths** API and build a warm-intro email for the account owner.
2. Call CTD **reachable-person** API and write six CTD fields on the Contact.
3. Send the HTML email to the Contact owner (or an address you choose in the Flow).

This runs on **Contact** only. It does not fire on Lead unless you add a separate Lead flow or always convert/sync LinkedIn to Contact.

---

## Roles and prerequisites

| Role | Who | Responsibility |
|------|-----|----------------|
| CTD org admin | Drips user with **admin** access in CTD (e.g. Miller if admin) | Create **Enterprise API key** on [API keys](https://app.ctd.ai/account/api-access/api-keys); confirm Drips users and graph data are in CTD |
| Salesforce admin | **Miller** (`miller@drips.com`) | Named Credential, deploy metadata, map LinkedIn field, activate Flow, assign permission set |
| CTD delivery | CTD team (Aleks) | Provide the deployment ZIP/repo (`sf-flow-ctd-email` package) |

**Miller needs in Salesforce:**

- System Administrator (or equivalent: customize app, manage flows, deploy Apex, manage named credentials)
- [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) (`sf`) installed on a machine that can log into the Drips org
- Ability to run tests on deploy (production and most sandboxes require `RunSpecifiedTests`)

**Drips CTD workspace:**

- Users who should appear as connectors must be CTD users in the **Drips** workspace.

---

## Part A — CTD: Create the Enterprise API key

This integration requires an **Enterprise API key** (`eak_…`). Do **not** use a **Personal API key** (`uak_…`). Enterprise keys are **Admins only** and power the `/enterprise/atc-paths-api/...` routes (organization-wide network data for Salesforce and other internal tools).

Miller (`miller@drips.com`) must be a **CTD org admin** to create this key. Salesforce admin rights alone are not enough if the CTD account is not an admin.

### Step A1 — Open the API keys page

1. Sign in to [Connect The Dots](https://app.ctd.ai) with a Drips **admin** account.
2. Open **[API keys](https://app.ctd.ai/account/api-access/api-keys)** (`https://app.ctd.ai/account/api-access/api-keys`).

You will see two sections:

| Section | Use for this integration? |
|---------|---------------------------|
| **Personal API key** | **No** — per-user key; wrong for Salesforce org integration |
| **Enterprise API key** (badge: *Admins only*) | **Yes** — org-wide key for internal tools and automations |

### Step A2 — Create the Enterprise key

1. In the **Enterprise API key** section, click **Add key** (right side of the row).
2. Complete the prompt and copy the key immediately. It starts with `eak_…`. You cannot view the full key again later.
3. Store it in your company password manager. **Do not** paste it into Slack, email, or a Google Doc in a shared drive.

**Important:** CTD allows only **one active Enterprise API key** at a time. Creating a new Enterprise key revokes the previous one.

**Do not** click **Add key** under **Personal API key** for this Salesforce flow.

### Step A3 — Choose `ctd-client-id` for Salesforce

The CTD API requires two headers on every call:

| Header | Value |
|--------|--------|
| `ctd-api-key` | The **Enterprise** API key (`eak_…`) |
| `ctd-client-id` | A **@drips.com** email of a real CTD user (used for audit and access control) |

Pick one email and use it consistently in the Named Credential (recommended: `miller@drips.com`).

### Step A4 — Smoke-test the key (optional but recommended)

From a terminal (replace placeholders):

```bash
export CTD_KEY='eak_YOUR_KEY_HERE'
export CTD_CLIENT='miller@drips.com'
export LINKEDIN='https://www.linkedin.com/in/example'

curl -sS \
  -H "ctd-api-key: ${CTD_KEY}" \
  -H "ctd-client-id: ${CTD_CLIENT}" \
  "https://api.ctd.ai/enterprise/atc-paths-api/public/v1/reachable-person?person_linkedin_url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''${LINKEDIN}'''))")"
```

- **200** with JSON: key and client id are valid.
- **401** with message about `ctd-api-key`: wrong key, or a **Personal** key used instead of an **Enterprise** key.
- **401** about `ctd-client-id`: header missing or email not valid in the workspace.

Hand the key to Miller only through a secure channel (1Password share, etc.).

---

## Part B — Salesforce: Decide the LinkedIn field API name

The reference integration reads LinkedIn from **`LinkedIn_Profile__c`** on Contact. Drips only needs to change this if your org already uses a **different** field.

### Step B1 — Find your existing field

1. In Salesforce: **Setup** → **Object Manager** → **Contact** → **Fields & Relationships**.
2. Search for LinkedIn (label or API name).
3. Write down the **API Name** (ends in `__c`), for example:
   - `LinkedIn_Profile__c` (same as reference; no Flow changes needed)
   - `LinkedIn__c`, `LinkedIn_URL__c`, `LID__LinkedIn_URL__c`, etc. (requires Flow updates; see Part D)

### Step B2 — If you have no LinkedIn field on Contact

Create one (recommended to match the package defaults):

| Setting | Value |
|---------|--------|
| Data Type | **URL** (or Long Text Area if you must store non-URL text) |
| Field Label | `LinkedIn Profile` |
| **API Name** | **`LinkedIn_Profile__c`** |
| Required | No |
| Visible on layouts | Yes (AEs must be able to enter URLs) |

Document your choice for the team:

| Item | Drips value |
|------|-------------|
| LinkedIn field API name | `___________________________` |
| Used in Flow entry criteria? | Yes (must match) |
| Used in Flow action `personLinkedInUrl`? | Yes (must match) |

---

## Part C — Salesforce: Named Credential `CTD_API_NC`

Apex calls CTD with `callout:CTD_API_NC/enterprise/...`. The credential **name must be exactly** `CTD_API_NC`. The URL host must be **`https://api.ctd.ai`** (no trailing path).

This package is **not** in the metadata ZIP; Miller creates it manually in each org.

### Step C1 — Create an External Credential (recommended; supports two headers)

1. **Setup** → search **External Credentials** → **New External Credential**.
2. **Label:** `CTD API`  
   **Name:** `CTD_API` (any internal name is fine).
3. **Authentication Protocol:** **Custom**.
4. Under **Custom Headers** (or Principal / Parameters, depending on Salesforce version), add:

   | Header name | Header value |
   |-------------|----------------|
   | `ctd-api-key` | Paste the Drips **Enterprise** API key (`eak_…`) |
   | `ctd-client-id` | e.g. `miller@drips.com` (must be a CTD user in Drips workspace) |

5. Save.

### Step C2 — Create the Named Credential

1. **Setup** → **Named Credentials** → **New** (or **New Named Credential**).
2. **Label:** `CTD API NC`  
   **Name:** **`CTD_API_NC`** (required; Apex depends on this).
3. **URL:** `https://api.ctd.ai`
4. **External Credential:** select the credential from Step C1.
5. **Callout Options:** enable **Allow Merge Fields in HTTP Body** only if your org requires it (default off is fine).
6. Save.

### Step C3 — Allow callouts for the integration user

1. **Setup** → **Remote Site Settings** is **not** required when using Named Credentials (Salesforce routes via the credential).
2. Ensure the Flow runs in a context that can call out (default **Async After Commit** does).

### Step C4 — Verify in a sandbox

After Apex is deployed (Part E), you can run a one-off test from **Developer Console** → **Debug** → **Open Execute Anonymous** (or wait for Part G). If callouts fail with 401, re-check both headers and that the key is **Enterprise** (`eak_…`), not Personal (`uak_…`).

**Legacy Named Credential (if External Credentials are unavailable):** Create `CTD_API_NC` with URL `https://api.ctd.ai` and custom authentication that sends `ctd-api-key`. If your Salesforce edition only allows one custom header, ask CTD support for the supported pattern or use External Credentials.

---

## Part D — Customize the package (only if LinkedIn API name ≠ `LinkedIn_Profile__c`)

CTD will provide the folder **`sf-flow-ctd-email`** (same as CTD’s reference project). Before deploy, edit **one file** if your LinkedIn field differs:

**File:** `force-app/main/default/flows/Contact_LinkedIn_CTD_API_Enrichment.flow-meta.xml`

Replace **every** occurrence of `LinkedIn_Profile__c` with your API name in:

1. **Start** → entry condition `filterFormula`  
   Example pattern:  
   `AND(NOT(ISBLANK({!$Record.Your_Field__c})), OR(ISNEW(), ISCHANGED({!$Record.Your_Field__c})))`

2. **Action** `Build_And_Send_Paths_Email` → input `personLinkedInUrl` → `$Record.Your_Field__c`

Optional (UI only): update `layouts/Contact-Contact Layout.layout-meta.xml` and `flexipages/Contact_Record_Page.flexipage-meta.xml` to show your LinkedIn field and CTD sections. If deploy fails on layout/flexipage because Drips uses different names, remove those two files from the deploy folder and add CTD fields to the Contact page manually in Setup.

---

## Part E — Deploy metadata to Drips Salesforce

### Step E1 — Get the package from CTD

CTD will deliver `sf-flow-ctd-email` (SFDX project). Unzip to a local path, for example:

```text
~/projects/drips-sf-flow-ctd-email/
```

### Step E2 — Authenticate the CLI to Drips

```bash
sf org login web \
  --instance-url https://YOUR_DRIPS_INSTANCE.my.salesforce.com \
  --alias drips-sandbox \
  --set-default
```

Use the sandbox first. Repeat with `--alias drips-prod` for production when ready.

### Step E3 — Deploy with tests

```bash
cd ~/projects/drips-sf-flow-ctd-email

sf project deploy start \
  --source-dir force-app/main/default \
  --test-level RunSpecifiedTests \
  --tests CTDContactPathsEmailBuilderTest,CTDContactPathsEmailActionTest,CTDSendHtmlEmailTest,CTDContactReachablePersonEnricherTest,CTDContactEnrichActionTest \
  -o drips-sandbox
```

**What this deploys:**

| Type | Names |
|------|--------|
| Flow | `Contact_LinkedIn_CTD_API_Enrichment` |
| Apex classes | `CTDContactPathsEmailAction`, `CTDContactPathsEmailBuilder`, `CTDContactReachablePersonEnricher`, `CTDContactReachablePersonEnrichAction`, `CTDSendHtmlEmail`, plus test classes |
| Contact fields | `CTD_Total_Paths__c`, `CTD_Total_2nd_Degree_Paths__c`, `CTD_Score__c`, `CTD_Score_Scaled__c`, `CTD_Score_Label__c`, `CTD_Is_Target_Person__c` |
| Permission set | `CTD_Contact_Enrichment_Fields` |
| Layout / Flexipage | `Contact-Contact Layout`, `Contact_Record_Page` (may need manual adjustment; see Part D) |

If deploy fails on layout or flexipage, deploy without them:

```bash
sf project deploy start \
  --source-dir force-app/main/default/classes \
  --source-dir force-app/main/default/flows \
  --source-dir force-app/main/default/objects \
  --source-dir force-app/main/default/permissionsets \
  --test-level RunSpecifiedTests \
  --tests CTDContactPathsEmailBuilderTest,CTDContactPathsEmailActionTest,CTDSendHtmlEmailTest,CTDContactReachablePersonEnricherTest,CTDContactEnrichActionTest \
  -o drips-sandbox
```

Then add fields to the Contact page layout in the UI.

---

## Part F — Configure the Flow after deploy

### Step F1 — Set the email recipient (production)

1. **Setup** → **Flows** → **Contact LinkedIn CTD API Enrichment** → **Open**.
2. Open the action **Build and send CTD paths email**.
3. Set **To address** to the Contact owner’s email:
   - Resource: **{!$Record}** → **Owner** → **Email**  
   - Or formula/resource: `{!$Record.Owner.Email}`
4. Remove any hardcoded test address (reference package may still contain a CTD test email).

For a shared inbox or queue, use a valid email your leadership approves; the Flow must pass a single address string into the Apex action.

### Step F2 — Confirm entry criteria

On the **Start** element, confirm:

- Object: **Contact**
- Trigger: **A record is created or updated**
- Run: **Asynchronously after the record is saved** (Async After Commit)
- Condition (formula): LinkedIn field is not blank **and** (record is new **or** LinkedIn field changed)

If you changed the LinkedIn API name in Part D, the formula must reference your field.

### Step F3 — Activate

Ensure Flow **Status** is **Active**. If it deployed as Active, skip.

---

## Part G — Permissions, UI, and email deliverability

### Step G1 — Assign permission set

1. **Setup** → **Permission Sets** → **CTD Contact Enrichment Fields**.
2. **Manage Assignments** → add all users who should **see and edit** the six CTD fields on Contact (typically AEs and sales leadership).
3. Miller should assign to himself for testing.

New custom fields are not visible until Field-Level Security is granted; this permission set covers the six CTD fields.

### Step G2 — Contact page layout

Add sections (or use deployed layout if it applied cleanly):

**Section: CTD Path Enrichment**

- `CTD_Total_Paths__c`
- `CTD_Total_2nd_Degree_Paths__c`
- `CTD_Score_Label__c`
- `CTD_Is_Target_Person__c`
- Your **LinkedIn** field

**Section: CTD Scores (Optional)**

- `CTD_Score__c` (label can read “CTD Score (Optional)”)
- `CTD_Score_Scaled__c`

For **Lightning Experience**, edit the **Contact record page** assigned to your app (may not be named `Contact_Record_Page` in Drips) and add the same fields.

### Step G3 — Email deliverability

1. **Setup** → **Deliverability** → access level **All email** (or your org’s approved setting for automated email).
2. **Setup** → **Organization-Wide Addresses** (optional but recommended): configure a no-reply or salesops address if you want a fixed “From” for `Messaging.sendEmail`. The reference Apex does not set `setOrgWideEmailAddressId`; emails use the default system sender unless you extend Apex.
3. Confirm spam filters allow mail from Salesforce to AEs.

---

## Part H — End-to-end test (Miller)

### Step H1 — Pick a test Contact

Use a real Contact with a LinkedIn URL that exists in **Drips’ CTD network** (a profile someone at Drips could path to).

### Step H2 — Fire the Flow

1. Open the Contact.
2. Set or update the **LinkedIn** field (save).
3. Wait 1–3 minutes (async path).

### Step H3 — Verify CRM

On the Contact record, confirm:

| Field | Expected |
|-------|----------|
| `CTD_Total_Paths__c` | Number (or cleared if person not in CTD) |
| `CTD_Total_2nd_Degree_Paths__c` | Number |
| `CTD_Score_Label__c` | `weak` / `medium` / `strong` or blank |
| `CTD_Is_Target_Person__c` | Checkbox |
| `CTD_Score__c`, `CTD_Score_Scaled__c` | Optional scores |

### Step H4 — Verify email

- Recipient = Contact **Owner** email (after Part F).
- Subject like “Warm intro to … at …”
- Body includes recommended path and **Draft intro email for {connector}** links (Claude pre-fill).

### Step H5 — Debug if nothing happens

1. **Setup** → **Flows** → **Contact LinkedIn CTD API Enrichment** → **View Runs and Details**.
2. Check **Debug Logs** for Apex errors (callout 401, email not sent, validation).
3. Re-run curl test from Part A4 with the same LinkedIn URL.

---

## Part I — Production go-live checklist

Copy this list and check off with Miller before enabling in production:

- [ ] **Enterprise API key** created in production CTD org (or same key approved for prod SF org)
- [ ] Named Credential `CTD_API_NC` created in **production** SF org with `ctd-api-key` + `ctd-client-id`
- [ ] LinkedIn field API name documented and Flow updated if not `LinkedIn_Profile__c`
- [ ] Metadata deployed to production with passing tests
- [ ] Flow **To address** = `{!$Record.Owner.Email}` (or approved alternative)
- [ ] Flow **Active**
- [ ] Permission set assigned to sales users
- [ ] Contact Lightning page shows LinkedIn + CTD fields
- [ ] Deliverability tested with two real AEs
- [ ] Internal doc: AE clicks Claude link → polishes ghost email → shares with connector via CTD ghost emails API from Claude (not automated in Salesforce)

---

## Reference: CTD APIs used (unchanged for Drips)

| Purpose | Method | Path (after `callout:CTD_API_NC`) |
|---------|--------|-----------------------------------|
| Email paths (up to 5) | GET | `/enterprise/atc-paths-api/public/v2/global/paths?person_linkedin_url=…&degree[]=first&degree[]=second` |
| Contact enrichment | GET | `/enterprise/atc-paths-api/public/v1/reachable-person?person_linkedin_url=…` |

**Contact fields written:**

| Salesforce field | CTD API field |
|------------------|---------------|
| `CTD_Total_Paths__c` | `total_paths` |
| `CTD_Total_2nd_Degree_Paths__c` | `total_2nd_degree_paths` |
| `CTD_Score__c` | `ctd_score` |
| `CTD_Score_Scaled__c` | `ctd_score_scaled` |
| `CTD_Score_Label__c` | `ctd_score_label` |
| `CTD_Is_Target_Person__c` | `is_target_person` |

---

## Troubleshooting

| Symptom | Likely cause | Fix |
|---------|----------------|-----|
| Flow runs, no email | `toAddress` blank or deliverability | Set Owner email in Flow; check Deliverability |
| Flow fault on Apex | Named Credential missing/wrong name | Create `CTD_API_NC` exactly |
| 401 on callout | Personal key instead of Enterprise key; missing `ctd-client-id` | Create **Enterprise** key on [API keys](https://app.ctd.ai/account/api-access/api-keys); fix External Credential headers |
| Fields stay blank | Person not in CTD graph; bad LinkedIn URL | Use full `linkedin.com/in/...` URL; confirm person in CTD |
| Flow never runs | LinkedIn blank; wrong object; formula uses wrong field | Fix Part B/D entry criteria |
| Layout deploy failed | Drips uses different layout/page names | Deploy without layout/flexipage; edit UI manually |
| AEs see empty CTD fields | FLS | Assign `CTD_Contact_Enrichment_Fields` |

---

## Support contacts

| Topic | Contact |
|-------|---------|
| CTD API key, workspace, graph coverage | CTD Customer Success / your CTD account team |
| Salesforce deploy, Flow, Named Credential | **Miller** (`miller@drips.com`) |
| Reference implementation / package updates | CTD Product (Aleks Kocic, `aleks@ctd.ai`) |

---

## Related CTD internal docs

- One-pager (HTML, this folder): `Contact-LinkedIn-CTD-Enrichment-One-Pager.html`
- One-pager (markdown): `_workspaces/aleksandrakocic/Contact-LinkedIn-CTD-Enrichment-One-Pager.md`
- Source project: `_workspaces/aleksandrakocic/Drips-CTD-Chicago/sf-flow-ctd-email/`
- CTD API reference: `200-product/api-docs.md`

*Last updated: 2026-05-18 — Play 1, Drips client rollout*
