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

# Enroll contacts with preflight

> Check contacts, choose how to handle existing sequence enrollments, and enroll them in a multichannel sequence.

Before you enroll contacts, create a preflight. A preflight checks whether the selected contacts are already in another sequence or have replied. It returns the available choices without changing any enrollments.

You then confirm one of two actions: skip contacts that need a decision, or move them into this sequence. If you plan to move contacts, you can preview the result before confirming.

<Warning>
  The direct enrollment endpoint is deprecated. Use preflight and confirmation for new integrations.
</Warning>

## Prerequisites

Prepare these values before you start:

* `WORKSPACE_ID`
* `SEQUENCE_ID` for the sequence you want to enroll contacts into
* `SALESFORGE_API_KEY`
* the contact filters or contact IDs you want to use

Send the raw API key in the `Authorization` header:

```http theme={null}
Authorization: YOUR_API_KEY
```

For the shared authentication pattern, see [Authentication](/authentication).

## Select contacts

You must provide at least one contact filter or set `limit` to a number greater than zero.

<Accordion title="Available contact filters">
  * Contact IDs: `leadIds`, `notInLeadIds`
  * Tag IDs: `tagIds`, `notInTagIds`
  * Email service providers: `esps`, `notInESPs`
  * Custom variable values: `customVars`, `notInCustomVars`
  * Custom variable IDs: `customVarIds`, `notInCustomVarIds`
  * Contact search: `searchQuery`
  * Email validation: `validationStatuses`, `validationRunId`
  * Contact state: `excludeContacted`, `hasValidLinkedIn`, `hasEmail`
</Accordion>

Use `limit` to set the maximum number of contacts the preflight checks.

## Enrollment flow

```mermaid theme={null}
flowchart LR
  A[Check contacts] --> B{Contacts need a decision?}
  B -->|No| C[Confirm enrollment]
  B -->|Yes, skip them| C
  B -->|Yes, move them| D[Preview move]
  D --> C
```

