# Onboard a new employer group

This playbook is for implementation teams and integrators who set up new employer groups through VBAPI. At the end you have a new group and a division copied from a proven template, with the template's contracts, plans, networks, plan years, and premium rates, ready for enrollment.

|  |  |
|  --- | --- |
| **Who this is for** | Implementation teams and integrators onboarding employer groups |
| **Time to run** | About 20 minutes, including reading the template's setup |
| **Writes data** | Yes. Try it in a non-production database first if you have one. |
| **Operations used** | 8: [`GET /groups/{groupID}`](/openapi/vbasoftware/vbasoftware.api/groups/getgroups), [`POST /groups`](/openapi/vbasoftware/vbasoftware.api/groups/creategroups), [`GET /groups/{groupID}/divisions/{divisionID}`](/openapi/vbasoftware/vbasoftware.api/group-divisions/getgroupdivision), [`POST /groups/{groupID}/divisions/{divisionID}/copy`](/openapi/vbasoftware/vbasoftware.api/adv.-group-divisions/copygroupdivision), [`GET /groups/{groupID}/divisions/{divisionID}/contracts`](/openapi/vbasoftware/vbasoftware.api/group-contracts/listgroupcontract), [`POST /groups/{groupID}/divisions/{divisionID}/add-contract`](/openapi/vbasoftware/vbasoftware.api/adv.-group-contracts/addnextgroupcontract), [`GET /billing-cycles/{billingCycleID}`](/openapi/vbasoftware/vbasoftware.api/billing-cycles/getbillingcycle), [`GET /groups/{groupID}/divisions/{divisionID}/contracts/{coverageStart}/plans`](/openapi/vbasoftware/vbasoftware.api/group-contract-plans/listgroupcontractplan) |


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.

