Skip to content
All posts
ConfigurationDevelopersReference

Product definition reference

Every field of a product definition, every check it must pass, how a request runs through it, and the API that authors, executes, explains and replays products.

The BimaStack team · · 9 min read

Every field of a product definition, every check it must pass, how a request runs through it, and the API that authors, executes, explains and replays products. The guide builds one end to end.

Identity and versions

A definition is a PRODUCT_DEFINITION setting identified by organisation, optional binder, and product code, with the key definition. Versions, effective dates, two-person approval and audit are the settings machinery’s; a binder’s definition overrides the organisation’s for that binder. It is published dated: a future start leaves today’s version live until then.

The document

Shape (schemaVersion 1)
{
  "schemaVersion": 1,
  "name": "Outbound Travel Insurance",
  "currency": "USD",
  "records":   [ { "name": "Traveller", "fields": [ ... ] } ],
  "inputs":    [ ... ],
  "coverages": [ ... ],
  "stages":    { "validation": {...}, "eligibility": {...}, "underwriting": {...},
                 "pricing": {...}, "quote": {...} },
  "rules":     [ ... ],
  "options":   { "pricingOnDecline": false },
  "carrier":   { "name": "Insurer X Ltd", "reference": "IX-TRAVEL-2026" },
  "source":    { "name": "Broker Y Ltd", "reference": null },
  "quoteClass": { ... }
}

The document is read strictly: an unknown property is a DEFINITION_UNPARSEABLE finding, never dropped. carrier is required on a broker’s or agency’s product and refused on an insurer’s; source only comes with a carrier.

inputs

Field Meaning
name, typetype is STRING, NUMBER, BOOLEAN, DATE, ENUM, LIST or RECORD
allowedValuesFor an ENUM
elementType, recordFor a LIST, and for a RECORD or LIST of RECORD (records are declared under records, scalar fields only)
label, description, displayOrderFor forms and the generated schema
constraintsrequired, default, minimum, maximum (inclusive), allowedValues, minItems, maxItems, dateRange, ageRange, requiredWhenCoverage
  • dateRange bounds are today (the request’s date), an ISO date, or another DATE input’s name; an absent referenced input sets no bound.
  • ageRange is whole years on the request’s date, for a date of birth; one after the date is out of range.
  • requiredWhenCoverage makes an input required when a named cover is on.
  • Record fields are checked the same way, recursively, with paths like inputs.travellers[1].dateOfBirth.

coverages

Field Forms
code, name, type, parenttype SECTION, COVERAGE or BENEFIT; parent makes the tree
mandatory, defaultSelectedAlways on; on unless the request deselects it
limit{currency, fixed} · {options[], default} · {min, max, default}
deductible, excess{amount} or {percent, of: SUM_INSURED | CLAIM, minimum}
waitingPeriod{days}
requires, excludesCodes that must, or must not, be on together
enabledWhenA BOOLEAN input that switches it on
conditionsText shown on the quote, not evaluated

Resolution is deterministic: mandatory, default and requested items are on; a child of an item that is off is off; a child explicitly requested under a parent that isn’t chosen is a COVERAGE_CONFLICT, never silently dropped. Stages receive the covers that are on as coverages, a list of records with code, type, parent, limitAmount, deductibleAmount, deductiblePercent, excessAmount, excessPercent and waitingDays (absent amounts 0, no parent "").

stages

A stage served by a workflow written with its own names, unedited
"pricing": { "workflow": "motor-rating",
  "inputMapping":  { "sumInsured": "vehicleValue",
                     "ratingDate": "context.asOfDate",
                     "currency": "context.currency",
                     "loadingPercent": "underwriting.loadingPercent" },
  "outputMapping": { "pricing.netPremium": "premium",
                     "pricing.taxTotal": "levies",
                     "pricing.total": "total" } }
A stage input may be filled from Value
an input’s nameThe checked, normalised answer
coveragesThe covers that are on
context.asOfDateThe request’s date (DATE or STRING)
context.currencyThe definition’s currency
<stage>.<output>Any output of an earlier stage, e.g. underwriting.loadingPercent

A stage takes only what it declares. A workflow record may ask for a subset of a product record’s fields; a field the workflow wants that the product lacks is a stage failure, never a default. A product DATE fills a DATE or STRING; an ENUM fills an ENUM or STRING. Stages run in the order validation, eligibility, underwriting, pricing; any may be absent.

The pricing contract

Output Type
pricing.totalNUMBERRequired: the amount payable, tax and fees included
pricing.netPremiumNUMBEROptional: before tax and fees
pricing.taxTotal, pricing.feeTotalNUMBEROptional
pricing.linePremiumsLIST<NUMBER>Optional: one per cover that is on, in order

