Skip to content
All posts
ConfigurationInsurers

Rate tables: your whole rate card as one approved document

Keys, bands and rates, checked for gaps and overlaps, loaded from Excel, approved by a second person and scheduled to a date. And how a workflow reads them.

The BimaStack team · · 6 min read

A rate table holds one whole rate card, every row of it, as a single document. It is checked for gaps and overlaps before it is saved, approved by a second person, and takes effect on the date you choose, all at once. Rates live here and only here, never as numbers typed inside a formula, so a rate change is publishing a new table.

The shape of a table

A table has some key columns (exact matches: a zone, a cover type, “individual” or “family”), an optional band (a numeric range: days, age, sum insured) and a rate on each row.

Zone Policy Days from Days to Rate (USD)
WORLDWIDE_BASICINDIVIDUAL1922.34
WORLDWIDE_BASICINDIVIDUAL91530.21
WORLDWIDE_PLUSFAMILY1975.84
AFRICA_ASIAINDIVIDUAL1912.90
The same table as stored
{
  "schemaVersion": 2,
  "name": "Outbound travel base premium",
  "keys": ["zone", "policy"],
  "band": { "name": "days", "allowGaps": false },
  "rows": [
    { "keys": ["WORLDWIDE_BASIC", "INDIVIDUAL"], "from": 1, "to": 9,  "rate": 22.34 },
    { "keys": ["WORLDWIDE_BASIC", "INDIVIDUAL"], "from": 9, "to": 15, "rate": 30.21 },
    { "keys": ["WORLDWIDE_PLUS",  "FAMILY"],     "from": 1, "to": 9,  "rate": 75.84 },
    { "keys": ["AFRICA_ASIA",     "INDIVIDUAL"], "from": 1, "to": 9,  "rate": 12.90 }
  ]
}
  • A band matches from ≤ value < to. “1 to 8 days” is written from 1 to 9. The top of one band is the bottom of the next, so there is never a value that falls between them.
  • Keys are text and matched by position: the first key of every row is the zone. The labels are for people.
  • Any number of keys, or none (a table that is only a band, such as a minimum premium by sum insured).
  • A rate may be negative, for a discount table.
  • Up to 5,000 rows in one table.
  • A number to match exactly (a limit of 500,000) is a band too: from 500000 to the next limit.

Checked before it is saved

A table with any problem is refused, with every problem listed and pointing at its row. A half-wrong rate card can never reach a quote.

Problem Finding
The same keys twice (no band)DUPLICATE_ROW_KEY
Two bands for the same keys that share valuesBAND_OVERLAP
Values between two bands that no row covers (unless the table allows gaps)BAND_GAP
A row with the wrong number of keys, or a blank oneROW_KEY_COUNT_MISMATCH, ROW_KEY_MISSING
from not below to, or a band missingROW_BAND_INVALID, ROW_BAND_MISSING
No rate on a rowROW_RATE_REQUIRED
A misspelt column ("raet")TABLE_UNPARSEABLE: never silently dropped

Nobody can know the largest value a quote will bring, so give the top band a generous “to”. A value that still matches no row stops the quote with a clear error. It is never priced at zero.

Bring it in from Excel

  1. 1

    Download the template

    Made from your table’s own shape: a column per key, the band’s from and to, then rate. Key columns are text, so a code like 01 keeps its zero.

    Rate tables
  2. 2

    Fill it in

    Type or paste values. Numbers may carry thousands commas (5,000,000). Blank rows are skipped.

  3. 3

    Upload it

    Every problem comes back with the row number Excel shows. Nothing is saved until you review the table and save it as a draft.

Approve, then schedule

  • A draft prices nothing. Save as often as you like.
  • A second person approves. Whoever drafted it can’t (unless they are their organisation’s only member).
  • Publish with a start date. Publish next month’s card today: this month’s keeps pricing until the new one starts, then the whole new card applies at once.
  • History stays. A quote for a past date reads the table that was live then.
  • Retire ends a version with no replacement.

One code, many scopes

A table is found by its code (travel-outbound-base-premium) in a scope: your organisation, and optionally a binder or a product. The most specific wins, so a scheme rate negotiated for one binder overrides your standard card for that binder only, and the workflow that reads it doesn’t change.

Reading a table in a workflow

A workflow reads a rate with a ready-made table-rate block. You give it the table’s code and wire in the keys and the band value; it returns the rate.

Block Wired inputs Matches
protected:table-rate:bandxThe band only
protected:table-rate:1d / 2d / 3d / 4dkey1 … key4Exact keys
protected:table-rate:1d-band / 2d-band / 3d-bandkey1 … key3, xExact keys and the band
Pricing a trip from the table above
workflow "travel-base" {
    input destinationZone : STRING
    input familyPolicy : BOOLEAN
    input tripDays : NUMBER

    formula policyKey {
        out label : STRING
        in familyPolicy : BOOLEAN
        expression: if(familyPolicy, "FAMILY", "INDIVIDUAL")
    }
    block baseRate {
        in key1 : STRING
        in key2 : STRING
        in x : NUMBER
        out rate : NUMBER
        param tableCode = "travel-outbound-base-premium"
        binding: block "protected:table-rate:2d-band"
    }
    output result {
    }

    connect familyPolicy -> policyKey.familyPolicy
    connect destinationZone -> baseRate.key1
    connect policyKey.label -> baseRate.key2
    connect tripDays -> baseRate.x
    connect baseRate.rate -> result.basePremium
}

These blocks are ordinary workflows written in the same language as yours, shipped by the platform. When the table has no matching row, the block stops the run with “FIRST_MATCH found no matching element”.

For developers

Call Does
GET /api/rate-tables?tableCode=&organizationId=&productId=&status=Every version, a page at a time
GET /api/rate-tables/{id}One version: {summary, table}
POST /api/rate-tables/validateCheck without saving: {valid, findings}
POST /api/rate-tablesSave a draft: {scope, tableCode, table, effectiveFrom, effectiveTo, expectedCurrentVersion, reason}; 422 with findings
POST /api/rate-tables/{id}/approve · publish · retireLifecycle, each with {reason}
GET /api/rate-tables/{id}/template.xlsx · GET /api/rate-tables/template.xlsx?name=&keys=&band=The Excel template, filled or blank
POST /api/rate-tables/workbookRead a filled-in template back: {valid, table, findings}; saves nothing

expectedCurrentVersion is null for a table’s first version and the version you edited from after that; someone else’s save in between is a 409. A finding is {rule, path, message}, with sheetRow when it came from a workbook.

Load your rate card

Bring the spreadsheet; we’ll help you set up the first table.

Book a demo