Skip to content

Bill premiums and apply payments

This playbook is for finance and billing teams that want to automate the monthly premium cycle. You generate a group's premium invoice, check its totals, and import a bank payment file (CSV) that creates the payments and posts them to the invoice, without keying payments by hand.

Who this is forPremium billing and finance teams, and the developers who automate their bank files
Time to runAbout 15 minutes
Writes data?Yes. Try it in a non-production database first if you have one. "Try it" calls in the API reference are real production calls.
Operations used9, linked in each step
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 completed the quick start and your environment variables are set.
  • A test group with enrolled subscribers, premium rates, and a billing cycle already exists. The samples use the fake group DOCS-GRP1; replace it at the top of each sample.
  • The group is not set up for self-admin billing. Self-admin groups use a different set of operations that this playbook does not cover.
  • You know the payor and account that payments should be recorded against. GET /payor-accounts lists each payor with its account_Key.
  • A premium payment file format is set up in VBASoftware.
  • Your API user has rights to premium billing and premium payments.

How it fits together

VBAPIYour systemVBAPIYour systemGET /groups/{groupID}, GET /billing-cycles/{billingCycleID}POST /generate-group-invoiceinvoice_KeyGET /premium-invoices/{invoiceKey}/summaryGET /premium-payment-file-formatsPOST /process-payment-file (base64 CSV)payment and invoice countsGET /premium-invoice-payments-ext/{invoiceKey}
VBAPIYour systemVBAPIYour systemGET /groups/{groupID}, GET /billing-cycles/{billingCycleID}POST /generate-group-invoiceinvoice_KeyGET /premium-invoices/{invoiceKey}/summaryGET /premium-payment-file-formatsPOST /process-payment-file (base64 CSV)payment and invoice countsGET /premium-invoice-payments-ext/{invoiceKey}

Steps

1. Confirm the group's billing cycle

GET /groups/{groupID}, then GET /billing-cycles/{billingCycleID}

The group record says which billing cycle its invoices use (billing_Cycle_ID) and whether it is a self-admin group (self_Admin). The billing cycle tells you how often the group is billed, so you can pick the right billing period in step 2. To see every cycle, use GET /billing-cycles. Both calls only read data.

        var group = (await api.GetAsync($"/groups/{GroupId}")).Data!;
        if (group["self_Admin"]?.GetValue<bool>() == true)
            throw new InvalidOperationException($"{GroupId} uses self-admin billing, which this playbook does not cover.");
        var cycle = (await api.GetAsync($"/billing-cycles/{group["billing_Cycle_ID"]}")).Data!;
        Console.WriteLine($"Billing cycle: {cycle["billing_Cycle_ID"]} {cycle["description"]}");
        

2. Generate the group invoice

POST /generate-group-invoice

This builds one premium invoice for the group for the billing period you give it, and returns it. The GeneratePremInvoice schema marks no field as required, but the API source (FABilling/Services/BillingService.cs) rejects a request with 400 unless group_ID and bill_From are set, and requires division_ID and subscriber_ID to be empty for a group invoice. The minimal body:

{
  "group_ID": "DOCS-GRP1",
  "bill_From": "2030-02-01T00:00:00",
  "bill_Thru": "2030-02-28T00:00:00",
  "invoice_Date": "2030-01-15T00:00:00",
  "due_Date": "2030-02-01T00:00:00",
  "regenerate": false
}

Do not send user_ID: VBAPI sets it to the API user. Use the billing period you actually bill; the 2030 dates are placeholders.

        var invoice = (await api.PostAsync("/generate-group-invoice", new
        {
            group_ID = GroupId,
            bill_From = "2030-02-01T00:00:00",
            bill_Thru = "2030-02-28T00:00:00",
            invoice_Date = "2030-01-15T00:00:00",
            due_Date = "2030-02-01T00:00:00",
            regenerate = false,
        })).Data!;
        var invoiceKey = invoice["invoice_Key"]!.GetValue<int>();
        Console.WriteLine($"Invoice {invoiceKey} number {invoice["invoice_Number"]} status {invoice["invoice_Status"]}");
        

