Skip to content

Enroll employees and dependents, and request ID cards

This playbook is for HR and benefits integrators and enrollment teams who load new hires or a census into VBASoftware. At the end, each family is a subscriber with members, enrolled in a group contract and plan, with an ID card request on file.

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 and can get an ID token. The samples read your credentials from environment variables; see Set up your environment.
  • The employer's group and division exist, with at least one contract and a plan on it. The samples use DOCS-GRP1 and DOCS-DIV1 from Onboard a new employer group.
  • The API user can create subscribers, members, and enrollments, and create ID card requests.
  • You have read the subscribers, members, and enrollments part of the data model.

How it fits together

A subscriber is the family unit, identified by subscriber_ID. Members are the people in it, identified by subscriber_ID plus a two-character member_Seq. One member is the insured (the employee). The family's enrollment ties the subscriber to a group contract for a date range, and enrollment plans say which plan they are covered by.

VBAPIYour systemVBAPIYour systemGET contracts and plans, GET /relationshipscoverage dates, plan_ID, relationship codesPOST /subscribers200 subscriberPOST /subscribers/{id}/members (insured, then dependents)200 member with member_SeqPOST /subscribers/{id}/add-enrollment202 Accepted, no bodyGET /subscribers/{id}/enrollmentsthe new enrollmentPOST .../enrollments/.../plans200 enrollment planPOST /id-card-requests200 with idCardRequest_Key
VBAPIYour systemVBAPIYour systemGET contracts and plans, GET /relationshipscoverage dates, plan_ID, relationship codesPOST /subscribers200 subscriberPOST /subscribers/{id}/members (insured, then dependents)200 member with member_SeqPOST /subscribers/{id}/add-enrollment202 Accepted, no bodyGET /subscribers/{id}/enrollmentsthe new enrollmentPOST .../enrollments/.../plans200 enrollment planPOST /id-card-requests200 with idCardRequest_Key

The samples use the fake subscriber DOCS-0001. subscriber_ID allows only 9 characters, so check the length of your own IDs.

Steps

1. Pick the contract and plan

GET /groups/{groupID}/divisions/{divisionID}/contracts and GET /groups/{groupID}/divisions/{divisionID}/contracts/{coverageStart}/plans

Enrollment points at a specific group contract, identified by its coverage_Start. Read the division's contracts, choose the one that covers the employee's start date (the sample takes the latest), and read the plans offered on it. Pass coverageStart in the path as a date, for example 2030-01-01.

        var contracts = new List<JsonNode>();
        await foreach (var c in api.GetAllAsync($"/groups/{GroupId}/divisions/{DivisionId}/contracts"))
            contracts.Add(c!);
        var contract = contracts.OrderBy(c => c["coverage_Start"]!.GetValue<string>(), StringComparer.Ordinal).Last(); // the latest contract
        var coverageStart = DatePart(contract["coverage_Start"]);
        var coverageEnd = DatePart(contract["coverage_End"]);
        if (coverageEnd.Length == 0)
            throw new InvalidOperationException("The contract has no coverage_End. add-enrollment needs an end_Date; set one for this sample.");
        var groupPlans = (await api.GetAsync($"/groups/{GroupId}/divisions/{DivisionId}/contracts/{coverageStart}/plans")).Data!.AsArray();
        var planId = groupPlans[0]!["plan_ID"]!.GetValue<string>();
        Console.WriteLine($"Contract {coverageStart} to {coverageEnd} plan {planId}");
        

Save from the response: the contract's coverage_Start and coverage_End (steps 5 and 7), and a plan_ID (step 7).

2. Look up the relationship codes

GET /relationships

