Skip to content

Member self-service

This playbook is for developers building the backend of a member portal or mobile app on VBAPI. It shows the calls that answer a member's everyday questions: what plan am I on, how much of my deductible have I met, what happened to my claims, and how do I get a new ID card.

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.

Your app is responsible for member security

VBAPI authenticates your integration, not your members. The API user is a service account that can read every member in the database, and VBAPI returns whatever subscriberID and memberSeq you ask for. Your app must:

  • sign in its own members with your own identity provider
  • map each signed-in member to their subscriber_ID and member_Seq on your server, and never take those values from the browser or app
  • call VBAPI only from your server, so the API key, password, and ID token never reach a member's device
  • show a member only their own data, or their family's if your policy allows it

Data masking settings in VBASoftware can hide some fields (for example SSN) in responses; do not rely on masking for access control.

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 member is enrolled in a plan. The samples use DOCS-0001, member 01, from Enroll employees and dependents.
  • The API user can read members, enrollments, accumulators, and claims, and create ID card requests.

How it fits together

VBAPIYour serverMember's appVBAPIYour serverMember's appSigns in (your identity provider)GET member, enrollments, plansgroup_ID, plan_IDGET list-plan-year, list-accum-detailplan year, deductible and out-of-pocket totalsGET list-member-claim, GET /claims/{claimNumber}/eobclaims, EOB PDFRequests a new ID cardPOST /id-card-requestsidCardRequest_Key
VBAPIYour serverMember's appVBAPIYour serverMember's appSigns in (your identity provider)GET member, enrollments, plansgroup_ID, plan_IDGET list-plan-year, list-accum-detailplan year, deductible and out-of-pocket totalsGET list-member-claim, GET /claims/{claimNumber}/eobclaims, EOB PDFRequests a new ID cardPOST /id-card-requestsidCardRequest_Key

Steps

1. Read the member

GET /subscribers/{subscriberID}/members/{memberSeq}

Load the signed-in member's name and details for the app's profile screen.

        var member = (await api.GetAsync(memberPath)).Data!;
        Console.WriteLine($"Member: {member["first_Name"]} {member["last_Name"]} relationship: {member["relationship"]}");
        

2. Find the member's current plan

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

Enrollments belong to the family (the subscriber). Pick the enrollment whose start_Date to end_Date covers today, then read its plans. The sample falls back to the latest enrollment, which helps with test data dated in the future.

        var enrollments = new List<JsonNode>();
        await foreach (var e in api.GetAllAsync($"/subscribers/{SubscriberId}/enrollments"))
            enrollments.Add(e!);
        enrollments = enrollments.OrderBy(e => DatePart(e["start_Date"]), StringComparer.Ordinal).ToList();
        var enrollment = enrollments.FirstOrDefault(e => Covers(e, "start_Date", "end_Date", today)) ?? enrollments[^1];
        var groupId = enrollment["group_ID"]!.GetValue<string>();
        var divisionId = enrollment["division_ID"]!.GetValue<string>();
        var plans = (await api.GetAsync(
            $"/subscribers/{SubscriberId}/enrollments/{groupId}/{divisionId}/{DatePart(enrollment["group_Coverage_Start"])}/{DatePart(enrollment["start_Date"])}/plans")).Data!.AsArray();
        var planId = plans[0]!["plan_ID"]!.GetValue<string>();
        Console.WriteLine($"Group {groupId} plan {planId}");
        

Save from the response: group_ID, division_ID, and plan_ID (steps 4 and 7).

3. Find the plan year

GET /subscribers/{subscriberID}/members/{memberSeq}/list-plan-year

Accumulators (deductible and out-of-pocket totals) reset each plan year. This lists the plan years that apply to the member, from the group's plan years and the member's own plan year if they have one. Pick the one whose from_Date to thru_Date covers today.

        var planYears = (await api.GetAsync($"{memberPath}/list-plan-year")).Data!.AsArray()
            .Select(y => y!).OrderBy(y => DatePart(y["from_Date"]), StringComparer.Ordinal).ToList();
        var planYear = (planYears.FirstOrDefault(y => Covers(y, "from_Date", "thru_Date", today)) ?? planYears[^1])["plan_Year"]!.GetValue<string>();
        Console.WriteLine($"Plan year {planYear}");
        

Example response (illustrative; field values are fake):

