> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.vitablehealth.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.vitablehealth.com/_mcp/server.

# Enrollment Lifecycle

> How enrollments move through statuses and which webhook events fire at each transition.

An enrollment represents an employee's participation in a benefit plan. Each enrollment moves through a defined set of statuses as the employee selects, confirms, or declines coverage. Vitable fires webhook events at every transition, giving your system a real-time view of where each enrollment stands.

Enrollment statuses apply across all benefit categories: `Medical`, `Dental`, `Vision`, and `Hospital`.

## Quick Reference

This table maps every enrollment status transition to the webhook event that triggers it.

| Current Status | Event Fired                                  | New Status | What Happened                                                                                            |
| -------------- | -------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------- |
| *(none)*       | `enrollment.started`                         | `pending`  | An enrollment window opened for the employee                                                             |
| `pending`      | `enrollment.granted`                         | `pending`  | The system or an admin created the enrollment opportunity — the employee still needs to make an election |
| `pending`      | `enrollment.elected` + `enrollment.accepted` | `enrolled` | The employee selected a plan and confirmed — both events fire together from the same action              |
| `pending`      | `enrollment.waived`                          | `waived`   | The employee declined the benefit                                                                        |
| `enrolled`     | `enrollment.terminated`                      | `inactive` | Coverage has ended                                                                                       |

> **Warning**
>
> There is no `terminated` enrollment status. When an enrollment is terminated, it transitions to `inactive`. The `enrollment.terminated` event signals the transition, but the resulting status on the enrollment resource is `inactive`.

## Mapping Statuses

| Status     | Meaning                                          | Events                                                            | Transitions To       |
| ---------- | ------------------------------------------------ | ----------------------------------------------------------------- | -------------------- |
| `pending`  | Enrollment window opened, employee has not acted | `enrollment.started`, `enrollment.granted`                        | `enrolled`, `waived` |
| `enrolled` | Employee selected a plan and confirmed           | `enrollment.elected` + `enrollment.accepted` (both fire together) | `inactive`           |
| `waived`   | Employee declined the benefit                    | `enrollment.waived`                                               | —                    |
| `inactive` | Coverage has ended                               | `enrollment.terminated`                                           | —                    |

The high-level flow is:

```text
pending -> enrolled (elected + accepted fire together)
pending -> waived
enrolled -> inactive (via terminated)
```

## Understanding the Events

Vitable fires six enrollment webhook events. Each event follows the standard webhook payload format with `event_id`, `event_name`, `resource_type`, `resource_id`, and `created_at` fields. After receiving any enrollment event, fetch the current state by calling `GET /v1/enrollments/{id}` with the `resource_id` from the payload.

### Starting an Enrollment

When an employee becomes eligible for benefits and an enrollment window opens, Vitable fires `enrollment.started`. The enrollment is created with a status of `pending`. This event fires during both open enrollment periods and qualifying life event (QLE) windows.

At this point, the employee can view available plans and make a selection.

### Granting an Enrollment Opportunity

The `enrollment.granted` event fires when the system or an admin creates an enrollment opportunity for the employee. The enrollment remains in `pending` status — the employee still needs to make an election. This is distinct from `enrollment.started`, which signals the enrollment window opened.

### Electing and Confirming Coverage

When the employee selects a plan, signs, and confirms in the enrollment widget, Vitable fires both `enrollment.elected` and `enrollment.accepted` together. The enrollment moves from `pending` to `enrolled` in a single step — there is no intermediate state between selecting a plan and confirming coverage. Once enrolled, the employee has active coverage and the enrollment's `coverage_start` field reflects when coverage began.

> **Tip**
>
> After receiving `enrollment.accepted`, fetch the enrollment with `GET /v1/enrollments/{id}` to read the `employee_deduction_in_cents` and `employer_contribution_in_cents` fields. These values are set once coverage is confirmed.

### Waiving Coverage

If the employee declines a benefit, Vitable fires `enrollment.waived` and the enrollment moves to the `waived` status. The `answered_at` field on the enrollment records when the employee made this decision.

### Terminating Coverage

When active coverage ends, Vitable fires `enrollment.terminated` and the enrollment transitions to `inactive`. The enrollment's `terminated_at` and `coverage_end` fields reflect when coverage was terminated and when it officially ends.

Coverage can end for several reasons: the employee leaves the company, the employer terminates the plan, or an administrative change removes the employee's eligibility. Regardless of the cause, the resulting status is always `inactive`.

## Enrolling Through Open Enrollment

During an open enrollment period, all eligible employees receive enrollment windows for each available benefit category. The typical flow looks like this:

```mermaid
sequenceDiagram
    participant V as Vitable API
    participant P as Partner
    V--)P: enrollment.started (status: pending)
    Note over P: Employee views plans
    V--)P: enrollment.elected + enrollment.accepted (status: enrolled)
    Note over V: Deductions generated on monthly schedule
```

**Diagram summary:**

1. Vitable fires `enrollment.started` when the enrollment window opens. The enrollment is `pending`.
2. The employee selects a plan. Vitable fires both `enrollment.elected` and `enrollment.accepted` together. The enrollment moves to `enrolled`.
3. Payroll deductions are generated later on a monthly schedule — `employee.deduction_created` fires on the 1st of each month, not immediately after enrollment.

An employee may also choose to waive coverage, in which case step 2 would be replaced by an `enrollment.waived` event and the enrollment would move to `waived`.