Relationship codes are configured per database, so look them up rather than hardcoding them. The relationship whose subscriber flag is "1" marks the insured; VBAPI uses it to decide which member is the employee. The one whose spouse flag is "1" is the spouse. Other dependents (children, for example) use the code whose description matches.

        var relationships = (await api.GetAsync("/relationships")).Data!.AsArray();
        var insuredCode = relationships.First(r => r!["subscriber"]?.GetValue<string>() == "1")!["relate_Code"]!.GetValue<string>();
        var spouseCode = relationships.First(r => r!["spouse"]?.GetValue<string>() == "1")!["relate_Code"]!.GetValue<string>();
        Console.WriteLine($"Insured relationship: {insuredCode} spouse relationship: {spouseCode}");
        

Save from the response: the insured and spouse relate_Code values (step 4).

3. Create the subscriber

POST /subscribers

The Subscribers schema has no required fields. Send the family's ID, name, and address:

{
  "subscriber_ID": "DOCS-0001",
  "first_Name": "Sample",
  "last_Name": "Member",
  "address": "123 Example St",
  "city": "Exampleville",
  "state": "TX",
  "zip_Code": "00000"
}

If you leave out subscriber_ID, VBAPI generates a 9-character ID and returns it; GET /subscribers-generate-id also returns one. Creating a subscriber does not create any member, not even the employee, so step 4 is always needed.

        if (await ExistsAsync(api, $"/subscribers/{SubscriberId}"))
        {
            Console.WriteLine($"Subscriber {SubscriberId} already exists; skipping create");
        }
        else
        {
            var subscriber = (await api.PostAsync("/subscribers", new
            {
                subscriber_ID = SubscriberId,
                first_Name = "Sample",
                last_Name = "Member",
                address = "123 Example St",
                city = "Exampleville",
                state = "TX",
                zip_Code = "00000",
            })).Data!;
            Console.WriteLine($"Created subscriber {subscriber["subscriber_ID"]}");
        }
        

Save from the response: subscriber_ID (every later step).

Check it worked: GET /subscribers/{subscriberID} returns 200.

4. Add the employee and dependents

POST /subscribers/{subscriberID}/members

Create the employee first, with the insured relationship code, then each dependent. subscriber_ID in the body must match the path. Members has six required booleans; false is the usual value for a new enrollee:

{
  "subscriber_ID": "DOCS-0001",
  "first_Name": "Sample",
  "last_Name": "Member",
  "relationship": "INSURED_CODE_FROM_STEP_2",
  "adult_Dependent": false,
  "continue_Coverage": false,
  "initial_Volume_Salary_Pct": false,
  "other_Insurance": false,
  "smoker": false,
  "use_Member_Plan_Year": false
}

Leave out member_Seq and VBAPI assigns the next number in the family: 01 for the first member, then 02, and so on, up to 99. Creating the employee first gives them 01. Add birth_Date, sex, and the other fields your plans' eligibility rules use; the Members schema lists them.

        var members = (await api.GetAsync($"/subscribers/{SubscriberId}/members")).Data?.AsArray()
            .Select(m => m!).ToList() ?? new List<JsonNode>();
        var existing = members.Select(m => m["relationship"]?.GetValue<string>()).ToHashSet();
        var people = new[]
        {
            (FirstName: "Sample", Relationship: insuredCode), // the insured: create first
            (FirstName: "Sample Spouse", Relationship: spouseCode),
        };
        foreach (var person in people)
        {
            if (existing.Contains(person.Relationship))
                continue;
            var member = (await api.PostAsync($"/subscribers/{SubscriberId}/members", new
            {
                subscriber_ID = SubscriberId,
                first_Name = person.FirstName,
                last_Name = "Member",
                relationship = person.Relationship,
                // Required booleans.
                adult_Dependent = false,
                continue_Coverage = false,
                initial_Volume_Salary_Pct = false,
                other_Insurance = false,
                smoker = false,
                use_Member_Plan_Year = false,
            })).Data!;
            members.Add(member);
            Console.WriteLine($"Created member {member["member_Seq"]} {member["first_Name"]}");
        }
        var insuredSeq = members.First(m => m["relationship"]?.GetValue<string>() == insuredCode)["member_Seq"]!.GetValue<string>();
        

Example response (illustrative; field values are fake):