{
  "data": [
    {
      "group_ID": "DOCS-GRP1",
      "division_ID": "DOCS-DIV1",
      "plan_Year": "2030",
      "from_Date": "2030-01-01T00:00:00",
      "thru_Date": "2030-12-31T00:00:00",
      "qtr4_Accum": false,
      "qtr4_Deductible": false,
      "qtr4_Member": false,
      "qtr4_Out_Of_Pocket": false,
      "qtr4_Subscriber": false
    }
  ],
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}

Save from the response: plan_Year (step 4).

4. Show deductible and out-of-pocket progress

GET /subscribers/{subscriberID}/members/{memberSeq}/list-accum-detail

Pass groupId, planId, and planYear as query parameters. Each row has the limits and the amounts used so far, for the member, the family (subscriber), and by network.

        var query = $"groupId={Uri.EscapeDataString(groupId)}&planId={Uri.EscapeDataString(planId)}&planYear={Uri.EscapeDataString(planYear)}";
        var accumulators = (await api.GetAsync($"{memberPath}/list-accum-detail?{query}")).Data!.AsArray();
        foreach (var row in accumulators)
        {
            var oopMax = Amount(row!, "ooP_Max_Mem");
            var oopUsed = Amount(row!, "out_Of_Pocket");
            Console.WriteLine($"Deductible: {Amount(row!, "deductible_Used"):F2} of {Amount(row!, "member_Deductible"):F2} met");
            Console.WriteLine($"Out of pocket: {oopUsed:F2} of {oopMax:F2}, {Math.Max(oopMax - oopUsed, 0):F2} to go");
        }
        

These fields answer the two common questions. The sample uses the member-level, all-network fields:

QuestionLimitUsed so far
How much of my deductible have I met?member_Deductibledeductible_Used
How much of my family's deductible is met?subscriber_Deductiblefam_Deductible_Used
How much until my out-of-pocket maximum?ooP_Max_Memout_Of_Pocket
How much until my family's out-of-pocket maximum?ooP_Max_Subfam_Out_Of_Pocket

Fields with PPO or Net in the name are the in-network figures, and OON the out-of-network figures, for plans that track them separately; ooP_Calc_Option describes how the plan combines them. Have your benefits team confirm which fields match your plan designs before you show them to members.

Example response (illustrative; field values are fake):

{
  "data": [
    {
      "plan_ID": "DOCS-PLAN1",
      "member_Deductible": 1500.0,
      "deductible_Used": 400.0,
      "subscriber_Deductible": 3000.0,
      "fam_Deductible_Used": 650.0,
      "ooP_Max_Mem": 5000.0,
      "out_Of_Pocket": 820.0,
      "ooP_Max_Sub": 10000.0,
      "fam_Out_Of_Pocket": 1100.0
    }
  ],
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}

5. Show claim history

GET /subscribers/{subscriberID}/members/{memberSeq}/list-member-claim

This returns every claim for the member, newest service date first, in one response. It takes no paging or filter parameters, so page and filter in your app (for example, the last 12 months, 10 per screen).

        var claims = (await api.GetAsync($"{memberPath}/list-member-claim")).Data?.AsArray()
            .Select(c => c!).ToList() ?? new List<JsonNode>(); // newest service date first
        foreach (var claim in claims.Take(10)) // the endpoint returns every claim; page them in your app
        {
            var state = claim["inBatch"]?.GetValue<int>() == 1 ? "in process" : claim["claim_Status"]?.ToString();
            Console.WriteLine($"{DatePart(claim["service_Date"])} {claim["provider_Name"]} {state} billed {claim["billedAmount"]} paid {claim["paidAmount"]}");
        }
        

Fields worth showing a member:

FieldMeaning
service_Date, service_ThruDates of service
provider_NameWho provided the care
inBatch1 for a claim still being entered or adjudicated (in a claim batch), 0 for a processed claim
claim_StatusThe claim's status code. Codes are configured per database; read their descriptions with GET /claim-statuses and agree member-friendly wording with your claims team
billedAmount, paidAmountWhat the provider billed and what the plan paid
check_Number, check_DatePayment details, once paid
claim_NumberThe processed claim's number, used for the EOB in step 6

Example response (illustrative; field values are fake):