<Steps>
  <Step title="Check the contacts">
    Create a preflight for the contacts you want to enroll.

    `POST /multichannel/workspaces/{workspaceID}/sequences/{sequenceID}/enrollments/preflight`

    <Tabs>
      <Tab title="Contact IDs">
        ```json theme={null}
        {
          "filters": {
            "leadIds": [
              "CONTACT_ID_1",
              "CONTACT_ID_2",
              "CONTACT_ID_3"
            ]
          },
          "limit": 3,
          "selectionScope": "all"
        }
        ```
      </Tab>

      <Tab title="Validation run">
        ```json theme={null}
        {
          "filters": {
            "validationRunId": "VALIDATION_RUN_ID"
          },
          "limit": 100,
          "selectionScope": "not_in_sequence"
        }
        ```
      </Tab>
    </Tabs>

    `selectionScope` controls which matching contacts are included:

    * `all` includes all matching contacts.
    * `in_sequence` includes contacts with at least one enrollment that could produce a preflight decision: any enrollment in a draft sequence, or an enrollment with status `active`, `paused`, `out_of_office`, `failed`, `company_limit_reached`, or `replied`.
    * `not_in_sequence` includes contacts without one of those decision-producing enrollments. A contact whose only enrollments are `completed`, `dnc`, `unsubscribed`, or `bounced` is included in this scope unless one of those enrollments belongs to a draft sequence.

    If you omit `selectionScope`, it defaults to `all`.

    <Note>
      A `validationRunId` must belong to the workspace, be completed, and contain at least one contact. If you also provide `leadIds`, the API keeps only contacts present in both selections.
    </Note>
  </Step>

  <Step title="Read the result">
    The response shows how many contacts can be enrolled and which contacts need a decision:

    ```json theme={null}
    {
      "preflightId": "2f4f75ad-2527-4e89-b0dc-2cd75589c64f",
      "expiresAt": "2026-08-18T13:15:00Z",
      "summary": {
        "candidateCount": 4,
        "decisionRequiredCount": 2,
        "automaticEnrollmentCount": 1,
        "alreadyInTargetCount": 1
      },
      "moveGroups": [
        {
          "sequenceId": 42,
          "sequenceName": "Existing outbound",
          "sequenceStatus": "active",
          "selectedContactCount": 2
        }
      ],
      "repliedDecision": {
        "contactCount": 1
      }
    }
    ```

    | Field                          | Meaning                                                          |
    | ------------------------------ | ---------------------------------------------------------------- |
    | `candidateCount`               | Contacts checked by the preflight.                               |
    | `decisionRequiredCount`        | Contacts that are already in another sequence or have replied.   |
    | `automaticEnrollmentCount`     | Contacts that can be enrolled without changing another sequence. |
    | `alreadyInTargetCount`         | Contacts already enrolled in this sequence.                      |
    | `moveGroups`                   | Sequences that contain contacts you can move.                    |
    | `repliedDecision.contactCount` | Contacts that have replied in another sequence.                  |

    A contact may appear in more than one move group. For that reason, move-group contact counts may overlap.

    Save `preflightId` for the next request. The preflight expires at `expiresAt`, 15 minutes after creation.
  </Step>

  <Step title="Choose skip or move">
    Choose the action that matches what you want to do:

    * **Skip** enrolls contacts that do not need a decision. It leaves the other contacts in their current sequences.
    * **Move** enrolls eligible contacts in this sequence and updates their enrollment in the sequences you select.

    To move contacts, select the relevant `sequenceId` values from `moveGroups`. If a contact appears in several required groups, select all of them. Otherwise, the API skips that contact.

    When a move is confirmed, a draft source enrollment is deleted. Any other selected source enrollment is set to `completed`, and its pending steps are canceled.
  </Step>

  <Step title="Preview a move">
    This step is optional. It shows the expected result without changing any enrollments.

    `POST /multichannel/workspaces/{workspaceID}/sequences/{sequenceID}/enrollments/preflight/{preflightID}/move-preview`

    ```json theme={null}
    {
      "moveSourceSequenceIds": [42],
      "skipReplied": true
    }
    ```

    You must set `skipReplied`:

    * `true` does not enroll contacts that have replied.
    * `false` allows them to be enrolled if you selected all required sequences.

    The response shows the expected outcome:

    ```json theme={null}
    {
      "summary": {
        "enrolledCount": 2,
        "skippedCount": 1,
        "sourceCleanupCount": 1,
        "alreadyInTargetCount": 1
      },
      "skipBreakdown": {
        "repliedCount": 1,
        "unselectedSourceCount": 0
      }
    }
    ```

    `sourceCleanupCount` is the number of contacts whose enrollment in another sequence will be updated. `skipBreakdown` shows whether contacts are skipped because they replied or because you did not select every required sequence.
  </Step>

  <Step title="Apply the decision">
    Confirm the preflight with `skip` or `move`.

    `POST /multichannel/workspaces/{workspaceID}/sequences/{sequenceID}/enrollments/preflight/{preflightID}/confirm`

    <Tabs>
      <Tab title="Enroll eligible contacts">
        ```json theme={null}
        {
          "action": "skip"
        }
        ```
      </Tab>

      <Tab title="Move contacts">
        ```json theme={null}
        {
          "action": "move",
          "moveSourceSequenceIds": [42],
          "skipReplied": true
        }
        ```
      </Tab>
    </Tabs>

    `skipReplied` is required for `move`. It is not required for `skip`.

    A successful response contains the enrolled contact IDs and the final counts:

    ```json theme={null}
    {
      "enrolledLeadIds": [
        "CONTACT_ID_1",
        "CONTACT_ID_2"
      ],
      "summary": {
        "enrolledCount": 2,
        "skippedCount": 1,
        "sourceCleanupCount": 1,
        "alreadyInTargetCount": 1
      }
    }
    ```

    Preview returns expected counts. Confirmation returns the counts that were actually applied.

    <Warning>
      A contact suppressed elsewhere by do-not-contact, unsubscribe, or bounce shield is enrolled with that status and is never contacted. The response still includes the contact in `enrolledLeadIds` and `enrolledCount`, without a field that distinguishes it from a contactable enrollment. Do not treat `enrolledCount` as the number of contactable contacts.
    </Warning>
  </Step>
</Steps>

## If the preflight changes or expires

Contact enrollments can change after you create a preflight. If this happens, preview or confirmation returns `409` with `message` set to `preflight_stale`.

The `data` field contains a new preflight:

```json theme={null}
{
  "message": "preflight_stale",
  "data": {
    "preflightId": "9a650383-6a70-44d6-8ba7-af366065cf89",
    "expiresAt": "2026-08-18T13:20:00Z",
    "summary": {
      "candidateCount": 4,
      "decisionRequiredCount": 0,
      "automaticEnrollmentCount": 3,
      "alreadyInTargetCount": 1
    },
    "moveGroups": [],
    "repliedDecision": {
      "contactCount": 0
    }
  }
}
```

Review the new result and use `data.preflightId` for the next preview or confirmation request.

An expired preflight returns `404`. Create another preflight with the same contact selection. A `422` response with `insufficient remaining active credits balance` means confirmation would exceed the account contact limit. A `423` response with `enrollment_confirmation_busy` means another enrollment is being confirmed; try again after it finishes.

## If a validation run is not ready

When you use `validationRunId`, the run must be completed and contain at least one contact.

| Validation run             | Error                                       | What to do                                                   |
| -------------------------- | ------------------------------------------- | ------------------------------------------------------------ |
| Pending or in progress     | `409` with `validation_run_not_completed`   | Wait for the run to complete, then create another preflight. |
| Failed                     | `409` with `validation_run_failed`          | Start or select another validation run.                      |
| Completed with no contacts | `400` with `validation_run_selection_empty` | Change the validation selection or contact filters.          |

## Related resources

* [Create a multichannel sequence](/products/salesforge/create-multichannel-sequence)
* [Multichannel Sequence (V2)](/salesforge-api/concepts/multichannel-sequence)
* [Authentication](/authentication)
* [API endpoints](/salesforge-api/api-endpoints)