{
  "data": {
    "subscriber_ID": "DOCS-0001",
    "member_Seq": "01",
    "first_Name": "Sample",
    "last_Name": "Member",
    "relationship": "INSURED_CODE_FROM_STEP_2",
    "adult_Dependent": false,
    "continue_Coverage": false,
    "initial_Volume_Salary_Pct": false,
    "other_Insurance": false,
    "smoker": false,
    "use_Member_Plan_Year": false
  },
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}

Save from the response: each member's member_Seq (the employee's is used in step 9).

Check it worked: GET /subscribers/{subscriberID}/members lists every member.

5. Enroll the family

POST /subscribers/{subscriberID}/add-enrollment

This creates the family's enrollment in the group contract. subEnrollment names the contract by group_ID, division_ID, and group_Coverage_Start (the contract's coverage_Start), and the family's coverage dates by start_Date and end_Date.

{
  "requesting_User_ID": "your-api-username",
  "addMembers": true,
  "subEnrollment": {
    "subscriber_ID": "DOCS-0001",
    "group_ID": "DOCS-GRP1",
    "division_ID": "DOCS-DIV1",
    "group_Coverage_Start": "2030-01-01T00:00:00",
    "start_Date": "2030-01-01T00:00:00",
    "end_Date": "2030-12-31T00:00:00"
  }
}
  • requesting_User_ID is required (up to 20 characters). The samples send the API user name.
  • end_Date is not marked required in the schema, but VBAPI rejects the request without it. Use the contract's coverage_End unless the employee's coverage ends sooner.
  • addMembers: true includes the dependents. With false, only the insured member is made active on the enrollment.
  • VBAPI checks that the subscriber exists, that at least one member exists, and that the group contract exists, and returns 400 with a message if not.

The response is 202 Accepted with no body.

        if (await FindEnrollmentAsync(api, coverageStart) is not null)
        {
            Console.WriteLine($"Already enrolled in {GroupId} {DivisionId} {coverageStart}");
        }
        else
        {
            var response = await api.PostAsync($"/subscribers/{SubscriberId}/add-enrollment", new
            {
                requesting_User_ID = requestingUserId,
                addMembers = true,
                subEnrollment = new
                {
                    subscriber_ID = SubscriberId,
                    group_ID = GroupId,
                    division_ID = DivisionId,
                    group_Coverage_Start = $"{coverageStart}T00:00:00",
                    start_Date = $"{coverageStart}T00:00:00",
                    end_Date = $"{coverageEnd}T00:00:00",
                },
            });
            Console.WriteLine($"add-enrollment returned {(int)response.Status}"); // 202 Accepted, no body
        }
        

6. Confirm the enrollment

GET /subscribers/{subscriberID}/enrollments

A 202 has no body, so read the enrollment back. The sample checks a few times, two seconds apart, before it gives up. Match on group_ID, division_ID, and group_Coverage_Start.

        JsonNode? enrollment = null;
        for (var attempt = 0; attempt < 5 && enrollment is null; attempt++)
        {
            enrollment = await FindEnrollmentAsync(api, coverageStart);
            if (enrollment is null)
                await Task.Delay(TimeSpan.FromSeconds(2));
        }
        if (enrollment is null)
            throw new InvalidOperationException("The enrollment did not appear. Check the subscriber in VBASoftware.");
        var startDate = DatePart(enrollment["start_Date"]);
        Console.WriteLine($"Enrollment {enrollment["group_ID"]} {enrollment["division_ID"]} {startDate} to {DatePart(enrollment["end_Date"])}");
        

Example response (illustrative; field values are fake):

{
  "data": [
    {
      "subscriber_ID": "DOCS-0001",
      "group_ID": "DOCS-GRP1",
      "division_ID": "DOCS-DIV1",
      "group_Coverage_Start": "2030-01-01T00:00:00",
      "start_Date": "2030-01-01T00:00:00",
      "end_Date": "2030-12-31T00:00:00"
    }
  ],
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}