Example response (illustrative; field values are fake, and some fields are left out):

{
  "data": {
    "invoice_Key": 900001,
    "invoice_Number": "DOCS-900001",
    "invoice_Level": "G",
    "invoice_Status": "OPEN",
    "billing_Cycle_ID": "MO",
    "billing_From": "2030-02-01T00:00:00",
    "billing_Thru": "2030-02-28T00:00:00",
    "invoice_Date": "2030-01-15T00:00:00",
    "due_Date": "2030-02-01T00:00:00",
    "bill_Name": "Sample Employer Group",
    "finalized": false
  },
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}

Save from the response: data.invoice_Key (the invoiceKey path parameter, and the Invoice_Key column in the payment file).

Running it again: each call with regenerate set to false takes a new invoice key, so a second call creates a second invoice for the same period. To rebuild an existing invoice, use POST /invoice-regenerate/{invoiceKey}, which deletes the invoice and its attached adjustments and payments and rebuilds it under the same key. With regenerate set to true, this operation reuses the invoice_Key you send instead of taking a new one.

3. Review the invoice

GET /premium-invoices/{invoiceKey}/summary

The summary totals the invoice: premium, premiumReceived, postedPayment, unpostedAmount, the adjustment totals, amountDue, and subscriber and member counts. Check that the counts and premium match what you expect before you send the invoice or apply money to it. GET /premium-invoices/{invoiceKey} returns the invoice header. Both calls only read data.

        var summary = (await api.GetAsync($"/premium-invoices/{invoiceKey}/summary")).Data!;
        Console.WriteLine($"Premium: {summary["premium"]} Amount due: {summary["amountDue"]} Subscribers: {summary["subscriberCount"]}");
        

4. Find the payment file format

GET /premium-payment-file-formats

The payment file request needs a payment_File_Format_Key. List the formats and use the one your administrator set up for bank files. The samples print every format and use DOCS_PAYMENT_FILE_FORMAT_KEY if it is set, otherwise the first one.

        var formats = (await api.GetAsync("/premium-payment-file-formats")).Data?.AsArray() ?? new JsonArray();
        foreach (var f in formats)
            Console.WriteLine($"File format {f?["payment_File_Format_Key"]} {f?["description"]}");
        var formatKey = int.TryParse(Environment.GetEnvironmentVariable("DOCS_PAYMENT_FILE_FORMAT_KEY"), out var configured)
            ? configured
            : formats[0]!["payment_File_Format_Key"]!.GetValue<int>();
        

Save from the response: payment_File_Format_Key.

5. Prepare the payment file

The payment file is a CSV with a header row. These are the columns from the VBA_ProcessPaymentFile schema:

ColumnRequiredMeaning
Payment_AccountYesName of the paying account
Payment_IDYesYour unique ID for the payment, for example the bank reference
Payment_DateYesPayment date, YYYY-MM-DD
Payment_AmountYesAmount of the payment
Payment_NameYesName of the payer
Payor_IDYesPayor for the payment; the request's payor_ID overrides it
Account_KeyYesAccount for the payment; the request's account_Key overrides it when above 0
Payment_TypeNoA value from the premium payment types table
Payment_Batch, Payment_Batch_SeqNoBatch and sequence from your source system
Group_ID, Division_ID, Subscriber_IDNoWho the payment is from
Invoice_KeyNoInvoice to post the payment to
PremRate_KeyNoRate to apply the payment to
PremPayment_KeyNoExisting payment to apply the payment to

This sample file has two fake payments: one for the invoice from step 2 and one with no invoice. The {{INVOICE_KEY}} placeholder is replaced with the real invoice key before the file is sent; a file with the placeholder still in it is rejected.