The quote rounds these to two places, half to even. It doesn’t check that lines add up to the total, so round before combining in the workflow when they must.

rules

{code, stage, effect (ERROR | DECLINE | REFER), message, firedBy, coverage?}. firedBy names a BOOLEAN output of that stage’s workflow, after its outputMapping; a missing or non-BOOLEAN flag is a stage failure, never “not fired”. Pricing-stage rules are assessed after pricing and can move an ACCEPT to REFER or DECLINE; whether pricing runs is decided by the earlier stages.

quoteClass

Part Meaning
codeThe standard form answered, e.g. TRAVEL, MOTOR_PRIVATE, MEDICAL
inputs.<name>Exactly one source: from (a question, or list.field), value (a constant), or addOn (true when chosen)
asYEARS_SINCE, AGE_AT (with at), DOB_FROM_AGE, COUNT, SUM, DAYS_BETWEEN, DAYS_SPANNED (with to)
valuesTranslates answers; an answer with no entry leaves the product out of that comparison
fieldsFills a record, or a list of records, item by item
refineFromA question asked once a plan is chosen that gives a better value; the product is priced again with it at review
acceptsThe only answers it prices, per question
addOns, coverageLimits, highlightsForm add-on → product cover; a limit from an amount question; the cover each price-card line reads

Checks

A finding is {rule, path, message, tier}. STRUCTURAL findings refuse the draft. REFERENCE findings are reported by validate and block publish, since a workflow may be written after the definition.

Area Rules
Header and requestDEFINITION_UNPARSEABLE, SCOPE_PRODUCT_CODE_REQUIRED, NAME_REQUIRED, CURRENCY_INVALID, SCHEMA_VERSION_UNSUPPORTED, EFFECTIVE_WINDOW_INVALID
Inputs and recordsDUPLICATE_INPUT_NAME, INPUT_ENUM_VALUES_REQUIRED, INPUT_LIST_ELEMENT_TYPE_REQUIRED, RECORD_UNKNOWN, RECORD_FIELD_NOT_SCALAR, CONSTRAINT_NOT_APPLICABLE, CONSTRAINT_RANGE_INVALID, CONSTRAINT_DATE_BOUND_INVALID
CoveragesDUPLICATE_COVERAGE_CODE, COVERAGE_PARENT_UNKNOWN, COVERAGE_TREE_CYCLE, COVERAGE_REQUIRES_UNKNOWN, COVERAGE_EXCLUDES_UNKNOWN, COVERAGE_ENABLED_WHEN_INVALID, COVERAGE_LIMIT_INVALID
RulesDUPLICATE_RULE_CODE, RULE_EFFECT_REQUIRED, RULE_FIRED_BY_REQUIRED, RULE_STAGE_UNKNOWN, RULE_COVERAGE_UNKNOWN
CarrierCARRIER_REQUIRED, CARRIER_NOT_ALLOWED, SOURCE_WITHOUT_CARRIER
Stage fit (REFERENCE)STAGE_WORKFLOW_NOT_FOUND, STAGE_INPUT_UNRESOLVED, STAGE_INPUT_TYPE_MISMATCH, STAGE_OUTPUT_MAPPING_UNKNOWN, RULE_FLAG_OUTPUT_MISSING, RULE_FLAG_NOT_BOOLEAN, PRICING_OUTPUT_MISSING, PRICING_OUTPUT_INVALID
Quote classQUOTE_CLASS_INPUT_UNKNOWN, QUOTE_CLASS_INPUT_UNMAPPED (a required input left unfilled), QUOTE_CLASS_SOURCE_INVALID, QUOTE_CLASS_VALUES_NOT_ALLOWED
Quote form fit (REFERENCE)QUOTE_CLASS_FORM_NOT_FOUND, QUOTE_CLASS_QUESTION_UNKNOWN, QUOTE_CLASS_NEEDS_PROPOSAL_DATA, QUOTE_CLASS_TYPE_MISMATCH, QUOTE_CLASS_ADD_ON_UNKNOWN

How a request runs

# Step Fails with
1Find the version in effect on the datePRODUCT_NOT_FOUND, NO_PRODUCT_VERSION_IN_EFFECT
2Check inputs, resolve covers, conditional requirementsINVALID_REQUEST with every error at once: INPUT_REQUIRED, INPUT_UNKNOWN, INPUT_TYPE_INVALID, INPUT_NOT_ALLOWED, INPUT_OUT_OF_RANGE, LIST_TOO_SHORT, LIST_TOO_LONG, COVERAGE_UNKNOWN, COVERAGE_LIMIT_INVALID, COVERAGE_CONFLICT
3–5Validation, eligibility, underwriting workflows, then their rulesA fired ERROR rule: INVALID_REQUEST with RULE_ERROR
6Pricing, unless an earlier stage declined (and pricingOnDecline is off)STAGE_FAILED, PRICING_OUTPUT_MISSING, PRICING_OUTPUT_INVALID
7Outcome: DECLINE, else REFER, else ACCEPT
8–9Assemble the quote and the per-stage trace