Save from the response: start_Date (with group_Coverage_Start, it identifies the enrollment in step 7).

7. Add the plan to the enrollment

POST /subscribers/{subscriberID}/enrollments/{groupID}/{divisionID}/{groupCoverageStart}/{startDate}/plans

add-enrollment does not choose a plan. Add one for each plan the family elected. The path identifies the enrollment from step 6; pass both dates as dates, for example 2030-01-01. plan_ID must be a plan on the group contract (step 1).

{
  "subscriber_ID": "DOCS-0001",
  "group_ID": "DOCS-GRP1",
  "division_ID": "DOCS-DIV1",
  "group_Coverage_Start": "2030-01-01T00:00:00",
  "start_Date": "2030-01-01T00:00:00",
  "plan_ID": "PLAN_ID_FROM_STEP_1",
  "plan_Start": "2030-01-01T00:00:00",
  "plan_End": "2030-12-31T00:00:00",
  "use_Volume_Scale": false
}

use_Volume_Scale is the one required boolean; it matters for life and disability volume plans. tier sets the family's coverage tier when the plan is tiered.

        var enrolledPlans = (await api.GetAsync(plansPath)).Data?.AsArray() ?? new JsonArray();
        if (enrolledPlans.Any(p => p!["plan_ID"]!.GetValue<string>() == planId))
        {
            Console.WriteLine($"Plan {planId} already on the enrollment");
        }
        else
        {
            await api.PostAsync(plansPath, new
            {
                subscriber_ID = SubscriberId,
                group_ID = GroupId,
                division_ID = DivisionId,
                group_Coverage_Start = $"{coverageStart}T00:00:00",
                start_Date = $"{startDate}T00:00:00",
                plan_ID = planId,
                plan_Start = $"{startDate}T00:00:00",
                plan_End = $"{coverageEnd}T00:00:00",
                use_Volume_Scale = false,
            });
            Console.WriteLine($"Added plan {planId}");
        }
        

8. Confirm the plans

GET /subscribers/{subscriberID}/enrollments/{groupID}/{divisionID}/{groupCoverageStart}/{startDate}/plans

This only reads data. You should see each plan with its plan_Start and plan_End.

        foreach (var plan in (await api.GetAsync(plansPath)).Data?.AsArray() ?? new JsonArray())
            Console.WriteLine($"Enrolled plan {plan!["plan_ID"]} {DatePart(plan["plan_Start"])} to {DatePart(plan["plan_End"])}");
        

9. Request ID cards

GET /id-card-request-types and POST /id-card-requests

An ID card request records that a card should be produced for a member. Request types are configured per database, so read them first and use the right request_Type.

{
  "idCardRequest_Key": 0,
  "renewal": false,
  "subscriber_ID": "DOCS-0001",
  "member_Seq": "01",
  "group_ID": "DOCS-GRP1",
  "division_ID": "DOCS-DIV1",
  "request_Type": "REQUEST_TYPE_FROM_LOOKUP",
  "request_Status": "REQ",
  "request_Date": "2030-01-02T00:00:00",
  "as_Of_Date": "2030-01-01T00:00:00",
  "request_Quantity": 1,
  "physical_Card_Requested": true
}
  • idCardRequest_Key and renewal are required. Send 0 for the key; the database assigns it.
  • request_Status takes a value from GET /id-card-request-statuses: REQ (Requested), COMPLETE, or CANCEL.
  • as_Of_Date is the date the card is effective from.
  • issuance_Type is left out. Which values your card vendor expects is set up per client; ask VBA before you send it.
        var requestTypes = (await api.GetAsync("/id-card-request-types")).Data!.AsArray();
        var card = (await api.PostAsync("/id-card-requests", new
        {
            idCardRequest_Key = 0, // 0 on create; VBAPI assigns the key
            renewal = false,
            subscriber_ID = SubscriberId,
            member_Seq = insuredSeq,
            group_ID = GroupId,
            division_ID = DivisionId,
            request_Type = requestTypes[0]!["request_Type"]!.GetValue<string>(),
            request_Status = "REQ",
            request_Date = $"{DateTime.Today:yyyy-MM-dd}T00:00:00",
            as_Of_Date = $"{startDate}T00:00:00",
            request_Quantity = 1,
            physical_Card_Requested = true,
        })).Data!;
        var cardKey = card["idCardRequest_Key"]!.GetValue<int>();
        Console.WriteLine($"ID card request {cardKey}");
        

