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

# Campaign

# Campaign Service

## Add or Create Contacts in Campaign

```http theme={null}
POST /campaign/:campaign_id/contact
```

### Request Body

| Field                   | Type    | Required | Description                                                                                                                                          |
| ----------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| contact\_ids            | array   | No       | Array of existing contact IDs to add to the campaign                                                                                                 |
| contacts                | array   | No       | Array of new contacts to create and add to the campaign                                                                                              |
| allow\_duplicate\_phone | boolean | No       | Defaults to `false`. Controls how a `contacts` entry whose phone already exists is handled. See [Duplicate phone numbers](#duplicate-phone-numbers). |

Each contact in the `contacts` array should have the following fields:

| Field             | Type   | Required | Description                                                                                                                                            |
| ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| first\_name       | string | No       | Contact's first name                                                                                                                                   |
| last\_name        | string | No       | Contact's last name                                                                                                                                    |
| phone             | string | Yes      | Contact's phone number in E.164 format (e.g., +1234567890)                                                                                             |
| email             | string | No       | Contact's email address                                                                                                                                |
| additional\_notes | string | No       | Additional notes about the contact                                                                                                                     |
| custom\_fields    | object | No       | Custom field values keyed by the custom field keys defined in your workspace (Portal → Contacts → Custom Fields). See [Custom fields](#custom-fields). |
| variables         | object | No       | Per-contact call variables for this campaign enrollment. See [Variables](#variables-and-prompt-personalization).                                       |

Note: A contact already enrolled in the campaign is not enrolled again, and a
re-add never schedules a second call. If you re-submit an already-enrolled
contact in the `contacts` array with new `variables`, those variables are
refreshed on the existing enrollment, and `custom_fields` on a matched contact
are merged and updated. Only the enrollment (and any pending call) is left
untouched.

### Duplicate phone numbers

By default (`allow_duplicate_phone` omitted or `false`), a `contacts` entry
whose phone number already exists in your workspace does **not** create a
second contact. The most recently created matching contact is reused (its
`custom_fields`/`variables` are refreshed) and enrolled. If more than one
existing contact shares that phone, the newest is chosen and a
`multiple_matches` entry is added to the response `contact_warnings` array.
This default protects against a retried request creating duplicate contacts
and placing duplicate calls.

Set `allow_duplicate_phone: true` when you genuinely want a new, distinct
contact even though the phone already exists (the platform allows multiple
contacts to share a number, the same as the app UI). With it enabled, every
`contacts` entry is created as a new contact and enrolled, and its
`id` appears in `added_contact_ids`.

* The flag only affects the `contacts` array. `contact_ids` are unaffected.
* When enabled, no phone matching happens, so no `multiple_matches` entry is
  ever added to `contact_warnings`.
* Caution: because each request creates new contacts, **retrying the same
  request with `allow_duplicate_phone: true` creates additional contacts and
  places additional calls.** Use it only for genuinely distinct people, and do
  not use it as a blind retry.
* If the value is not a boolean, the request fails with a `400`.

Example: create a distinct new contact on a number that already exists.

```json theme={null}
{
  "allow_duplicate_phone": true,
  "contacts": [
    {
      "first_name": "Jane",
      "last_name": "Smith",
      "phone": "+14156648765"
    }
  ]
}
```

```json theme={null}
{
  "status": 201,
  "data": {
    "message": "Campaign contacts created successfully",
    "added_contact_ids": ["9f2c4e77-3b1a-4c22-9a8e-0d5b6f1e2a34"],
    "conversation_status": "scheduled",
    "scheduled_time": "2026-03-20T10:00:00Z",
    "campaign_id": "campaign-123"
  }
}
```

The new contact's id appears in `added_contact_ids`, and there is no
`contact_warnings` entry even though the phone was already in use.

### Variables and prompt personalization

Campaign calls support the same `{{var.KEY}}` prompt substitution as the
[direct call endpoint](/api-reference/endpoint/call). Per-contact `variables`
(inside a `contacts` entry) are stored on the campaign enrollment and
injected into **every call the campaign places to that contact** — including
calls deferred by quiet hours and later steps of a multi-step campaign. They
are not used for SMS steps.

Variables are only accepted on `contacts` entries. Contacts enrolled via
`contact_ids` carry no variables — to attach variables to an existing
contact's calls, set values on the contact's `custom_fields` instead (those
render as `{{custom.<key>}}` and apply to every call with the contact).

Constraints (same as the call endpoint): at most 50 keys, keys must match
`^[a-zA-Z][a-zA-Z0-9_]{0,62}$`, values up to 1000 characters, whole object
under 4KB serialized. Violations fail the request with a 400.

In your agent prompt or greeting, reference variables as `{{var.KEY}}`:

> `Hi {{var.lead_name}}, this is about your {{var.product}} appointment on {{var.appt_time}}.`

### Custom fields

`custom_fields` values are validated against the field definitions configured
in your workspace (Portal → Contacts → Custom Fields) and stored on the
contact itself. Stored values can be referenced in agent prompts and
greetings as `{{custom.<key>}}` — on every call with that contact, not just
campaign calls.

Validation is **drop-and-report**: keys that are not defined in your
workspace, archived, or reserved — and values that fail the field's type
validation — are dropped while the contact is still created and enrolled.
Every dropped entry is reported in the `custom_field_warnings` array of the
response, with the contact's index and phone for correlation. Warning codes:
`unknown_key`, `archived_key`, `reserved_key`, `type_mismatch`,
`option_invalid`, `too_long`, `empty_value`.

Custom fields vs variables:

|                   | `custom_fields`             | `variables`                                  |
| ----------------- | --------------------------- | -------------------------------------------- |
| Lives on          | the contact (CRM data)      | the campaign enrollment (this campaign only) |
| Prompt reference  | `{{custom.<key>}}`          | `{{var.KEY}}`                                |
| Validated against | workspace field definitions | shape rules only                             |
| Scope             | every call with the contact | calls placed by this campaign                |

### Example Request

```json theme={null}
{
  "contact_ids": ["123e4567-e89b-12d3-a456-426614174000"],
  "contacts": [
    {
      "first_name": "John",
      "last_name": "Doe",
      "phone": "+1234567890",
      "email": "john@example.com",
      "additional_notes": "VIP customer",
      "custom_fields": {
        "account_tier": "gold",
        "renewal_date": "2026-09-01"
      },
      "variables": {
        "appt_time": "Tuesday 3pm",
        "quote_amount": "1450"
      }
    }
  ]
}
```

In this example, every campaign call to John renders `{{var.appt_time}}` and
`{{var.quote_amount}}` in the agent prompt; the existing contact enrolled by
ID gets no variables.

### Response

```json theme={null}
{
  "status": 201,
  "data": {
    "message": "Campaign contacts created successfully",
    "added_contact_ids": [
      "123e4567-e89b-12d3-a456-426614174000",
      "323e4567-e89b-12d3-a456-426614174002"
    ],
    "conversation_status": "scheduled",
    "scheduled_time": "2024-03-20T10:00:00Z",
    "campaign_id": "campaign-123",
    "custom_field_warnings": [
      {
        "index": 0,
        "phone": "+1234567890",
        "key": "acount_tier",
        "code": "unknown_key",
        "message": "No such custom field is defined for this workspace"
      }
    ]
  }
}
```

`custom_field_warnings` is present only when at least one custom\_fields entry
was dropped.
