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

# Add Lead to Campaign Endpoint

> Add a lead to a specific campaign and start that lead through the sequence

## Endpoint

* **Method**: `POST`
* **Path**: `/api/campaigns/{id}/leads`

`{id}` is the campaign ID in Tuco. Copy the campaign ID from the campaign detail page in the app (copy button next to the ID).

***

## Quick start (copy-paste example)

**Request (existing lead):**

```http theme={null}
POST https://app.tuco.ai/api/campaigns/{campaignId}/leads
Content-Type: application/json
Authorization: Bearer <API_KEY>
```

```json theme={null}
{
  "leadId": "507f1f77bcf86cd799439011"
}
```

**Request (create new lead and add to campaign):**

```json theme={null}
{
  "lead": {
    "firstName": "Jane",
    "lastName": "Doe",
    "phone": "+14155551234",
    "email": "jane@example.com"
  }
}
```

**Response (201):**

```json theme={null}
{
  "success": true,
  "leadId": "507f1f77bcf86cd799439011",
  "campaignId": "507f1f77bcf86cd799439022",
  "messageCount": 1,
  "message": "Lead added to campaign and sequence started"
}
```

***

## Use Cases

* Enroll an existing lead into an iMessage campaign from your backend or CRM.
* Create a new lead and add them to a running or scheduled campaign in a single call.

***

## Request Body (Existing Lead)

```json theme={null}
{
  "leadId": "667f1f77bcf86cd799439012"
}
```

Tuco will:

* Validate that the lead belongs to the same workspace.
* Attach the lead to the campaign’s list (if needed).
* Create the campaign messages for that lead based on the campaign’s steps and settings.

***

## Request Body (Create & Add Lead)

```json theme={null}
{
  "lead": {
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@example.com",
    "phone": "+12025550000",
    "companyName": "Acme Corp",
    "jobTitle": "VP Sales"
  }
}
```

If you send `lead` instead of `leadId`, Tuco:

* Creates the lead in the same workspace.
* Adds it to the campaign’s underlying list.
* Starts that lead through the campaign sequence.

***

## Response

```json theme={null}
{
  "success": true,
  "leadId": "667f1f77bcf86cd799439012",
  "campaignId": "6680e6d0bcf86cd799439099",
  "messageCount": 3,
  "message": "Lead added to campaign and sequence started"
}
```

When the lead is **already enrolled** in this campaign, you receive `200` instead of `201`:

```json theme={null}
{
  "success": true,
  "leadId": "667f1f77bcf86cd799439012",
  "campaignId": "6680e6d0bcf86cd799439099",
  "messageCount": 0,
  "alreadyInCampaign": true,
  "message": "Lead already in campaign (re-entry not allowed)."
}
```

If the campaign or lead cannot be found you receive a `404`. If the payload is invalid (for example, missing both `leadId` and `lead`) you receive a `400` with a descriptive error. Authentication failures return `401`.

***

## Deduplication

When you send `lead` (rather than `leadId`), Tuco runs a workspace-wide dedup check before creating anything.

**Match rule:** any of `email`, `phone`, `altEmail1-3`, `altPhone1-3` matching an existing lead (after normalization) reuses that lead.

**Normalization:**

* `email` → trimmed and lowercased
* `phone` → E.164 (`+15551234567`)

**Important:** when an existing lead matches, the request body fields (`firstName`, `email`, `phone`, `companyName`, etc.) are **NOT** applied to it. The existing lead is reused as-is. To update a matched lead, call `PUT /api/leads/{id}` separately, then add to the campaign by `leadId`.

| Existing lead in workspace         | Already in this campaign? | Result                                                  |
| ---------------------------------- | ------------------------- | ------------------------------------------------------- |
| None                               | —                         | **201** — new lead created and enrolled                 |
| Match by phone (E.164)             | No                        | **201** — existing lead enrolled, fields *not* updated  |
| Match by email (lowercased)        | No                        | **201** — existing lead enrolled, fields *not* updated  |
| Match by `altEmail*` / `altPhone*` | No                        | **201** — alts count, existing lead enrolled            |
| Match by phone or email            | Yes                       | **200** with `alreadyInCampaign: true` (safe to retry)  |
| Phone formatted differently        | —                         | Normalized first — `9042956129` matches `+919042956129` |
| Email casing differs               | —                         | Lowercased first — `A@X.com` matches `a@x.com`          |

***

## GHL Workflow Integration (15-minute sync gap)

Tuco's GHL connector syncs contacts on a 15-minute schedule. To enroll a GHL contact into a Tuco campaign **immediately**, call this endpoint from a GHL workflow Webhook action.

### How merging works across the sync gap

When you add a lead via this API and the periodic GHL sync later sees the same contact, Tuco merges by **phone only** (`integration-sync.ts`).

| Lead state in Tuco                                    | GHL contact has  | Sync result                                                                   |
| ----------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------- |
| Lead has phone P, no `integrationIds.ghlRecordId` yet | Same phone P     | Merges: sets `ghlRecordId` on existing lead, updates fields, `$addToSet` tags |
| Lead has email match but phone differs / is missing   | Email match only | **No merge** — creates a *separate* GHL-sourced lead (gotcha)                 |
| Lead already has `integrationIds.ghlRecordId = X`     | `contact.id = X` | Normal upsert: updates fields, merges tags                                    |
| No Tuco lead exists                                   | New contact      | Inserts new lead with `source = "ghl"`                                        |

**Recommendation:** pass `ghlContactId` (and optionally `ghlLocationId`) in the request body for deterministic linking. The endpoint:

* Adds `integrationIds.ghlRecordId` to the dedup match (a second call with the same `ghlContactId` reuses the lead even if phone or email shifted).
* Backfills `integrationIds.ghlRecordId` / `ghlLocationId` onto a matched existing lead when the lead doesn't have them yet (never overwrites).

If you can't pass `ghlContactId`, always send `phone` in E.164 — the next periodic GHL sync (every 15 minutes) merges by phone. Email-only matching at sync time is **not** supported and will create a duplicate GHL-sourced lead.

The same applies for HubSpot via `hsContactId` (stored as `integrationIds.hubspotRecordId`).
