# Campaign API guide (/docs/api-reference/campaign-enrollment/guide)



Use campaign actions to connect your own trigger logic to careCycle. Customers
can be identified by their careCycle customer ID or US phone number. Every action
accepts one customer or a list of up to **1,000 input entries**.

Authentication [#authentication]

Create a key in [Developer Settings](https://app.carecycle.ai/settings/developer/api-keys&#x29;
with the &#x2A;*`campaign_enrollment.manage`** capability. Include it in `x-api-key`.
The same capability authorizes result polling. Add `campaigns.view` if your
integration needs to discover campaign IDs through the campaigns API.

All campaign and customer references are scoped to the key's organization.
Read-only playground keys cannot perform these actions.

Endpoints [#endpoints]

| Method | Path                                                     | Action                         |
| ------ | -------------------------------------------------------- | ------------------------------ |
| POST   | `/v2/campaigns/{campaignId}/enrollment`                  | Add customers                  |
| POST   | `/v2/campaigns/{campaignId}/enrollment/restart`          | Restart existing enrollments   |
| POST   | `/v2/campaigns/{campaignId}/enrollment/status`           | Set terminal enrollment status |
| GET    | `/v2/campaigns/{campaignId}/enrollment/requests/{jobId}` | Read progress and results      |

Identify customers [#identify-customers]

Each reference must contain exactly one field:

```json
{ "customerId": "223e4567-e89b-42d3-a456-426614174000" }
```

```json
{ "phoneNumber": "+12025550123" }
```

US numbers such as `2025550123`, `12025550123`, and `(202) 555-0123` are
normalized to `+12025550123`. International numbers outside the `+1` numbering
format are not supported by this lookup.

Customers must already exist. These endpoints do not create customers or restore
deleted records. A missing, deleted, or other-organization customer produces
`customer_not_found`. Malformed customer entries fail individually with
`invalid_customer_reference` and field-level `validationErrors`. Valid entries
continue processing. For example, two malformed entries in a 1,000-entry list
produce two item failures while the other 998 are processed normally.

The request still returns `202` with a job ID, even if all customer entries are
invalid. Poll for results and correlate failures using each item's zero-based
`index`. Request-level errors, such as an invalid campaign ID, providing both
`customer` and `customers`, an empty or oversized list, or an unsupported terminal
status, reject the whole request with `422`.

Provide &#x2A;*either `customer` or `customers`**, never both. A list may mix IDs and
phone numbers. If multiple references resolve to the same customer, its action
runs once. Results retain every input position and identify duplicates with
`duplicateOf`, the zero-based index of the first reference.

Add one customer [#add-one-customer]

```bash
curl --request POST \
  "https://api.carecycle.ai/v2/campaigns/$CAMPAIGN_ID/enrollment" \
  --header "x-api-key: $CARECYCLE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "customer": { "phoneNumber": "+12025550123" }
  }'
```

Both individual and list requests return **HTTP 202 Accepted** with a job ID:

```json
{
  "jobId": "123e4567-e89b-42d3-a456-426614174000",
  "statusUrl": "/v2/campaigns/423e4567-e89b-42d3-a456-426614174000/enrollment/requests/123e4567-e89b-42d3-a456-426614174000",
  "total": 1,
  "resolvedCount": 1
}
```

`total` counts input entries. `resolvedCount` counts unique existing customers
that matched. Acceptance does not mean the workflow has executed or a call has
been placed. Even requests where no customers match return a job ID with final
per-item failures available through polling.

Change status for a list [#change-status-for-a-list]

POST to `/v2/campaigns/{campaignId}/enrollment/status`:

```json
{
  "customers": [
    { "customerId": "223e4567-e89b-42d3-a456-426614174000" },
    { "phoneNumber": "+12025550124" }
  ],
  "status": "cancelled",
  "reason": "Outreach completed in our CRM"
}
```

Allowed statuses are `completed`, `failed`, and `cancelled`. `reason` is optional
and limited to 255 characters. This changes the customer's enrollment, not the
campaign itself or the customer's overall CRM status. Customers not enrolled in
this campaign are skipped.

Setting a terminal status stops future campaign work and removes queued campaign
events. It does not hang up a call already in progress or cancel separately
scheduled appointments or inquiry callbacks.

Restart [#restart]

POST to `/v2/campaigns/{campaignId}/enrollment/restart`:

```json
{
  "customer": { "phoneNumber": "+12025550123" }
}
```

Restart requires an existing enrollment and a Pulse workflow. It reuses the
enrollment record, clears workflow execution and failure state, and starts from
the currently published workflow's entry node when the campaign is active.
Customers not enrolled are skipped. Historical calls and enrollment history are
preserved. Restart is not a full reset of customer data, call attempts, or
appointment-booked information.

Adding a customer who is already active returns `already_active`. Adding someone
whose enrollment has finished returns `terminal_exists`. Neither operation
implicitly restarts them; use the restart endpoint explicitly.

Campaign restrictions [#campaign-restrictions]

* Active campaigns can execute newly enrolled or restarted customers.
* Draft and paused campaigns accept enrollment, but workflow execution waits for
  the campaign to become active.
* Completed, cancelled, archived, and deleted campaigns cannot receive new
  enrollments or restarts.
* Receptionist campaign enrollments are managed automatically and cannot be
  changed through these endpoints.
* Customers can participate in multiple campaigns at once. Enrolling or
  restarting a customer in one campaign preserves their other enrollments and
  workflow progress.
* There is one enrollment record per customer/campaign pair. Restart does not
  create a second simultaneous enrollment in the same campaign.
* Required customer fields, workflow rules, calling hours, and call/SMS
  eligibility still apply. Enrollment does not bypass them.

Poll results [#poll-results]

GET the returned `statusUrl` with the same API-key header. Poll every **2-5 seconds**
until `status` is `completed`, `partial`, or `failed`. Polls count toward the key's
request limit.

```json
{
  "jobId": "123e4567-e89b-42d3-a456-426614174000",
  "campaignId": "423e4567-e89b-42d3-a456-426614174000",
  "action": "enroll",
  "status": "completed",
  "total": 1,
  "processed": 1,
  "succeeded": 1,
  "skipped": 0,
  "failed": 0,
  "completedAt": "2026-10-01T17:00:00.000Z",
  "items": [
    {
      "index": 0,
      "customerId": "223e4567-e89b-42d3-a456-426614174000",
      "duplicateOf": null,
      "outcome": "succeeded",
      "code": "enrolled",
      "customerCampaignId": "523e4567-e89b-42d3-a456-426614174000"
    }
  ]
}
```

Results and counts refer to input positions, including duplicates. Items remain
in input order. `pending` means an outcome is not available yet. A completed job
can contain skips; always inspect item outcomes. Processing is per customer and
is not an all-or-nothing transaction.

| Code                         | Meaning                                                                            |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| `enrolled`                   | Created an enrollment                                                              |
| `restarted`                  | Restarted an existing enrollment                                                   |
| `status_changed`             | Applied the requested terminal status                                              |
| `already_active`             | Enrollment already active; no new enrollment                                       |
| `terminal_exists`            | Enrollment already finished; explicit restart required                             |
| `not_enrolled`               | Restart/status skipped because no enrollment exists                                |
| `customer_not_found`         | No available customer in this organization                                         |
| `invalid_customer_reference` | Malformed customer entry; inspect `validationErrors` for the field and explanation |
| `missing_required_fields`    | Enrollment cannot proceed until required customer fields are populated             |
| `campaign_unavailable`       | Campaign became unavailable or ineligible before processing                        |
| `no_pulse_workflow`          | No workflow is available to restart                                                |
| `not_processed`              | The job failed before this item was attempted                                      |
| `execution_uncertain`        | An attempt was interrupted; review the enrollment before requesting another action |

Results are persisted independently of the queue's 24-hour completed-job
retention. Requests are scoped to both organization and campaign; inaccessible
job IDs return `404`.

Requests and optional retry protection [#requests-and-optional-retry-protection]

**You do not need to generate an idempotency key to use these endpoints.**
careCycle creates the request identity and returns a job ID for polling. Without
an `idempotencyKey`, each POST is a new request. A repeated add does not create
another enrollment for the same customer/campaign pair; a repeated restart is a
new restart action.

For integrations that automatically retry requests after timeouts, optionally
include an **`idempotencyKey` in the JSON body**. An existing upstream event ID
can serve as the key. The key is scoped to your organization across all campaign
action endpoints. Reuse it for retries of one logical request, and use a new key
for a new action.

```json
{
  "customer": { "phoneNumber": "+12025550123" },
  "idempotencyKey": "upstream-event-123"
}
```

* With a key, retry the same payload and key after a timeout. The original
  job ID and original resolved customer selection are reused.
* Reusing the key with a different campaign, action, customer selection, status,
  or reason returns `409 IDEMPOTENCY_CONFLICT`.
* Completed items are not executed again when a job is replayed.
* If a process fails after claiming an item but before recording its outcome,
  careCycle reports `execution_uncertain` instead of automatically restarting
  the customer again. Check the enrollment before issuing a new key.
* To retry known failed items after correcting their data, send only those
  customers in a new request. If using retry keys, supply a new key for the
  corrected payload. Do not include already-successful customers in a new restart.
* If a job remains queued because publication failed or its queue entry was
  lost, a keyed request can be republished by retrying its original POST and key.
* Without a retry key, do not blindly retry a timed-out restart: it may have
  already been accepted. If you received a job ID, poll it; otherwise check the
  enrollment before initiating another restart.

Lists above 1,000 entries are rejected rather than truncated. Split larger sets
into smaller batches and track each job separately. If using retry keys, give
each batch its own key. Respect `429` responses and their `retryAfter` value.