{
  "data": [
    {
      "inBatch": 0,
      "claim_Number": 900001,
      "batch_Number": 7001,
      "batch_Claim": 1,
      "subscriber_ID": "DOCS-0001",
      "member_Seq": "01",
      "provider_Name": "Example Clinic",
      "service_Date": "2030-03-04T00:00:00",
      "service_Thru": "2030-03-04T00:00:00",
      "claim_Status": "PAID",
      "billedAmount": 250.0,
      "paidAmount": 180.0,
      "check_Number": "No Check"
    }
  ],
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}

6. Get an EOB

GET /claims/{claimNumber}/eob

For a processed claim, this returns the member's Explanation of Benefits. If your company uses Zelis RedCard EOBs, VBAPI tries that first; otherwise, or if that fails, VBASoftware generates the EOB. report_Bytes holds the document (generally a PDF) as base64, and isVBA_EOB says which source produced it. Stream it to the signed-in member; do not store or log it.

        var processed = claims.FirstOrDefault(c => c["inBatch"]?.GetValue<int>() == 0 && c["claim_Number"] is not null);
        if (processed is not null)
        {
            var claimNumber = processed["claim_Number"]!.GetValue<int>();
            var eob = (await api.GetAsync($"/claims/{claimNumber}/eob")).Data!;
            var reportBytes = eob["report_Bytes"]?.GetValue<string>();
            var pdf = reportBytes is null ? Array.Empty<byte>() : Convert.FromBase64String(reportBytes);
            // Stream the PDF to the signed-in member. Do not store or log it: it contains PHI.
            Console.WriteLine($"EOB for claim {claimNumber} {pdf.Length} bytes");
        }
        

Check that the EOB's subscriber_ID and member_Seq match the signed-in member before you return it.

7. Request a replacement ID card

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

This is the only step that writes data. The body is the same as in the enrollment playbook, with the member's current group and division and the request type your database uses for replacements:

{
  "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-03-10T00:00:00",
  "request_Quantity": 1,
  "physical_Card_Requested": true
}

The sample takes the first request type for simplicity. In your app, choose the replacement type by its description.

        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 = MemberSeq,
            group_ID = groupId,
            division_ID = divisionId,
            request_Type = requestTypes[0]!["request_Type"]!.GetValue<string>(),
            request_Status = "REQ",
            request_Date = $"{today}T00:00:00",
            request_Quantity = 1,
            physical_Card_Requested = true,
        })).Data!;
        var cardKey = card["idCardRequest_Key"]!.GetValue<int>();
        Console.WriteLine($"Replacement ID card requested: {cardKey}");
        

Save from the response: idCardRequest_Key (step 8). Each call creates a new request, so stop members from submitting twice, for example by checking GET /id-card-requests on your server for a recent open request.

8. Check the request

GET /id-card-requests/{iDCardRequestKey}

Show the member the request's request_Status: REQ (Requested), COMPLETE, or CANCEL, from GET /id-card-request-statuses.

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

Verify the result

Run the sample against a test member whose plan, accumulators, and claims you know. The deductible and out-of-pocket figures should match what VBASoftware shows for the same plan year, and the claim list should match the member's claims. Step 8 confirms the ID card request was saved.

Caching and performance

A member screen can need five or more calls, and every call counts toward your API key's rate limit of 25 requests per second.

  • Cache per member on your server, never on the device. Member details, enrollments, plans, and plan years rarely change; caching them for the session, or for several hours, is reasonable.
  • Accumulators and claims change when claims are adjudicated. Cache them briefly (minutes, not days), refresh them when the member opens the screen, and show an "as of" time.
  • Request types and statuses are lookups; cache them for a day.
  • Load the screens a member opens, not everything at sign-in.

Undo

A replacement request can be cancelled by updating it with request_Status CANCEL and a cancellation_Reason through PUT /id-card-requests/{iDCardRequestKey}, or removed with DELETE /id-card-requests/{iDCardRequestKey} (204). The other steps only read data.

Common errors

SymptomLikely causeFix
404 on step 1Wrong subscriberID or memberSeq; memberSeq is two characters, for example 01Check your mapping from the signed-in member
Empty accumulator listgroupId, planId, or planYear does not match the member's enrollmentUse the values from steps 2 and 3
404 or 500 on the EOBThe claim number is not a processed claim, or the EOB could not be producedOnly request EOBs for claims with inBatch 0; contact support with the activityID if it persists
429 during busy periodsToo many calls per second for the API keyCache as above; the sample helpers retry with backoff
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/member-self-service/: MemberSelfServicePlaybook.cs, member_self_service.py, and memberSelfService.ts. See _samples/playbooks/README.md for how to run them.