Payment_Type,Payment_Batch,Payment_Batch_Seq,Payment_Account,Payment_ID,Payment_Date,Payment_Amount,Payment_Name,Group_ID,Division_ID,Subscriber_ID,Payor_ID,Account_Key,Invoice_Key,PremRate_Key,PremPayment_Key
,DOCS-BATCH-1,1,DOCS Sample Bank Account,DOCS-PAY-0001,2030-02-01,250.00,Sample Employer Group,DOCS-GRP1,,,DOCS-PAYOR,0,{{INVOICE_KEY}},,
,DOCS-BATCH-1,2,DOCS Sample Bank Account,DOCS-PAY-0002,2030-02-01,75.00,Sample Employer Group,DOCS-GRP1,,,DOCS-PAYOR,0,,,

The request sends the file as a base64-encoded string in payment_File:

        var csvText = File.ReadAllText(SamplePath("payments-sample.csv"))
            .Replace("{{INVOICE_KEY}}", invoiceKey.ToString());
        var paymentFile = Convert.ToBase64String(Encoding.UTF8.GetBytes(csvText));
        

6. Import the payment file

POST /process-payment-file

This creates one premium payment for every row in the file and, depending on four options, posts the payments to invoices, applies them, and closes invoices. It returns 200 when it has finished. As with step 2, do not send user_ID; VBAPI sets it.

What the four options do, from BillingService.ProcessPaymentFile in the API source:

OptionWhen true
create_Invoice_PaymentsFor each row with an Invoice_Key, posts the row's full Payment_Amount to that invoice. Rows without an Invoice_Key only create a payment.
apply_PaymentsRuns auto-apply on each invoice in the file, the same as POST /premium-invoices/{invoiceKey}/auto-apply.
close_InvoicesRecalculates the amount due on each invoice in the file. An invoice with nothing due is closed. An overpaid invoice goes through the invoice tolerance process and stays open. For an invoice that still has a balance, see the next row.
only_Close_Satisfied_InvoicesLeaves invoices that still have a balance open. When this is false and close_Invoices is true, VBAPI posts an adjustment for the remaining balance and closes the invoice anyway, so the unpaid amount is written off.

A safe first run: create the payments and post them to invoices, but do not apply them or close anything. This is what the samples send:

{
  "payment_File_Format_Key": 1,
  "payment_File": "UGF5bWVudF9UeXBlLFBheW1lbnRfQmF0Y2gs...",
  "payor_ID": "DOCS-PAYOR",
  "account_Key": 0,
  "create_Invoice_Payments": true,
  "apply_Payments": false,
  "close_Invoices": false,
  "only_Close_Satisfied_Invoices": true
}

After you have checked the result (step 7), apply the payments with auto-apply. Once you trust the file and the invoices, you can let the import apply and close in one run by setting apply_Payments and close_Invoices to true. Keep only_Close_Satisfied_Invoices set to true unless your finance team wants unpaid balances written off.

        var result = (await api.PostAsync("/process-payment-file", new
        {
            payment_File_Format_Key = formatKey,
            payment_File = paymentFile,
            payor_ID = PayorId,
            account_Key = AccountKey,
            create_Invoice_Payments = true,
            apply_Payments = false,
            close_Invoices = false,
            only_Close_Satisfied_Invoices = true,
        })).Data!;
        Console.WriteLine($"Payments created: {result["payment_Created_Count"]}");
        Console.WriteLine($"Invoice payments posted: {result["invoice_Payment_Posted_Count"]}");
        Console.WriteLine($"Invoices closed: {result["invoice_Closed_Count"]} left open: {result["invoice_Left_Open_Count"]}");
        

Example response for the sample file and the safe first run (illustrative):