A stage that can’t do its job (its workflow not in effect, an input it can’t be given, a value of the wrong type) fails closed with its stage and workflow named. An eligibility check that couldn’t run must never read as “eligible”.

Executing: API version 2

Send the header API-Version: 2 with every product call. A version the API doesn’t serve is 400 API_VERSION_UNSUPPORTED. Within a version, changes are only additive: a field may be added, never renamed or removed; a breaking change is a new version served beside it.

Call Does
POST /api/products/{code}/executeRun the published product; return the quote; record it
POST /api/products/{code}/explainAs execute, with the per-stage trace; recorded
POST /api/products/{code}/simulateAs execute against an inline draft definition; not recorded
GET /api/products/{code}/schemaA JSON Schema of the inputs, the cover catalogue, every reason code and the stages
GET /api/product-computationsRecorded computations, newest first, by product, outcome and time
GET /api/product-computations/{requestId}One: the request, the quote, the definition version
POST /api/product-computations/{requestId}/replayRun it again as recorded and report what changed
A request
curl -s -X POST https://<your-address>/api/products/travel-outbound/execute \
  -H "Authorization: Bearer $TOKEN" -H "API-Version: 2" -H "Content-Type: application/json" \
  -d '{
    "organizationId": "<insurer>",
    "binderCode": null,
    "asOfDate": "2026-10-09",
    "inputs": { "destinationZone": "WORLDWIDE_PLUS", "tripDays": 10,
                "travellerAges": [10, 70] },
    "coverages": [ { "code": "BAGGAGE", "selected": true } ],
    "options": { "includeTrace": false }
  }'
The quote (abridged)
{ "requestId": "019a…", "contractVersion": "2",
  "product": { "code": "travel-outbound", "name": "Outbound Travel Insurance",
               "versionId": "018f…", "versionNumber": 3, "effectiveFrom": "2026-10-01" },
  "asOfDate": "2026-10-09", "currency": "USD",
  "outcome": "ACCEPT", "reasons": [],
  "coverages": [ { "code": "BAGGAGE", "enabled": true, "limitAmount": 1500, … } ],
  "premium": { "netAmount": null, "taxAmount": null, "feeAmount": null,
               "totalAmount": 69.86, "lines": [] },
  "rateVersionsUsed": [
    { "kind": "PRODUCT_DEFINITION", "key": "travel-outbound", "version": 3, … },
    { "kind": "RATE_TABLE", "key": "travel-outbound-base-premium", "version": 2, … } ],
  "trace": null }
  • Numbers are exact decimals as sent; nothing is coerced, so "10" for a NUMBER is INPUT_TYPE_INVALID.
  • Every field is always present, null when it doesn’t apply; premium is null for a DECLINE.
  • rateVersionsUsed lists everything versioned the quote read: the definition, each stage workflow, every table, setting, formula and block link.
  • Errors are RFC 7807 problems with a stable code, the requestId, and for a bad request errors[] of {code, fieldPath, message}. A rejected request isn’t recorded.

Recorded and replayable

Every execute and explain writes one insert-only row: the request, the quote and the definition version. Its id is the requestId. Replay runs the request again at its own date against the same definition version, and says what moved:

Replay
{ "identical": false,
  "differences": [ "premium",
    "rateVersionsUsed: RATE_TABLE/travel-outbound-base-premium@v1 is no longer used",
    "rateVersionsUsed: RATE_TABLE/travel-outbound-base-premium@v2 is now used" ] }

The usual cause is a table or workflow republished with a start date on or before the quote’s date. A record also says who sold the product and on what basis: their own, a distribution agreement, through a broker, on a storefront or as platform content, and who carries the risk.

Authoring API

Call Does
GET /api/product-definitions?productCode=&organizationId=&binderId=&status=Versions, a page at a time
GET /api/product-definitions/{id}{summary, definition}
POST /api/product-definitions/validate{organizationId, productCode, asOfDate, definition} → {valid, findings}
POST /api/product-definitions{organizationId, binderId, productCode, definition, effectiveFrom, effectiveTo, expectedCurrentVersion, reason}; 422 on a structural finding
POST /api/product-definitions/{id}/approve · publish · retire{reason}. Approving your own draft is a 403; publishing with a REFERENCE finding is a 422

Integrate a product

One contract prices every product, in Sandbox first.

For developers