Save from the response: idCardRequest_Key (step 10).

10. Check the ID card request

GET /id-card-requests/{iDCardRequestKey}

Read the request back to confirm it was saved. request_Status changes as the request is handled, for example to COMPLETE.

        var savedCard = (await api.GetAsync($"/id-card-requests/{cardKey}")).Data!;
        Console.WriteLine($"ID card request {savedCard["idCardRequest_Key"]} status: {savedCard["request_Status"]}");
        

Example response (illustrative; field values are fake):

{
  "data": {
    "idCardRequest_Key": 5001,
    "subscriber_ID": "DOCS-0001",
    "member_Seq": "01",
    "group_ID": "DOCS-GRP1",
    "division_ID": "DOCS-DIV1",
    "request_Type": "NEWENROLL",
    "request_Status": "REQ",
    "request_Date": "2030-01-02T00:00:00",
    "as_Of_Date": "2030-01-01T00:00:00",
    "request_Quantity": 1,
    "renewal": false,
    "physical_Card_Requested": true
  },
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}

Verify the result

Run steps 6, 8, and 10 again; they only read data. The family should have one enrollment for the contract dates, the elected plans on it, and an ID card request with status REQ. GET /subscribers/{subscriberID}/list-all-enrollments gives a single view of every enrollment the subscriber has.

Census variation: many families at once

For a census file, load subscribers and members with the batch operations, then run steps 5 to 10 for each family.

Both create rows that do not exist and update rows that do, and return 207 Multi-Status. The data array has one item per row, with its own status (201 created, 200 updated, or an error status), a body message, and an id list of key and value pairs that identifies the row. One bad row does not stop the others, so always read every item.

Send member_Seq in the batch. The batch decides between create and update by subscriber_ID plus member_Seq, so without it every run adds new members. An update replaces the stored row with what you send, so include every field you want to keep.

The sample reads census-sample.csv, whose relationship column holds insured, spouse, or a code from GET /relationships:

subscriber_id,member_seq,relationship,first_name,last_name,address,city,state,zip_code
DOCS-0101,01,insured,Sample,Member,123 Example St,Exampleville,TX,00000
DOCS-0101,02,spouse,Sample Spouse,Member,123 Example St,Exampleville,TX,00000
DOCS-0102,01,insured,Test,Person,123 Example St,Exampleville,TX,00000

Load the subscribers and collect the failures:

        var families = ReadCsv(CensusFile()).GroupBy(row => row["subscriber_id"]).ToList();
        var subscribers = families.Select(family =>
        {
            var insured = family.First(r => r["relationship"] == "insured");
            return new
            {
                subscriber_ID = family.Key,
                first_Name = insured["first_name"],
                last_Name = insured["last_name"],
                address = insured["address"],
                city = insured["city"],
                state = insured["state"],
                zip_Code = insured["zip_code"],
            };
        }).ToList();
        var response = await api.PutAsync("/subscribers-batch", subscribers); // 207 Multi-Status
        var failedIds = new HashSet<string?>();
        foreach (var item in response.Data?.AsArray() ?? new JsonArray())
        {
            if (item!["status"]!.GetValue<int>() < 400)
                continue;
            var key = Key(item);
            failedIds.Add(key.GetValueOrDefault("Subscriber_ID"));
            Console.WriteLine($"Subscriber failed: {key.GetValueOrDefault("Subscriber_ID")} {item["status"]} {item["body"]}");
        }
        