{
  "data": {
    "payment_Created_Count": 2,
    "invoice_Payment_Posted_Count": 1,
    "invoice_Closed_Count": 0,
    "invoice_Left_Open_Count": 1
  },
  "error": null,
  "debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}

How to read the counts:

  • payment_Created_Count: rows in the file. Every row creates a payment, whatever the options.
  • invoice_Payment_Posted_Count: rows posted to an invoice. Zero when create_Invoice_Payments is false.
  • invoice_Closed_Count and invoice_Left_Open_Count: the outcome for each row that has an Invoice_Key. With close_Invoices set to false, every such row counts as left open.

Invoices left open: compare amountDue in the invoice summary (step 7). A short payment needs follow-up with the group, or an adjustment if your finance team approves one. An overpayment has been through the tolerance process; check the invoice in VBASoftware.

7. Apply and check the payments

POST /premium-invoices/{invoiceKey}/auto-apply, then GET /premium-invoice-payments-ext/{invoiceKey}

Auto-apply spreads the payments posted to the invoice across its premium lines. It returns 202 and runs in the background, so wait a moment before you check. The samples only call it when DOCS_APPLY_PAYMENTS is true.

The extended invoice payment list shows each payment posted to the invoice with paymentPosted and paymentApplied, and the summary shows the new postedPayment and amountDue. Both calls only read data.

        if (Environment.GetEnvironmentVariable("DOCS_APPLY_PAYMENTS") == "true")
        {
            var accepted = await api.PostAsync($"/premium-invoices/{invoiceKey}/auto-apply");
            Console.WriteLine($"Auto-apply accepted: {(int)accepted.Status}"); // 202; it runs in the background
        }
        
        await foreach (var p in api.GetAllAsync($"/premium-invoice-payments-ext/{invoiceKey}"))
            Console.WriteLine($"Payment {p?["paymentId"]} amount {p?["paymentAmount"]} posted {p?["paymentPosted"]} applied {p?["paymentApplied"]}");
        var after = (await api.GetAsync($"/premium-invoices/{invoiceKey}/summary")).Data!;
        Console.WriteLine($"Posted payment: {after["postedPayment"]} Amount due: {after["amountDue"]}");
        

Verify the result

These calls only read data:

Clean up

Common errors

SymptomLikely causeFix
400 "You must pass in a valid Group ID" or "valid Bill From"group_ID or bill_From is missingSend both
400 "the Division ID must be blank"division_ID or subscriber_ID was sent to the group invoice operationLeave them out, or use the division variation below
404 "PremInvoice not found" from POST /generate-group-invoiceNothing was billable for the period, so no invoice was created. A division copied with copyPremiums does not bring the template's group-level premium ratesCheck the group's rates with GET /included-premium-rates?groupId=... and the enrollment dates
An error from the invoice summaryThe invoice has no subscriber or self-admin lines, for example because nobody was enrolled for the periodCheck the billing period and the group's enrollment
400 "Invalid payment file"payment_File is empty, is not base64, or has no data rowsBase64-encode the whole CSV, header row included
An error when the file is processedA column has the wrong type, for example the {{INVOICE_KEY}} placeholder was not replacedFix the row; numbers and dates must parse
Payments created twiceThe same file was imported twiceEach import creates a payment per row; undo and delete the duplicates
403 with {"message":"Forbidden"} on every callThe x-api-key header is missing or wrongSee 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.

Variations

  • Group and division invoices together: POST /generate-group-division-invoice takes the same body. It creates the group invoice for divisions billed at group level and a separate invoice for each division billed at division level. It returns 202 with no body, so find the new invoices afterwards with GET /premium-invoices-slim and its groupId and invoiceYear filters.
  • Safe to re-run? No. Generating again creates another invoice, and importing the same file again creates the payments again. Keep track of the invoice keys and Payment_ID values you have already sent.

Download

The complete runnable samples are in the documentation repository under _samples/playbooks/premium-billing-and-payments/: PremiumBillingPlaybook.cs, premium_billing.py, premiumBilling.ts, and the sample payment file payments-sample.csv, which each sample reads from its own folder. Each sample uses the shared helper for its language described in the quick start.