Before you start
- You have finished the [quick start](/developer-portal/get-started/quick-start) and can get an ID token. The samples read your credentials from environment variables; see [Set up your environment](#set-up-your-environment).
- A **template group and division** already exist in your database, set up the way you want new groups to look: contracts, plans, networks and tiers, plan years, and premium rates. Ask your VBA implementation contact which one to use.
- The API user can read and create groups, divisions, and group contracts.
- You have read the groups, divisions, and contracts part of the [data model](/developer-portal/concepts/data-model).


## How it fits together

A **group** is the employer. A **division** is a part of the group that carries its own **contracts** (coverage periods), and each contract carries the **plans** offered in that period. Copying a template division brings all of that over in one call.

```mermaid
sequenceDiagram
  participant App as Your system
  participant API as VBAPI
  App->>API: GET template group and division
  API-->>App: billing_Cycle_ID and template settings
  App->>API: POST /groups (new group)
  API-->>App: 200 group
  App->>API: POST .../divisions/{templateDivision}/copy
  API-->>App: 200 new division
  App->>API: GET .../contracts on the new division
  API-->>App: copied contracts
  App->>API: GET /billing-cycles/{billingCycleID}
  API-->>App: billing cycle
```

The sample uses these fake IDs. Change them to your own naming scheme. `group_ID` and `division_ID` allow 20 characters.

| Value | Sample | Where it comes from |
|  --- | --- | --- |
| Template group | `DOCS_TEMPLATE_GROUP_ID` environment variable | Your VBA implementation contact |
| Template division | `DOCS_TEMPLATE_DIVISION_ID` environment variable | Your VBA implementation contact |
| New group | `DOCS-GRP1` | You choose it |
| New division | `DOCS-DIV1` | You choose it |


## Steps

### 1. Read the template group and division

[`GET /groups/{groupID}`](/openapi/vbasoftware/vbasoftware.api/groups/getgroups) and [`GET /groups/{groupID}/divisions/{divisionID}`](/openapi/vbasoftware/vbasoftware.api/group-divisions/getgroupdivision)

Read the template first. This confirms the IDs are right before anything is written, and gives you the template group's billing cycle to reuse on the new group.

C#
```csharp
        var templateGroup = (await api.GetAsync($"/groups/{TemplateGroupId}")).Data!;
        var templateDivision = (await api.GetAsync($"/groups/{TemplateGroupId}/divisions/{TemplateDivisionId}")).Data!;
        var billingCycleId = templateGroup["billing_Cycle_ID"]?.GetValue<string>();
        Console.WriteLine($"Template: {templateGroup["name"]} / {templateDivision["name"]} billing cycle: {billingCycleId}");
```

Python
```python
    template_group = api.get(f"/groups/{TEMPLATE_GROUP_ID}").data
    template_division = api.get(f"/groups/{TEMPLATE_GROUP_ID}/divisions/{TEMPLATE_DIVISION_ID}").data
    billing_cycle_id = template_group["billing_Cycle_ID"]
    print("Template:", template_group["name"], "/", template_division["name"], "billing cycle:", billing_cycle_id)
```

TypeScript
```typescript
  const templateGroup = (await api.get(`/groups/${TEMPLATE_GROUP_ID}`)).data;
  const templateDivision = (await api.get(`/groups/${TEMPLATE_GROUP_ID}/divisions/${TEMPLATE_DIVISION_ID}`)).data;
  const billingCycleId: string | null = templateGroup.billing_Cycle_ID;
  console.log("Template:", templateGroup.name, "/", templateDivision.name, "billing cycle:", billingCycleId);
```

**Save from the response:** `billing_Cycle_ID` from the group (used in step 2).

### 2. Create the group

[`POST /groups`](/openapi/vbasoftware/vbasoftware.api/groups/creategroups)

The division copy writes into an existing group, so create the group first. The database links every division to its group, so the copy fails if the destination group does not exist. Skip this step if the employer already has a group.

The `Groups` schema has 19 required fields: `group_ID` and 18 booleans. This is the smallest body, with every required boolean set to `false`, plus a name and the template's billing cycle:

```json
{
  "group_ID": "DOCS-GRP1",
  "name": "Docs Sample Group",
  "billing_Cycle_ID": "M1",
  "admin_COBRA": false,
  "advice_To_Pay": false,
  "apply_Ex_Code": false,
  "auth_Not_Reqd_Override": false,
  "coPay_Max_Per_Date_Provider": false,
  "default_Process_by_Disability_Date": false,
  "default_Roll_Up_Waiting_Period": false,
  "disable_COBRA_Auto_Select": false,
  "duplicate_Claim_Override": false,
  "individual_Billing": false,
  "individual_Cycle": false,
  "individual_Dates": false,
  "open_Closed_Enrollment": false,
  "paperless": false,
  "prepaid_Group": false,
  "prorate_Deduction": false,
  "self_Admin": false,
  "set_Billing_Due_Date": false
}
```

`M1` stands for whatever `billing_Cycle_ID` the template group has. Review the booleans with the employer's setup before you go live; for example `admin_COBRA` and `paperless` change how VBASoftware treats the group. The [`Groups` schema](/openapi/vbasoftware/vbasoftware.api/groups/creategroups) describes every field.

The sample checks whether the group exists first (a `404` from `GET /groups/{groupID}` means it does not), so a second run does not try to create it again.

C#
```csharp
        if (await ExistsAsync(api, $"/groups/{NewGroupId}"))
        {
            Console.WriteLine($"Group {NewGroupId} already exists; skipping create");
        }
        else
        {
            await api.PostAsync("/groups", new
            {
                group_ID = NewGroupId,
                name = "Docs Sample Group",
                billing_Cycle_ID = billingCycleId,
                // Required booleans. false is the safe default for a new group.
                admin_COBRA = false,
                advice_To_Pay = false,
                apply_Ex_Code = false,
                auth_Not_Reqd_Override = false,
                coPay_Max_Per_Date_Provider = false,
                default_Process_by_Disability_Date = false,
                default_Roll_Up_Waiting_Period = false,
                disable_COBRA_Auto_Select = false,
                duplicate_Claim_Override = false,
                individual_Billing = false,
                individual_Cycle = false,
                individual_Dates = false,
                open_Closed_Enrollment = false,
                paperless = false,
                prepaid_Group = false,
                prorate_Deduction = false,
                self_Admin = false,
                set_Billing_Due_Date = false,
            });
            Console.WriteLine($"Created group {NewGroupId}");
        }
```

Python
```python
    if exists(api, f"/groups/{NEW_GROUP_ID}"):
        print("Group", NEW_GROUP_ID, "already exists; skipping create")
    else:
        api.post("/groups", {
            "group_ID": NEW_GROUP_ID,
            "name": "Docs Sample Group",
            "billing_Cycle_ID": billing_cycle_id,
            # Required booleans. false is the safe default for a new group.
            "admin_COBRA": False,
            "advice_To_Pay": False,
            "apply_Ex_Code": False,
            "auth_Not_Reqd_Override": False,
            "coPay_Max_Per_Date_Provider": False,
            "default_Process_by_Disability_Date": False,
            "default_Roll_Up_Waiting_Period": False,
            "disable_COBRA_Auto_Select": False,
            "duplicate_Claim_Override": False,
            "individual_Billing": False,
            "individual_Cycle": False,
            "individual_Dates": False,
            "open_Closed_Enrollment": False,
            "paperless": False,
            "prepaid_Group": False,
            "prorate_Deduction": False,
            "self_Admin": False,
            "set_Billing_Due_Date": False,
        })
        print("Created group", NEW_GROUP_ID)
```

TypeScript
```typescript
  if (await exists(api, `/groups/${NEW_GROUP_ID}`)) {
    console.log(`Group ${NEW_GROUP_ID} already exists; skipping create`);
  } else {
    await api.post("/groups", {
      group_ID: NEW_GROUP_ID,
      name: "Docs Sample Group",
      billing_Cycle_ID: billingCycleId,
      // Required booleans. false is the safe default for a new group.
      admin_COBRA: false,
      advice_To_Pay: false,
      apply_Ex_Code: false,
      auth_Not_Reqd_Override: false,
      coPay_Max_Per_Date_Provider: false,
      default_Process_by_Disability_Date: false,
      default_Roll_Up_Waiting_Period: false,
      disable_COBRA_Auto_Select: false,
      duplicate_Claim_Override: false,
      individual_Billing: false,
      individual_Cycle: false,
      individual_Dates: false,
      open_Closed_Enrollment: false,
      paperless: false,
      prepaid_Group: false,
      prorate_Deduction: false,
      self_Admin: false,
      set_Billing_Due_Date: false,
    });
    console.log(`Created group ${NEW_GROUP_ID}`);
  }
```

**Save from the response:** `group_ID` (the new group; used in step 3 as `destinationGroupID`).

**Check it worked:** `GET /groups/{groupID}` with `DOCS-GRP1` returns `200` with the group.

### 3. Copy the template division

[`POST /groups/{groupID}/divisions/{divisionID}/copy`](/openapi/vbasoftware/vbasoftware.api/adv.-group-divisions/copygroupdivision)

The path names the **template** group and division. The body repeats them as `groupID` and `sourceDivisionID` (they must match the path), and names the new group and division as `destinationGroupID` and `destinationDivisionID`. The new division takes the template division's settings; the copy flags choose which related records come with it.

```json
{
  "groupID": "TEMPLATE-GRP",
  "sourceDivisionID": "TEMPLATE-DIV",
  "destinationGroupID": "DOCS-GRP1",
  "destinationDivisionID": "DOCS-DIV1",
  "copyContracts": "1",
  "copyPlans": "1",
  "copyRiders": "1",
  "copyNetworksTiers": "1",
  "copyPlanYears": "1",
  "copyPremiums": "1",
  "copyAdditional": "0",
  "copyClassOptions": "0",
  "copyCommissions": "0",
  "copyDisabilityOptions": "0",
  "copySelfAdmin": "0",
  "copyVolumeDetails": "0",
  "copyIDCodes": "0"
}
```

The flags are one-character strings: `"1"` copies, `"0"` does not. This is what each flag brings over, read from the database procedure behind this operation:

| Flag | With `"1"` | With `"0"` or left out |
|  --- | --- | --- |
| `copyContracts` | Group contracts (coverage periods) | No contracts |
| `copyPlans` | The plans on each contract. Needs `copyContracts`, because a contract plan belongs to a contract. | No plans |
| `copyRiders` | Plan riders and their external IDs | No riders |
| `copyNetworksTiers` | Networks, network criteria, and contract tiers | None |
| `copyPlanYears` | Plan years (accumulator periods) | None |
| `copyPremiums` | Premium rates and rate payees | None |
| `copyAdditional` | COBRA division links | The new division's distribution payor and account, invoice grace period, COBRA auto-term settings, and pay-to account are cleared |
| `copyClassOptions` | Group classes | None |
| `copyCommissions` | Commission rates and agency rep rates | None |
| `copyDisabilityOptions` | Disability deduction types | `"0"` resets the new division's disability tax and wage limits; leaving the flag out keeps the template's values |
| `copySelfAdmin` | Self-admin settings | None |
| `copyStudentAge` | Accepted, but the procedure does not use it |  |
| `copyVolumeDetails` | Volume settings | None |
| `copyIDCodes` | ID codes on the division, except the `VBAGATEWAY` code, which a nightly job assigns | None |


The sample copies the contract, plan, network, plan year, and premium setup, which is what enrollment and billing need, and leaves commissions and disability options to be set per employer.

C#
```csharp
        if (await ExistsAsync(api, $"/groups/{NewGroupId}/divisions/{NewDivisionId}"))
        {
            Console.WriteLine($"Division {NewDivisionId} already exists; skipping copy");
        }
        else
        {
            var division = (await api.PostAsync($"/groups/{TemplateGroupId}/divisions/{TemplateDivisionId}/copy", new
            {
                groupID = TemplateGroupId,
                sourceDivisionID = TemplateDivisionId,
                destinationGroupID = NewGroupId,
                destinationDivisionID = NewDivisionId,
                copyContracts = "1",
                copyPlans = "1",
                copyRiders = "1",
                copyNetworksTiers = "1",
                copyPlanYears = "1",
                copyPremiums = "1",
                copyAdditional = "0",
                copyClassOptions = "0",
                copyCommissions = "0",
                copyDisabilityOptions = "0",
                copySelfAdmin = "0",
                copyVolumeDetails = "0",
                copyIDCodes = "0",
            })).Data!;
            Console.WriteLine($"Copied division: {division["group_ID"]} {division["division_ID"]}");
        }
```

Python
```python
    if exists(api, f"/groups/{NEW_GROUP_ID}/divisions/{NEW_DIVISION_ID}"):
        print("Division", NEW_DIVISION_ID, "already exists; skipping copy")
    else:
        division = api.post(f"/groups/{TEMPLATE_GROUP_ID}/divisions/{TEMPLATE_DIVISION_ID}/copy", {
            "groupID": TEMPLATE_GROUP_ID,
            "sourceDivisionID": TEMPLATE_DIVISION_ID,
            "destinationGroupID": NEW_GROUP_ID,
            "destinationDivisionID": NEW_DIVISION_ID,
            "copyContracts": "1",
            "copyPlans": "1",
            "copyRiders": "1",
            "copyNetworksTiers": "1",
            "copyPlanYears": "1",
            "copyPremiums": "1",
            "copyAdditional": "0",
            "copyClassOptions": "0",
            "copyCommissions": "0",
            "copyDisabilityOptions": "0",
            "copySelfAdmin": "0",
            "copyVolumeDetails": "0",
            "copyIDCodes": "0",
        }).data
        print("Copied division:", division["group_ID"], division["division_ID"])
```

TypeScript
```typescript
  if (await exists(api, `/groups/${NEW_GROUP_ID}/divisions/${NEW_DIVISION_ID}`)) {
    console.log(`Division ${NEW_DIVISION_ID} already exists; skipping copy`);
  } else {
    const division = (
      await api.post(`/groups/${TEMPLATE_GROUP_ID}/divisions/${TEMPLATE_DIVISION_ID}/copy`, {
        groupID: TEMPLATE_GROUP_ID,
        sourceDivisionID: TEMPLATE_DIVISION_ID,
        destinationGroupID: NEW_GROUP_ID,
        destinationDivisionID: NEW_DIVISION_ID,
        copyContracts: "1",
        copyPlans: "1",
        copyRiders: "1",
        copyNetworksTiers: "1",
        copyPlanYears: "1",
        copyPremiums: "1",
        copyAdditional: "0",
        copyClassOptions: "0",
        copyCommissions: "0",
        copyDisabilityOptions: "0",
        copySelfAdmin: "0",
        copyVolumeDetails: "0",
        copyIDCodes: "0",
      })
    ).data;
    console.log("Copied division:", division.group_ID, division.division_ID);
  }
```

Example response (illustrative; field values are fake):

```json
{
  "data": {
    "group_ID": "DOCS-GRP1",
    "division_ID": "DOCS-DIV1",
    "name": "Template Division",
    "auth_Not_Reqd_Override": false,
    "cobrA_Division": false,
    "duplicate_Claim_Override": false,
    "prorate_Deduction": false
  },
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}
```

The response is the new division (`GroupDivision`), shortened here. It keeps the template division's name; update it with [`PUT /groups/{groupID}/divisions/{divisionID}`](/openapi/vbasoftware/vbasoftware.api/group-divisions/updategroupdivision) if you want a different one.

**Save from the response:** `group_ID` and `division_ID` (used in every later step, and in the [enrollment playbook](/developer-portal/playbooks/enroll-employees)).

**Check it worked:** `GET /groups/{groupID}/divisions/{divisionID}` with `DOCS-GRP1` and `DOCS-DIV1` returns `200`.

### 4. Check the contracts

[`GET /groups/{groupID}/divisions/{divisionID}/contracts`](/openapi/vbasoftware/vbasoftware.api/group-contracts/listgroupcontract)

List the contracts the copy brought over. Each contract is a coverage period with `coverage_Start` and `coverage_End`. Enrollment in the next playbook needs a contract that covers the enrollment dates. This is a paged list: pages start at 1, and the sample reads every page.

C#
```csharp
        var contracts = new List<JsonNode>();
        await foreach (var contract in api.GetAllAsync($"/groups/{NewGroupId}/divisions/{NewDivisionId}/contracts"))
            contracts.Add(contract!);
        contracts.Sort((a, b) => string.CompareOrdinal(a["coverage_Start"]!.GetValue<string>(), b["coverage_Start"]!.GetValue<string>()));
        foreach (var contract in contracts)
            Console.WriteLine($"Contract {DatePart(contract["coverage_Start"])} to {DatePart(contract["coverage_End"])}");
```

Python
```python
    contracts = list(api.iter_pages(f"/groups/{NEW_GROUP_ID}/divisions/{NEW_DIVISION_ID}/contracts"))
    contracts.sort(key=lambda c: c["coverage_Start"])
    for contract in contracts:
        print("Contract", contract["coverage_Start"][:10], "to", (contract.get("coverage_End") or "open")[:10])
```

TypeScript
```typescript
  const contracts: any[] = [];
  for await (const contract of api.getAll(`/groups/${NEW_GROUP_ID}/divisions/${NEW_DIVISION_ID}/contracts`)) {
    contracts.push(contract);
  }
  contracts.sort((a, b) => a.coverage_Start.localeCompare(b.coverage_Start));
  for (const contract of contracts) {
    console.log("Contract", datePart(contract.coverage_Start), "to", datePart(contract.coverage_End));
  }
```

Example response (illustrative; field values are fake):

```json
{
  "data": [
    {
      "group_ID": "DOCS-GRP1",
      "division_ID": "DOCS-DIV1",
      "coverage_Start": "2030-01-01T00:00:00",
      "coverage_End": "2030-12-31T00:00:00",
      "advice_To_Pay": false,
      "benefit_At_Member": "0",
      "comm_Pct_Flag": false,
      "max_At_Subscriber": "0",
      "rider_At_Member": "0",
      "salaried_Benefit": false,
      "total_At_Member": "0"
    }
  ],
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}
```

**Save from the response:** the latest contract's `coverage_Start` and `coverage_End` (used in step 5, and as the enrollment dates in the next playbook).

### 5. Add the next contract (optional)

[`POST /groups/{groupID}/divisions/{divisionID}/add-contract`](/openapi/vbasoftware/vbasoftware.api/adv.-group-contracts/addnextgroupcontract)

If the copied contracts end before the employer's coverage starts, add the next contract. VBAPI creates the contract you describe in `contract` and copies plans, networks, tiers, and riders forward from the current contract when you set those flags. The sample only runs this step when you set `DOCS_ADD_NEXT_CONTRACT=1`, because each run adds another contract.

`contract` holds the **new** contract: its dates, plus the required `GroupContract` fields, which the sample copies from the current contract. Send `planMappings` as an empty list when you are not renaming plans; leaving it out fails the request.

```json
{
  "contract": {
    "group_ID": "DOCS-GRP1",
    "division_ID": "DOCS-DIV1",
    "coverage_Start": "2031-01-01T00:00:00",
    "coverage_End": "2031-12-31T00:00:00",
    "advice_To_Pay": false,
    "benefit_At_Member": "0",
    "comm_Pct_Flag": false,
    "max_At_Subscriber": "0",
    "rider_At_Member": "0",
    "salaried_Benefit": false,
    "total_At_Member": "0"
  },
  "copyPlan": true,
  "copyNetwork": true,
  "copyTier": true,
  "copyRider": true,
  "reenroll": false,
  "reenrollPlan": false,
  "reenrollRider": false,
  "reenrollRiderIDCode": false,
  "copyPlanIDCode": false,
  "copyNetworkIDCode": false,
  "planMappings": []
}
```

The `reenroll` flags carry existing enrollments into the new contract. A new group has no enrollments yet, so they are `false` here. `user_ID` is optional and defaults to the API user.

C#
```csharp
            var latest = contracts[^1];
            var nextStart = DateOnly.Parse(DatePart(latest["coverage_End"])).AddDays(1);
            var nextEnd = nextStart.AddYears(1).AddDays(-1);
            var process = (await api.PostAsync($"/groups/{NewGroupId}/divisions/{NewDivisionId}/add-contract", new
            {
                contract = new
                {
                    group_ID = NewGroupId,
                    division_ID = NewDivisionId,
                    coverage_Start = $"{nextStart:yyyy-MM-dd}T00:00:00",
                    coverage_End = $"{nextEnd:yyyy-MM-dd}T00:00:00",
                    // Carry the current contract's settings forward.
                    advice_To_Pay = latest["advice_To_Pay"]!.GetValue<bool>(),
                    benefit_At_Member = latest["benefit_At_Member"]!.GetValue<string>(),
                    comm_Pct_Flag = latest["comm_Pct_Flag"]!.GetValue<bool>(),
                    max_At_Subscriber = latest["max_At_Subscriber"]!.GetValue<string>(),
                    rider_At_Member = latest["rider_At_Member"]!.GetValue<string>(),
                    salaried_Benefit = latest["salaried_Benefit"]!.GetValue<bool>(),
                    total_At_Member = latest["total_At_Member"]!.GetValue<string>(),
                },
                copyPlan = true,
                copyNetwork = true,
                copyTier = true,
                copyRider = true,
                reenroll = false,
                reenrollPlan = false,
                reenrollRider = false,
                reenrollRiderIDCode = false,
                copyPlanIDCode = false,
                copyNetworkIDCode = false,
                planMappings = Array.Empty<object>(), // send an empty list rather than leaving it out
            })).Data!;
            Console.WriteLine($"Add contract process {process["vbaProcess_Key"]} {process["vbaProcess_Type"]} result: {process["process_Result"]}");
```

Python
```python
        latest = contracts[-1]
        next_start = date.fromisoformat(latest["coverage_End"][:10]) + timedelta(days=1)
        next_end = next_start.replace(year=next_start.year + 1) - timedelta(days=1)
        process = api.post(f"/groups/{NEW_GROUP_ID}/divisions/{NEW_DIVISION_ID}/add-contract", {
            "contract": {
                "group_ID": NEW_GROUP_ID,
                "division_ID": NEW_DIVISION_ID,
                "coverage_Start": f"{next_start.isoformat()}T00:00:00",
                "coverage_End": f"{next_end.isoformat()}T00:00:00",
                # Carry the current contract's settings forward.
                "advice_To_Pay": latest["advice_To_Pay"],
                "benefit_At_Member": latest["benefit_At_Member"],
                "comm_Pct_Flag": latest["comm_Pct_Flag"],
                "max_At_Subscriber": latest["max_At_Subscriber"],
                "rider_At_Member": latest["rider_At_Member"],
                "salaried_Benefit": latest["salaried_Benefit"],
                "total_At_Member": latest["total_At_Member"],
            },
            "copyPlan": True,
            "copyNetwork": True,
            "copyTier": True,
            "copyRider": True,
            "reenroll": False,
            "reenrollPlan": False,
            "reenrollRider": False,
            "reenrollRiderIDCode": False,
            "copyPlanIDCode": False,
            "copyNetworkIDCode": False,
            "planMappings": [],  # send an empty list rather than leaving it out
        }).data
        print("Add contract process", process["vbaProcess_Key"], process["vbaProcess_Type"], "result:", process.get("process_Result"))
```

TypeScript
```typescript
    const nextStart = new Date(`${datePart(latest.coverage_End)}T00:00:00Z`);
    nextStart.setUTCDate(nextStart.getUTCDate() + 1);
    const nextEnd = new Date(nextStart);
    nextEnd.setUTCFullYear(nextEnd.getUTCFullYear() + 1);
    nextEnd.setUTCDate(nextEnd.getUTCDate() - 1);
    const vbaProcess = (
      await api.post(`/groups/${NEW_GROUP_ID}/divisions/${NEW_DIVISION_ID}/add-contract`, {
        contract: {
          group_ID: NEW_GROUP_ID,
          division_ID: NEW_DIVISION_ID,
          coverage_Start: `${nextStart.toISOString().slice(0, 10)}T00:00:00`,
          coverage_End: `${nextEnd.toISOString().slice(0, 10)}T00:00:00`,
          // Carry the current contract's settings forward.
          advice_To_Pay: latest.advice_To_Pay,
          benefit_At_Member: latest.benefit_At_Member,
          comm_Pct_Flag: latest.comm_Pct_Flag,
          max_At_Subscriber: latest.max_At_Subscriber,
          rider_At_Member: latest.rider_At_Member,
          salaried_Benefit: latest.salaried_Benefit,
          total_At_Member: latest.total_At_Member,
        },
        copyPlan: true,
        copyNetwork: true,
        copyTier: true,
        copyRider: true,
        reenroll: false,
        reenrollPlan: false,
        reenrollRider: false,
        reenrollRiderIDCode: false,
        copyPlanIDCode: false,
        copyNetworkIDCode: false,
        planMappings: [], // send an empty list rather than leaving it out
      })
    ).data;
    console.log("Add contract process", vbaProcess.vbaProcess_Key, vbaProcess.vbaProcess_Type, "result:", vbaProcess.process_Result);
```

The call runs the work before it responds, and returns a `VBAProcess` record describing the run.

Example response (illustrative; field values are fake):

```json
{
  "data": {
    "vbaProcess_Key": 1001,
    "vbaProcess_Type": "GROUPADDCONTRACT",
    "description": "Group Contract Add.",
    "process_Start": "2030-01-15T09:00:00",
    "process_End": "2030-01-15T09:00:02",
    "process_Result": null
  },
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}
```

**Save from the response:** `vbaProcess_Key` and `vbaProcess_Type`. If the new contract is missing afterwards, read the run's log with [`GET /vbaprocesses/{vBAProcessKey}/types/{vBAProcessType}/logs`](/openapi/vbasoftware/vbasoftware.api/process-logging/listvbaprocesslog).

**Check it worked:** step 4's list now includes the new contract.

### 6. Confirm the billing cycle

[`GET /billing-cycles/{billingCycleID}`](/openapi/vbasoftware/vbasoftware.api/billing-cycles/getbillingcycle)

Premium invoices use the group's billing cycle, so confirm the group has one and that it is the cycle you expect. If it is empty, update the group before you [bill premiums](/developer-portal/playbooks/premium-billing-and-payments). [`GET /billing-cycles`](/openapi/vbasoftware/vbasoftware.api/billing-cycles/listbillingcycle) lists every cycle.

C#
```csharp
        var group = (await api.GetAsync($"/groups/{NewGroupId}")).Data!;
        var groupCycleId = group["billing_Cycle_ID"]?.GetValue<string>();
        if (!string.IsNullOrEmpty(groupCycleId))
        {
            var cycle = (await api.GetAsync($"/billing-cycles/{groupCycleId}")).Data!;
            Console.WriteLine($"Billing cycle {cycle["billing_Cycle_ID"]} {cycle["description"]} months: {cycle["months"]}");
        }
        else
        {
            Console.WriteLine("The group has no billing cycle; set billing_Cycle_ID before you generate invoices");
        }
```

Python
```python
    group = api.get(f"/groups/{NEW_GROUP_ID}").data
    if group.get("billing_Cycle_ID"):
        cycle = api.get(f"/billing-cycles/{group['billing_Cycle_ID']}").data
        print("Billing cycle", cycle["billing_Cycle_ID"], cycle.get("description"), "months:", cycle.get("months"))
    else:
        print("The group has no billing cycle; set billing_Cycle_ID before you generate invoices")
```

TypeScript
```typescript
  const group = (await api.get(`/groups/${NEW_GROUP_ID}`)).data;
  if (group.billing_Cycle_ID) {
    const cycle = (await api.get(`/billing-cycles/${group.billing_Cycle_ID}`)).data;
    console.log("Billing cycle", cycle.billing_Cycle_ID, cycle.description, "months:", cycle.months);
  } else {
    console.log("The group has no billing cycle; set billing_Cycle_ID before you generate invoices");
  }
```

## Verify the result

[`GET /groups/{groupID}/divisions/{divisionID}`](/openapi/vbasoftware/vbasoftware.api/group-divisions/getgroupdivision) and [`GET /groups/{groupID}/divisions/{divisionID}/contracts/{coverageStart}/plans`](/openapi/vbasoftware/vbasoftware.api/group-contract-plans/listgroupcontractplan)

Read back the new division, then the plans on each contract. These calls only read data. You should see the division, every contract the template had, and the template's plans on each contract. Pass `coverageStart` as a date, for example `2030-01-01`.

C#
```csharp
        var newDivision = (await api.GetAsync($"/groups/{NewGroupId}/divisions/{NewDivisionId}")).Data!;
        Console.WriteLine($"Division: {newDivision["group_ID"]} {newDivision["division_ID"]} {newDivision["name"]}");
        await foreach (var contract in api.GetAllAsync($"/groups/{NewGroupId}/divisions/{NewDivisionId}/contracts"))
        {
            var coverageStart = DatePart(contract!["coverage_Start"]);
            var plans = (await api.GetAsync($"/groups/{NewGroupId}/divisions/{NewDivisionId}/contracts/{coverageStart}/plans")).Data?.AsArray() ?? new JsonArray();
            var planIds = string.Join(", ", plans.Select(p => p!["plan_ID"]!.GetValue<string>()));
            Console.WriteLine($"Contract {coverageStart} plans: {(planIds.Length > 0 ? planIds : "none")}");
        }
```

Python
```python
    division = api.get(f"/groups/{NEW_GROUP_ID}/divisions/{NEW_DIVISION_ID}").data
    print("Division:", division["group_ID"], division["division_ID"], division.get("name"))
    for contract in api.iter_pages(f"/groups/{NEW_GROUP_ID}/divisions/{NEW_DIVISION_ID}/contracts"):
        coverage_start = contract["coverage_Start"][:10]
        plans = api.get(f"/groups/{NEW_GROUP_ID}/divisions/{NEW_DIVISION_ID}/contracts/{coverage_start}/plans").data or []
        print("Contract", coverage_start, "plans:", ", ".join(p["plan_ID"] for p in plans) or "none")
```

TypeScript
```typescript
  const division = (await api.get(`/groups/${NEW_GROUP_ID}/divisions/${NEW_DIVISION_ID}`)).data;
  console.log("Division:", division.group_ID, division.division_ID, division.name);
  for await (const contract of api.getAll(`/groups/${NEW_GROUP_ID}/divisions/${NEW_DIVISION_ID}/contracts`)) {
    const coverageStart = datePart(contract.coverage_Start);
    const plans: any[] = (await api.get(`/groups/${NEW_GROUP_ID}/divisions/${NEW_DIVISION_ID}/contracts/${coverageStart}/plans`)).data ?? [];
    console.log("Contract", coverageStart, "plans:", plans.map((p) => p.plan_ID).join(", ") || "none");
  }
```

## Safe to re-run?

- **Group:** creating a group whose `group_ID` exists fails, so the sample checks with `GET /groups/{groupID}` first.
- **Division:** the copy inserts a new division and fails if `destinationDivisionID` already exists in the destination group. The sample checks with `GET /groups/{groupID}/divisions/{divisionID}` first and skips the copy.
- **Partial copies:** the copy writes the division first and then each kind of related record, without one transaction. A copy that fails partway (for example on `PK_GroupPlanYear` because plan years from an earlier division with the same IDs were left behind) leaves the division and its contracts in place. Read them back before you retry, and remove or finish what was created.
- **Next contract:** each `add-contract` call adds one more contract, so run it once per coverage period.


## Undo

To remove a test group, delete from the bottom up. Deleting a division or a group does not delete the records under it, and a later copy into the same IDs then fails with a `PK_GroupPlanYear` error:

1. Each contract plan: [`DELETE /groups/{groupID}/divisions/{divisionID}/contracts/{coverageStart}/plans/{planID}`](/openapi/vbasoftware/vbasoftware.api/group-contract-plans/deletegroupcontractplan).
2. Each contract: [`DELETE /groups/{groupID}/divisions/{divisionID}/contracts/{coverageStart}`](/openapi/vbasoftware/vbasoftware.api/group-contracts/deletegroupcontract).
3. Each plan year: [`DELETE /groups/{groupID}/divisions/{divisionID}/plan-years/{planYear}`](/openapi/vbasoftware/vbasoftware.api/group-plan-years/deletegroupplanyear).
4. The division with [`DELETE /groups/{groupID}/divisions/{divisionID}`](/openapi/vbasoftware/vbasoftware.api/group-divisions/deletegroupdivision), then the group with [`DELETE /groups/{groupID}`](/openapi/vbasoftware/vbasoftware.api/groups/deletegroups).


Every delete returns `204`, also when the record is already gone. Delete only groups you created for testing: a group with enrollments, claims, or invoices is real business data. If you used VBA's internal test database, ask the VBA docs team to reset it instead.

## Common errors

| Symptom | Likely cause | Fix |
|  --- | --- | --- |
| `404` on step 1 | The template group or division ID is wrong, or it is in another database | Check the IDs and the `vbasoftware-database` header |
| `400` on the copy, naming `groupID` or `sourceDivisionID` | The body IDs do not match the path | Use the template IDs in both the path and the body |
| Error on the copy right after creating the group | The destination division already exists, or the destination group is missing | Read both with the GET calls in steps 2 and 3 |
| `500` on `add-contract` | `planMappings` was left out | Send `"planMappings": []` |
| New contract missing after `add-contract` | The procedure logged an error | Read the run's log with `GET /vbaprocesses/{vBAProcessKey}/types/{vBAProcessType}/logs` |
| `403` with `{"message":"Forbidden"}` on every call | The `x-api-key` header is missing or wrong | See [Errors and troubleshooting](/developer-portal/concepts/errors) |


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

- **Build a division from scratch:** create it with [`POST /groups/{groupID}/divisions`](/openapi/vbasoftware/vbasoftware.api/group-divisions/creategroupdivision), then add contracts and plans. This needs many more calls and every required field of each schema; copying a template is the faster path.
- **Several divisions:** run step 3 once per division, each with its own `destinationDivisionID`.


## Set up your environment

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.

Also set `DOCS_TEMPLATE_GROUP_ID` and `DOCS_TEMPLATE_DIVISION_ID` to your template.

## Download

The full runnable samples are in the docs repository under `_samples/playbooks/onboard-employer-group/`: `OnboardEmployerGroupPlaybook.cs`, `onboard_employer_group.py`, and `onboardEmployerGroup.ts`. See `_samples/playbooks/README.md` for how to run them.

## Related

- Concepts: [Data model](/developer-portal/concepts/data-model), [IDs and conventions](/developer-portal/concepts/ids-and-conventions), [Pagination and sorting](/developer-portal/concepts/pagination-and-sorting)
- Next playbook: [Enroll employees and dependents](/developer-portal/playbooks/enroll-employees)
- All playbooks: [Playbooks](/developer-portal/playbooks/)
- Ask the AI: "Using the Onboard a new employer group playbook, write a C# function that copies a template division into a new group and lists the copied contracts."