Load each family's members, skipping families whose subscriber failed:

        foreach (var family in families)
        {
            if (failedIds.Contains(family.Key))
                continue; // fix the subscriber row first, then re-run; the batch updates rows that exist
            var members = family.Select(row => new
            {
                subscriber_ID = family.Key,
                member_Seq = row["member_seq"], // send it so a re-run updates instead of adding a member
                first_Name = row["first_name"],
                last_Name = row["last_name"],
                relationship = codes.GetValueOrDefault(row["relationship"], row["relationship"]),
                adult_Dependent = false,
                continue_Coverage = false,
                initial_Volume_Salary_Pct = false,
                other_Insurance = false,
                smoker = false,
                use_Member_Plan_Year = false,
            }).ToList();
            var memberResponse = await api.PutAsync($"/subscribers/{family.Key}/members-batch", members);
            foreach (var item in memberResponse.Data?.AsArray() ?? new JsonArray())
                Console.WriteLine($"{family.Key} {Key(item!).GetValueOrDefault("Member_Seq")} {item!["status"]} {item["body"]}"); // 201 Created, 200 Updated
        }
        

Example 207 response (illustrative; field values are fake):

{
  "data": [
    { "id": [{ "key": "Subscriber_ID", "value": "DOCS-0101" }], "status": 201, "body": "Created" },
    { "id": [{ "key": "Subscriber_ID", "value": "DOCS-0102" }], "status": 400, "body": "A validation message for this row" }
  ],
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}

Keep batches to a size your system can retry, and stay within the rate limits when you loop over families.

Safe to re-run?

  • The main sample checks before each create: it skips the subscriber if GET /subscribers/{subscriberID} finds it, skips members whose relationship is already in the family, skips add-enrollment if the enrollment exists, and skips plans already on it.
  • add-enrollment creates the enrollment row keyed by subscriber, group, division, contract start, and start date, so repeating it with the same dates fails rather than creating a duplicate.
  • Each ID card request is a new record. Running step 9 twice requests two cards.
  • The census batches are upserts: re-running them updates the same rows, as long as member_Seq is in the file.

Undo

To remove a test family:

  1. Delete the ID card request with DELETE /id-card-requests/{iDCardRequestKey}.
  2. Delete the subscriber with DELETE /subscribers/{subscriberID}. The database removes the subscriber's enrollments and members with it, and refuses the delete if the subscriber has claims, authorizations, or accumulators.

Deletes return 204. To remove only one enrollment or one dependent, use DELETE /subscribers/{subscriberID}/enrollments/{groupID}/{divisionID}/{groupCoverageStart}/{startDate} or DELETE /subscribers/{subscriberID}/members/{memberSeq}. Deleting the insured member this way returns 400; delete the subscriber instead. For a real member who is leaving, disenroll instead of deleting; see Disenroll a member.

Common errors

SymptomLikely causeFix
400 "No member exists for the Subscriber ID" on step 5Members were not createdRun step 4 first
400 "The Group Contract does not exsit" on step 5group_Coverage_Start does not match a contract's coverage_StartUse the exact date from step 1
400 "The End Date must be populated" on step 5end_Date was left outSend end_Date, usually the contract's coverage_End
400 on a member, naming subscriber_IDThe body subscriber_ID does not match the pathSend the same ID in both
422 "Maximum of 99 members exceeded"The family already has member_Seq 99Check for duplicate members
Batch item with status 400 or 500That row failed validationRead body, fix the row, and re-run; the other rows are already saved
403 with {"message":"Forbidden"} on every callThe x-api-key header is missing or wrongSee Errors and troubleshooting
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.

Variations

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:

$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"

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

Download

The full runnable samples are in the docs repository under _samples/playbooks/enroll-employees/: EnrollEmployeesPlaybook.cs, enroll_employees.py, and enrollEmployees.ts for one family, and EnrollEmployeesCensusPlaybook.cs, enroll_census.py, enrollCensus.ts, and census-sample.csv for the census variation. See _samples/playbooks/README.md for how to run them.