# Disenroll a member or group

This playbook is for enrollment integrators who send terminations from an HR, benefits administration, or exchange system to VBA. It disenrolls one member from their current enrollment, after looking up the options and previewing the premium impact, and shows the group-level version at the end.

This changes production data
VBAPI clients connect to production, so the write calls on this page create or change real records in the database you name in `vbasoftware-database`. Try the steps in a non-production database first if VBA has given you one, use clearly fake values while you test, and keep the "Check it worked" calls, which only read data. "Try it" in the API reference also calls production.

Disenrollment is a destructive business operation
A disenrollment ends coverage and can also end family members' coverage, end future enrollments, cancel authorizations that extend past the disenroll date, and change premiums. Clients only have production access, so:

- Run the samples only for a test member you created yourself, in a non-production database if VBA has given you one.
- Preview the billing impact (step 3) before every real disenrollment.
- Re-enroll operations exist (see [Undo a disenrollment](#undo-a-disenrollment)), but whether a re-enrollment fully restores everything a disenrollment changed needs confirmation. Treat a disenrollment as hard to reverse.


|  |  |
|  --- | --- |
| **Who this is for** | Enrollment and eligibility integrators |
| **Time to run** | About 10 minutes |
| **Writes data?** | Yes. Step 4 disenrolls the member. Every other step only reads data. "Try it" in the API reference also calls production. |
| **Endpoints used** | 7 for a member, plus 2 for the group variation |


Before you start
- You have completed the [quick start](/developer-portal/get-started/quick-start) and can get an ID token.
- You know the member's `subscriber_ID` and `member_Seq`. If you only have your own identifiers, find them with the [Search API](/developer-portal/concepts/search) and the `ENROLLMENT` SearchConfig.
- You know the disenroll date and the business reason, and an SME has agreed which options below apply to your terminations.
- Your API user has rights to disenroll. Which security areas that requires needs confirmation with VBA.


## How it fits together

```mermaid
sequenceDiagram
  participant App as Your system
  participant API as VBAPI
  App->>API: GET /disenroll-family-options, GET /reasons
  App->>API: GET /subscribers/{subscriberID}/enrollments
  API-->>App: Current enrollment (group, division, dates)
  App->>API: GET .../as-of-date/{asOfDate}/disenroll-adjustments
  API-->>App: Billing adjustments the disenroll would create
  App->>API: POST /subscribers/{subscriberID}/members/{memberSeq}/disenroll
  API-->>App: VBAProcess record with the result
  App->>API: GET /subscribers/{subscriberID}/members/{memberSeq}
  API-->>App: Member with disenroll_Date
```

The samples read your credentials from environment variables:

The samples read your credentials from environment variables, so nothing secret is written into code. Set them in the terminal you run the samples from:

PowerShell
```powershell
$env:VBAPI_BASE_URL    = "https://vbapi.vbasoftware.com/vbasoftware"
$env:VBAPI_API_KEY     = "your-api-key"
$env:VBAPI_CLIENT_ID   = "your-client-id"
$env:VBAPI_CLIENT_CODE = "your-client-code"
$env:VBAPI_DATABASE    = "YOUR_DATABASE"
$env:VBAPI_USERNAME    = "your-api-username"
$env:VBAPI_PASSWORD    = "your-api-password"
$env:VBAPI_USER_AGENT  = "YourCompany-Integration/1.0"
```

bash
```bash
export VBAPI_BASE_URL="https://vbapi.vbasoftware.com/vbasoftware"
export VBAPI_API_KEY="your-api-key"
export VBAPI_CLIENT_ID="your-client-id"
export VBAPI_CLIENT_CODE="your-client-code"
export VBAPI_DATABASE="YOUR_DATABASE"
export VBAPI_USERNAME="your-api-username"
export VBAPI_PASSWORD="your-api-password"
export VBAPI_USER_AGENT="YourCompany-Integration/1.0"
```

In production code, load these from your secrets manager instead.

## The options that control a disenrollment

The disenroll request (`EnrollmentDisenroll`) has 13 required fields. Three of them decide how far the disenrollment reaches:

| Field | Values | Effect |
|  --- | --- | --- |
| `disenrollFamily` | `1` Disenroll Family, `2` Disenroll Member, `3` Disenroll Dependents, `4` Disenroll Active Family | `1` disenrolls every family member regardless of status. `2` disenrolls only the selected member; if that member is the insured, the subscriber record is disenrolled too. `3` disenrolls only the non-insured members (spouse, children, and so on). `4` disenrolls every family member, including the insured, who is currently active. |
| `disenrollFuture` | `2` or `3` | `2` also disenrolls enrollments that start after the disenroll date; `3` leaves them in place. |
| `terminateAuth` | `true` or `false` | `true` cancels authorizations approved or requested beyond the disenroll date. |


The `disenrollFamily` values come from [`GET /disenroll-family-options`](/openapi/vbasoftware/vbasoftware.api/subscribers/listdisenrollmentfamilyoptions), which returns each value with a display name and description. Which combination a typical termination uses needs SME review; the sample uses `2`, `2`, and `true` for a dependent.

The request also has optional `set...` flags (for example `setMemberEligibilityStatus`, `setPremRateMember`, and `setMemberPCP`) plus `rateSplit` and `waivePremium`. The API passes each one to VBA's disenroll process, but the spec does not describe what they do. The samples leave them unset, which sends `false`. Which of them a typical termination needs requires SME review.

## Steps

### 1. Look up the options and reasons

[`GET /disenroll-family-options`](/openapi/vbasoftware/vbasoftware.api/subscribers/listdisenrollmentfamilyoptions) and [`GET /reasons`](/openapi/vbasoftware/vbasoftware.api/reasons/listreasons)

`reason_ID` must be a reason configured in your database. Each reason has a `reason_Type` that says whether it is a standard reason or a COBRA qualifying event; [`GET /cobra-disenroll-reason-types`](/openapi/vbasoftware/vbasoftware.api/subscribers/listcobradisenrollreasontypes) lists those two types (`R` for a standard reason, `Q` for a COBRA qualifying event).

C#
```csharp
        foreach (var option in (await api.GetAsync("/disenroll-family-options")).Data?.AsArray() ?? new JsonArray())
            Console.WriteLine($"disenrollFamily {option?["option_Key"]}: {option?["display"]} - {option?["description"]}");
        foreach (var reason in (await api.GetAsync("/reasons")).Data?.AsArray() ?? new JsonArray())
            Console.WriteLine($"reason_ID {reason?["reason_ID"]}: {reason?["description"]} ({reason?["reason_Type"]})");
```

Python
```python
    for option in api.get("/disenroll-family-options").data or []:
        print(f"disenrollFamily {option['option_Key']}: {option['display']} - {option.get('description')}")
    for reason in api.get("/reasons").data or []:
        print(f"reason_ID {reason['reason_ID']}: {reason.get('description')} ({reason.get('reason_Type')})")
```

TypeScript
```typescript
  for (const option of (await api.get<any[]>("/disenroll-family-options")).data ?? []) {
    console.log(`disenrollFamily ${option.option_Key}: ${option.display} - ${option.description}`);
  }
  for (const reason of (await api.get<any[]>("/reasons")).data ?? []) {
    console.log(`reason_ID ${reason.reason_ID}: ${reason.description} (${reason.reason_Type})`);
  }
```

The samples stop after this step until you set the reason ID constant, so they cannot disenroll anyone by accident.

### 2. Find the current enrollment

[`GET /subscribers/{subscriberID}/enrollments`](/openapi/vbasoftware/vbasoftware.api/subscriber-enrollments/listsubenrollment)

The disenroll request must name the member's current (or most recent) subscriber enrollment by its `group_ID`, `division_ID`, `group_Coverage_Start`, and `start_Date`. The samples pick the enrollment with no `end_Date` and the latest `start_Date`.

C#
```csharp
        var enrollments = new List<JsonNode>();
        await foreach (var e in api.GetAllAsync($"/subscribers/{SubscriberId}/enrollments"))
            if (e is not null) enrollments.Add(e);
        var open = enrollments.Where(e => e["end_Date"] is null).ToList();
        var current = (open.Count > 0 ? open : enrollments)
            .OrderByDescending(e => e["start_Date"]!.GetValue<string>())
            .First();
        Console.WriteLine($"Current enrollment: group {current["group_ID"]}, division {current["division_ID"]}, " +
                          $"coverage start {current["group_Coverage_Start"]}, start {current["start_Date"]}");
```

Python
```python
    enrollments = list(api.iter_pages(f"/subscribers/{SUBSCRIBER_ID}/enrollments"))
    open_enrollments = [e for e in enrollments if not e.get("end_Date")]
    current = max(open_enrollments or enrollments, key=lambda e: e["start_Date"])
    print(f"Current enrollment: group {current['group_ID']}, division {current['division_ID']}, "
          f"coverage start {current['group_Coverage_Start']}, start {current['start_Date']}")
```

TypeScript
```typescript
  const enrollments: any[] = [];
  for await (const e of api.getAll(`/subscribers/${SUBSCRIBER_ID}/enrollments`)) enrollments.push(e);
  const open = enrollments.filter((e) => !e.end_Date);
  const current = (open.length > 0 ? open : enrollments).sort((a, b) => b.start_Date.localeCompare(a.start_Date))[0];
  console.log(
    `Current enrollment: group ${current.group_ID}, division ${current.division_ID}, ` +
      `coverage start ${current.group_Coverage_Start}, start ${current.start_Date}`,
  );
```

**Save from the response:** `group_ID`, `division_ID`, `group_Coverage_Start`, and `start_Date` of the current enrollment (used in step 4).

### 3. Preview the billing impact

[`GET /subscribers/{subscriberID}/as-of-date/{asOfDate}/disenroll-adjustments`](/openapi/vbasoftware/vbasoftware.api/adv.-billing/listdisenrolladjustment)

This lists the premium invoice adjustments that disenrolling the subscriber on `asOfDate` would create, without changing anything. Each item has the invoice (`invoice_Number`, `billing_From`, `billing_Thru`), the rate, and the proposed `adjustment_Amount`.

C#
```csharp
        var asOfDate = DisenrollDate[..10];
        var adjustments = (await api.GetAsync($"/subscribers/{SubscriberId}/as-of-date/{asOfDate}/disenroll-adjustments")).Data?.AsArray() ?? new JsonArray();
        foreach (var adjustment in adjustments)
            Console.WriteLine($"Invoice {adjustment?["invoice_Number"]}: adjustment {adjustment?["adjustment_Amount"]}");
```

Python
```python
    as_of_date = DISENROLL_DATE[:10]
    adjustments = api.get(f"/subscribers/{SUBSCRIBER_ID}/as-of-date/{as_of_date}/disenroll-adjustments").data or []
    for adjustment in adjustments:
        print(f"Invoice {adjustment.get('invoice_Number')}: adjustment {adjustment.get('adjustment_Amount')}")
```

TypeScript
```typescript
  const asOfDate = DISENROLL_DATE.slice(0, 10);
  const adjustments = (await api.get<any[]>(`/subscribers/${SUBSCRIBER_ID}/as-of-date/${asOfDate}/disenroll-adjustments`)).data ?? [];
  for (const adjustment of adjustments) {
    console.log(`Invoice ${adjustment.invoice_Number}: adjustment ${adjustment.adjustment_Amount}`);
  }
```

The disenroll call only adjusts premiums if you pass the adjustments in its `premInvoiceAdjustments` array (`PremInvoiceAdjustment` objects). The samples send an empty array, so they create no premium adjustments. How to turn the preview into `premInvoiceAdjustments` needs confirmation with a billing SME.

### 4. Disenroll the member

[`POST /subscribers/{subscriberID}/members/{memberSeq}/disenroll`](/openapi/vbasoftware/vbasoftware.api/adv.-enrollment/memberdisenroll)

C#
```csharp
        var request = new Dictionary<string, object?>
        {
            ["user_ID"] = Environment.GetEnvironmentVariable("VBAPI_USERNAME"), // the user the request runs on behalf of
            ["subscriber_ID"] = SubscriberId,
            ["member_Seq"] = MemberSeq,
            ["subscriber"] = false, // true only when MemberSeq is the insured (the subscriber)
            // Copied from the current SubEnrollment record:
            ["group_ID"] = current["group_ID"]!.GetValue<string>(),
            ["division_ID"] = current["division_ID"]!.GetValue<string>(),
            ["group_Coverage_Start"] = current["group_Coverage_Start"]!.GetValue<string>(),
            ["start_Date"] = current["start_Date"]!.GetValue<string>(),
            ["disenroll_Date"] = DisenrollDate,
            ["term_Date"] = DisenrollDate, // informational only
            ["reason_ID"] = ReasonId,
            ["disenrollFamily"] = DisenrollMember,
            ["disenrollFuture"] = DisenrollFuture,
            ["terminateAuth"] = true, // cancel auths approved or requested beyond the disenroll date
            ["premRates"] = Array.Empty<int>(), // send empty lists, not null
            ["premInvoiceAdjustments"] = Array.Empty<object>(),
        };
        var process = (await api.PostAsync($"/subscribers/{SubscriberId}/members/{MemberSeq}/disenroll", request)).Data!;
        Console.WriteLine($"VBAProcess {process["vbaProcess_Key"]} ({process["vbaProcess_Type"]}): " +
                          $"result={process["process_Result"]} ended={process["process_End"]}");
```

Python
```python
    request = {
        "user_ID": os.environ["VBAPI_USERNAME"],  # the user the request runs on behalf of
        "subscriber_ID": SUBSCRIBER_ID,
        "member_Seq": MEMBER_SEQ,
        "subscriber": False,  # true only when MEMBER_SEQ is the insured (the subscriber)
        # Copied from the current SubEnrollment record:
        "group_ID": current["group_ID"],
        "division_ID": current["division_ID"],
        "group_Coverage_Start": current["group_Coverage_Start"],
        "start_Date": current["start_Date"],
        "disenroll_Date": DISENROLL_DATE,
        "term_Date": DISENROLL_DATE,  # informational only
        "reason_ID": REASON_ID,
        "disenrollFamily": DISENROLL_MEMBER,
        "disenrollFuture": DISENROLL_FUTURE,
        "terminateAuth": True,  # cancel auths approved or requested beyond the disenroll date
        "premRates": [],  # send empty lists, not null
        "premInvoiceAdjustments": [],
    }
    process = api.post(f"/subscribers/{SUBSCRIBER_ID}/members/{MEMBER_SEQ}/disenroll", request).data
    print(f"VBAProcess {process['vbaProcess_Key']} ({process['vbaProcess_Type']}): "
          f"result={process.get('process_Result')} ended={process.get('process_End')}")
```

TypeScript
```typescript
  const request = {
    user_ID: process.env.VBAPI_USERNAME, // the user the request runs on behalf of
    subscriber_ID: SUBSCRIBER_ID,
    member_Seq: MEMBER_SEQ,
    subscriber: false, // true only when MEMBER_SEQ is the insured (the subscriber)
    // Copied from the current SubEnrollment record:
    group_ID: current.group_ID,
    division_ID: current.division_ID,
    group_Coverage_Start: current.group_Coverage_Start,
    start_Date: current.start_Date,
    disenroll_Date: DISENROLL_DATE,
    term_Date: DISENROLL_DATE, // informational only
    reason_ID: REASON_ID,
    disenrollFamily: DISENROLL_MEMBER,
    disenrollFuture: DISENROLL_FUTURE,
    terminateAuth: true, // cancel auths approved or requested beyond the disenroll date
    premRates: [], // send empty lists, not null
    premInvoiceAdjustments: [],
  };
  const vbaProcess = (await api.post(`/subscribers/${SUBSCRIBER_ID}/members/${MEMBER_SEQ}/disenroll`, request)).data;
  console.log(
    `VBAProcess ${vbaProcess.vbaProcess_Key} (${vbaProcess.vbaProcess_Type}): ` +
      `result=${vbaProcess.process_Result} ended=${vbaProcess.process_End}`,
  );
```

The request body, as JSON (the reason ID is a placeholder):

```json
{
  "user_ID": "your-api-username",
  "subscriber_ID": "DOCS-0001",
  "member_Seq": "02",
  "subscriber": false,
  "group_ID": "DOCS-GRP1",
  "division_ID": "DOCS-DIV1",
  "group_Coverage_Start": "2030-01-01T00:00:00",
  "start_Date": "2030-01-01T00:00:00",
  "disenroll_Date": "2030-06-30T00:00:00",
  "term_Date": "2030-06-30T00:00:00",
  "reason_ID": 1,
  "disenrollFamily": 2,
  "disenrollFuture": 2,
  "terminateAuth": true,
  "premRates": [],
  "premInvoiceAdjustments": []
}
```

Notes on the fields:

- `user_ID` is the user the disenrollment runs on behalf of. The samples use your API username.
- `subscriber` is `true` only when the member is the insured (the subscriber), and `false` for a spouse, dependent, or other member.
- `term_Date` is informational and used in some reports; it is usually the same as `disenroll_Date`.
- `premRates` lists COBRA rate keys (`PremRate_Key` values) when the disenrollment moves the member to COBRA. Send empty arrays rather than `null` for `premRates` and `premInvoiceAdjustments`.


The call waits for the disenroll process to finish. It returns `200` with a `VBAProcess` record: `vbaProcess_Key`, `vbaProcess_Type`, `process_Start`, `process_End`, and `process_Result`. Disenrolling can also trigger process automation that is configured in your database for the disenroll result. Which `process_Result` values mean success needs confirmation.

Example response (illustrative; field values are fake):

```json
{
  "data": {
    "vbaProcess_Key": 1001,
    "vbaProcess_Type": "DISENROLL",
    "description": "Disenroll Subscriber/Member.",
    "process_User": "your-api-username",
    "process_Start": "2030-06-01T10:00:00",
    "process_End": "2030-06-01T10:00:02",
    "process_Result": "DOCS-RESULT"
  },
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}
```

**Save from the response:** `vbaProcess_Key` and `vbaProcess_Type`. You can read the record again later with [`GET /vbaprocesses/{vBAProcessKey}/types/{vBAProcessType}`](/openapi/vbasoftware/vbasoftware.api/processes/getvbaprocess).

### 5. Verify

[`GET /subscribers/{subscriberID}/members/{memberSeq}`](/openapi/vbasoftware/vbasoftware.api/members/getmembers)

C#
```csharp
        var member = (await api.GetAsync($"/subscribers/{SubscriberId}/members/{MemberSeq}")).Data!;
        Console.WriteLine($"Member {SubscriberId}-{MemberSeq}: disenroll_Date={member["disenroll_Date"]}");
```

Python
```python
    member = api.get(f"/subscribers/{SUBSCRIBER_ID}/members/{MEMBER_SEQ}").data
    print(f"Member {SUBSCRIBER_ID}-{MEMBER_SEQ}: disenroll_Date={member.get('disenroll_Date')}")
```

TypeScript
```typescript
  const member = (await api.get(`/subscribers/${SUBSCRIBER_ID}/members/${MEMBER_SEQ}`)).data;
  console.log(`Member ${SUBSCRIBER_ID}-${MEMBER_SEQ}: disenroll_Date=${member.disenroll_Date}`);
```

**Check it worked:** the member's `disenroll_Date` is set. If you disenrolled the subscriber, [`GET /subscribers/{subscriberID}/enrollments`](/openapi/vbasoftware/vbasoftware.api/subscriber-enrollments/listsubenrollment) should also show an `end_Date` on the enrollment. Exactly which records the disenroll process updates needs confirmation.

## Disenroll a whole group or division

[`POST /groups/{groupID}/disenroll`](/openapi/vbasoftware/vbasoftware.api/adv.-groups/groupdisenroll) disenrolls a group, or one division of it, and returns a `VBAProcess` record like the member version. This affects every member in the group, so check the impact first with [`GET /groups/{groupID}/disenroll-affected-claims`](/openapi/vbasoftware/vbasoftware.api/group-disenroll-affected-claims/listgroupdisenrollaffectedclaims), which takes a required `disenrollDate` query parameter and lists the claims the disenrollment would affect.

The body is `EnrollmentDisenrollGroup`. Its required fields are `user_ID`, `group_ID`, `disenroll_Date`, `reason_ID`, `disenrollFamily`, `terminateAuth`, `disenrollFuture`, and `groupDisenroll`. Example body (the reason ID is a placeholder):

```json
{
  "user_ID": "your-api-username",
  "group_ID": "DOCS-GRP1",
  "disenroll_Date": "2030-06-30T00:00:00",
  "term_Date": "2030-06-30T00:00:00",
  "reason_ID": 1,
  "disenrollFamily": 1,
  "disenrollFuture": 2,
  "terminateAuth": true,
  "groupDisenroll": true
}
```

To disenroll a single division, add `division_ID`. The spec describes `groupDisenroll` as "set this value to true if you intend to disenroll the group", and says that when `division_ID` is sent, the flag disenrolls the group and that division but not other divisions. That wording needs confirmation before you rely on it. The group samples are not included in the runnable files, because a group disenrollment should never be run as a test.

## Undo a disenrollment

[`POST /subscribers/{subscriberID}/members/{memberSeq}/reenroll`](/openapi/vbasoftware/vbasoftware.api/adv.-enrollment/memberreenroll) and [`POST /groups/{groupID}/reenroll`](/openapi/vbasoftware/vbasoftware.api/adv.-groups/groupreenroll) re-enroll a member or group and also return a `VBAProcess` record. The re-enroll request (`EnrollmentReenroll`) has its own family options, from [`GET /reenroll-family-options`](/openapi/vbasoftware/vbasoftware.api/subscribers/listreenrollmentfamilyoptions), and flags such as `resetAuth`. Whether a re-enrollment restores cancelled authorizations, premium adjustments, and future enrollments needs confirmation, so do not plan on it as an undo button.

## Common errors

| Symptom | Likely cause | Fix |
|  --- | --- | --- |
| Validation error naming a required field | One of the 13 required fields is missing. `subscriber`, `terminateAuth`, `disenrollFamily`, and `disenrollFuture` have no defaults. | Send every required field. |
| Error about `reason_ID` | The reason is not configured in your database. | Pick a `reason_ID` from `GET /reasons`. |
| Error about the enrollment | `group_ID`, `division_ID`, `group_Coverage_Start`, or `start_Date` does not match the current subscriber enrollment. | Copy them from `GET /subscribers/{subscriberID}/enrollments`. |
| Unexpected family members disenrolled | `disenrollFamily` was `1` or `4`, or the member was the insured with `2`. | Use `3` for dependents only, or `2` with a non-insured member. |


Contacting support
Send VBA support:

- the `debug.activityID` from the response
- the date and time of the call, with time zone
- the method and path, for example `POST /claim-batches`
- the full `error` object from the response
- the request body, with passwords, tokens, and personal data removed


Never send your password, API key, or tokens. See [Support and escalation](/developer-portal/help/support).

## Variations

- **COBRA:** pick a `reason_ID` whose `reason_Type` is a COBRA qualifying event, and pass the COBRA rate keys in `premRates`. The COBRA steps need SME review before they are documented here.
- **Safe to re-run?** No. Running step 4 again for an already disenrolled member does not restore the previous state. Check the member's `disenroll_Date` first.


## Download

The full runnable samples are in the portal repository under `_samples/playbooks/disenroll-member/`: `DisenrollMember.cs` (run with the shared C# runner as `DisenrollMemberPlaybook`), `disenroll_member.py`, and `disenrollMember.ts`. Each uses the shared client helper for its language and refuses to disenroll until you set a reason ID.

## Related

- Concepts: [Data model](/developer-portal/concepts/data-model), [IDs, keys and field names](/developer-portal/concepts/ids-and-conventions)
- Previous playbook: [Enroll employees and dependents](/developer-portal/playbooks/enroll-employees)
- Terms: [Glossary](/developer-portal/reference/glossary)
- Ask the AI: "Using the Disenroll a member or group playbook, explain what disenrollFamily 2 does when the member is the subscriber."