> **Note**
>
> Employees receive separate enrollments for each benefit category they are eligible for. A single employee may trigger `enrollment.started` for `Medical`, `Dental`, `Vision`, and `Hospital` independently.

## Enrolling Through a Qualifying Life Event

A qualifying life event (QLE) — such as marriage, the birth of a child, or loss of other coverage — opens a special enrollment window outside the regular open enrollment period. The webhook event flow is the same:

```mermaid
sequenceDiagram
    participant V as Vitable API
    participant P as Partner
    Note over V,P: Qualifying life event occurs
    V--)P: enrollment.started (status: pending)
    V--)P: enrollment.elected + enrollment.accepted (status: enrolled)
    Note over V: Deductions generated on monthly schedule
```

**Diagram summary:**

1. A qualifying life event triggers a new enrollment window. Vitable fires `enrollment.started`.
2. The employee selects a plan. Vitable fires both `enrollment.elected` and `enrollment.accepted` together. The enrollment moves to `enrolled`.
3. Payroll deductions are generated later on a monthly schedule — not immediately after enrollment.

The QLE flow produces the same sequence of events as open enrollment. The difference is the trigger — a qualifying life event rather than a scheduled enrollment period.

## Adding a Dependent

Saving a dependent and covering them are separate steps. Call `POST /v1/members/{member_id}/dependents` with the dependent's name, date of birth, relationship (`Spouse` or `Child`), and address. Saving creates no coverage and does not change the member's coverage tier.

The response has the same fields as a row from `GET /v1/members/{member_id}/dependents`, read from the saved record: the dependent's own `member_id`, `primary_member_id`, name, relationship, date of birth, age, and sex at birth. It also includes `legal_dependent_id` and `created`. The endpoint returns `201` with `created: true` when it creates a new relationship, and `200` with `created: false` when the member already has this dependent, so saving the same dependent twice is safe.

Every save sets the dependent's address to the one you send. When a new relationship is created at exactly the member's street address, Vitable also adds the dependent to the member's household where it can. This step is best effort and never fails the request; check the result with `GET /v1/members/{member_id}/household`.

If a later enrollment attempt fails or is abandoned, the saved dependent stays in place, so there is nothing to clean up. This endpoint does not accept a Social Security number.

## Terminating an Enrollment

When an enrolled employee's coverage ends, the enrollment transitions from `enrolled` to `inactive`:

| From       | To         | Event                   | Notes                                                                |
| ---------- | ---------- | ----------------------- | -------------------------------------------------------------------- |
| `enrolled` | `inactive` | `enrollment.terminated` | There is no `terminated` status — the enrollment moves to `inactive` |

* The enrollment's status field reads `inactive`, not `terminated`.

> **Tip**
>
> When you receive `enrollment.terminated`, check the `coverage_end` field on the enrollment to determine the last day of active coverage. This is important for payroll integration — you may need to adjust deductions for partial coverage periods.

## Troubleshooting

**Receiving events out of order.** Webhook events are delivered at least once and may arrive out of order. Always fetch the current enrollment state with `GET /v1/enrollments/{id}` after receiving an event rather than relying on event order to track status. If you receive `enrollment.accepted` before `enrollment.elected`, the enrollment's `status` field from the API reflects the true current state.

**Duplicate events.** Your endpoint may receive the same event more than once. Use the `event_id` field for idempotency — if you have already processed an event, skip it.

**Missing events.** If your endpoint was unreachable, use `GET /v1/webhook-events` to query for events you may have missed. You can filter by `resource_type` and time range to find enrollment events. For delivery details on a specific event, call `GET /v1/webhook-events/{event_id}/deliveries`.

**Enrollment stuck in pending.** An enrollment remains `pending` until the employee makes a selection and it is processed, or the enrollment window closes. If an enrollment has been `pending` longer than expected, the employee may not have completed their enrollment yet.

## Related Webhooks

| Event                        | Description                                                                                                                                          |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enrollment.started`         | An enrollment window has opened for an employee. [Learn more](/webhooks/events)                                                                      |
| `enrollment.elected`         | An employee has selected a benefit plan. [Learn more](/webhooks/events)                                                                              |
| `enrollment.accepted`        | An enrollment has been confirmed and coverage is active. [Learn more](/webhooks/events)                                                              |
| `enrollment.granted`         | An enrollment has been granted through auto-enrollment or admin action. [Learn more](/webhooks/events)                                               |
| `enrollment.waived`          | An employee has declined a benefit. [Learn more](/webhooks/events)                                                                                   |
| `enrollment.terminated`      | Coverage has ended and the enrollment is now `inactive`. [Learn more](/webhooks/events)                                                              |
| `employee.deduction_created` | Payroll deductions generated on monthly schedule for employees with active enrollments. [Learn more](/embedded_benefits/concepts/payroll-deductions) |

## Related API Endpoints

* [`GET /v1/employees/{id}/enrollments`](/api/employees/list-enrollments) — List all enrollments for an employee
* [`POST /v1/members/{member_id}/dependents`](/api/members/create-dependent) — Save a dependent for a member
* [`GET /v1/enrollments/{id}`](/api/enrollments/get) — Retrieve a single enrollment by ID
* [`GET /v1/webhook-events`](/api/webhook-events/list) — Query webhook events to recover missed deliveries
* [`GET /v1/webhook-events/{event_id}/deliveries`](/api/webhook-events/list-deliveries) — Inspect delivery attempts for a specific event