Loading documentation

Access required

Your account doesn't have access to the documentation. Contact your administrator.

Sign out
Skip to main content

Underwriting — Basis

Status: beta Category: underwriting Added: 2026-10-06 Last reviewed: 2026-10-06

Summary​

Returns the actuarial basis for an employer's workers' compensation programme: how many claims they open a year, what injuries cost them directly and indirectly, how that cost spreads across five claim sizes, and how much of it a care-management programme saves at the savings rates you send. You price your own deals on this basis; the engine returns no prices.

Endpoint​

FieldValue
MethodPOST
Path/api/underwriting/basis
Full URLhttps://engine.tripod-health.com/api/underwriting/basis
AuthenticationAPI key (required)
Request bodyapplication/json
Response bodyapplication/json
IdempotentYes
Rate-limitedYes
Credits450 per successful call, fixed

Purpose​

Anyone building a workers' compensation offer for an employer needs the same four answers before any commercial decision: the employer's claim volume, what those claims cost, where that cost sits by claim size, and what share of it is realistically movable. This operation supplies those four answers from whatever the employer has told you, and says how sure it is.

What it returns​

  • Claims a year, stated or estimated from the employer's industry injury rate, plus that industry rate whenever one is published for the code you send.
  • The employer's cost, split into claim costs, administration and premium.
  • Indirect cost (lost productivity, overtime, hiring and training) on top of claim costs, using OSHA's published indirect-cost ratios.
  • Five claim sizes, each with its share of claims, typical cost, share of claim spend and the savings rate it was priced on.
  • A blended savings rate across the employer's claim spend: your rates, or the engine's defaults when you send none.
  • Readiness and confidence: how complete the input is, and which request fields would most improve the answer.

What it deliberately does not return​

No costs of delivering a programme, no margins, no fees, no contract terms, no monthly series and no price. The request accepts none of those either. Your costs, overhead, margin targets, contract length, ramp-up and settlement rules are your own business decisions; apply them to this basis in your own application. How much your programme saves is your decision too: send it as savings_rates (see Savings rates are yours).

Authentication​

Every request must include a valid API key in the apikey header. Keys are issued per consumer; see authentication. A missing or unknown key returns 401 Unauthenticated at the gateway before the request reaches the engine.

Request​

Headers​

HeaderRequiredDescription
apikeyYesConsumer API key.
Content-TypeYesMust be application/json.

Path parameters​

None.

Query parameters​

None.

Body​

Every field is optional. A flat object; no nesting.

{
"covered_employees": 850,
"naics_code": "238220",
"states": 2,
"claim_costs_annual": 410000,
"administration_annual": 52000,
"premium_annual": 118000,
"savings_rates": [0.7, 0.59, 0.48, 0.36, 0.25]
}

Body fields​

FieldTypeRequiredConstraintsDescription
covered_employeesintegerNo> 0Employees the programme would cover. The denominator for every per-employee figure, and the exposure base when claims are estimated.
naics_codestringNo2-6 digitsThe employer's industry. Drives the claims estimate when claim_count_annual is absent.
ownershipstringNoprivate, state_government, local_governmentBLS ownership class used with naics_code. Absent means private.
statesintegerNo> 0Number of states the employer operates in. Carried for the record; no figure in the basis depends on it.
claim_costs_annualnumberNo>= 0, dollars a yearWhat the employer pays on claims in a typical year: medical, wage replacement and legal, combined.
administration_annualnumberNo>= 0, dollars a yearWhat the employer pays a year to have claims administered.
premium_annualnumberNo>= 0, dollars a yearInsurance premium a year: excess, stop-loss or guaranteed cost.
total_cost_annualnumberNo>= 0, dollars a yearClaim costs, administration and premium as one figure. Fills whichever parts you did not send; ignored when all three parts are present.
claim_count_annualnumberNo>= 0, claims a yearClaims opened in a typical year.
savings_ratesnumber[5]NoExactly five, each 0 to 1Per claim size, smallest first: the fraction of that size's cost you are confident your programme saves once it has taken hold. Absent means the engine's defaults. See Savings rates are yours.
opaque_correlation_idstringNoUUID, or 8-64 hex charactersYour own token for matching a response to your records. Echoed in provenance.correlation_id; the engine attaches no meaning to it.

Any field not in this table is rejected with 400 ValidationError.

Every money field is dollars a year. Do not send monthly figures, multi-year totals or per-employee amounts.

De-identification requirement​

All request bodies are scanned for identifiers on the way in. This body is entirely numbers, lists of numbers, digit strings and enumerations: there is no employer name, address, contact, date or identifier field, and none may be added. If you need to match a response to your own records, use opaque_correlation_id, which accepts a UUID or a hex string and nothing else.

A body containing an identifier returns 400 DeIdentificationViolation. See conventions.md § De-identification boundary.

Response​

Success — 200 OK​

The response to the body above.

{
"operation": "basis",
"claims": {
"count_annual": 25.5,
"source": "industry_rate",
"industry": {
"naics_code_used": "23822",
"industry_name": "Plumbing, heating, and air-conditioning contractors",
"year": 2024,
"total_recordable_rate": 3.0
},
"injuries_annual": 72.86
},
"cost": {
"claim_costs_annual": 410000.0,
"administration_annual": 52000.0,
"premium_annual": 118000.0,
"total_annual": 580000.0,
"source": "stated"
},
"indirect": {
"osha_ratio": 1.2735,
"indirect_annual": 522116.89
},
"movable_annual": 932116.89,
"claim_sizes": [
{
"size": 1,
"label": "Minor",
"share_of_claims": 0.755184,
"typical_medical_cost": 731.0,
"medical_from": 0.0,
"medical_to": 3000.0,
"wage_replacement": 0.05,
"legal": 0.01,
"typical_direct_cost": 774.86,
"osha_ratio": 4.5,
"share_of_claim_spend": 0.049391,
"claim_spend_annual": 20250.47,
"savings_rate": 0.7
},
{
"size": 2,
"label": "Moderate",
"share_of_claims": 0.098481,
"typical_medical_cost": 4584.0,
"medical_from": 3000.0,
"medical_to": 7000.0,
"wage_replacement": 0.4,
"legal": 0.05,
"typical_direct_cost": 6646.8,
"osha_ratio": 1.2,
"share_of_claim_spend": 0.055251,
"claim_spend_annual": 22652.92,
"savings_rate": 0.59
},
{
"size": 3,
"label": "Significant",
"share_of_claims": 0.122459,
"typical_medical_cost": 18946.0,
"medical_from": 7000.0,
"medical_to": 50000.0,
"wage_replacement": 0.7,
"legal": 0.12,
"typical_direct_cost": 34481.72,
"osha_ratio": 1.1,
"share_of_claim_spend": 0.356414,
"claim_spend_annual": 146129.78,
"savings_rate": 0.48
},
{
"size": 4,
"label": "Serious",
"share_of_claims": 0.022309,
"typical_medical_cost": 92556.0,
"medical_from": 50000.0,
"medical_to": 250000.0,
"wage_replacement": 0.9,
"legal": 0.25,
"typical_direct_cost": 198995.4,
"osha_ratio": 1.1,
"share_of_claim_spend": 0.374713,
"claim_spend_annual": 153632.19,
"savings_rate": 0.36
},
{
"size": 5,
"label": "Catastrophic",
"share_of_claims": 0.001557,
"typical_medical_cost": 531769.0,
"medical_from": 250000.0,
"medical_to": null,
"wage_replacement": 1.0,
"legal": 0.35,
"typical_direct_cost": 1249657.15,
"osha_ratio": 1.1,
"share_of_claim_spend": 0.164231,
"claim_spend_annual": 67334.65,
"savings_rate": 0.25
}
],
"savings": {
"rate": 0.4142,
"source": "caller"
},
"plausibility": [
{ "key": "claim_costs_annual", "per_employee_per_month": 40.1961, "range": [0.5, 60.0], "within": true },
{ "key": "administration_annual", "per_employee_per_month": 5.098, "range": [0.1, 15.0], "within": true },
{ "key": "premium_annual", "per_employee_per_month": 11.5686, "range": [0.05, 20.0], "within": true }
],
"readiness": {
"completeness": 0.9286,
"release": "directional_only",
"still_needed": [
{ "key": "claim_count_annual", "tier": 2, "blocks_pricing": false }
]
},
"confidence": {
"overall": 0.85,
"tier": "high",
"breakdown": {
"evidence": null,
"input_completeness": 0.9285714285714286,
"imputation_variance": null,
"model_certainty": null
}
},
"notes": [
"BLS publishes no rate for NAICS 238220; the nearest published code, 23822 (Plumbing, heating, and air-conditioning contractors), was used.",
"Claims estimated from the 2024 BLS total recordable rate for NAICS 23822, 3 per 100 full-time workers, with covered employees standing in for full-time equivalents and 1 claim per recordable. Send claim_count_annual to replace the estimate.",
"Confidence is capped at 0.85. A complete submission is still priced on a typical claim mix rather than this employer's own claims, and on savings rates whose uncertainty nothing here measures."
],
"provenance": {
"calibration_versions": {
"underwriting_claim_costing": "2026-10-06-caller-savings",
"underwriting_completeness": "2026-10-06-basis"
},
"naics_year": 2024,
"catalog_version": "0.37.0",
"content_hash": "cb9832171a0a46df91b01178e0523a8b6405dc9f8ed567cc039098218d2e8545",
"correlation_id": null
},
"request_id": "3f6b2a1e-8c4d-4e7a-9b1f-2d5c8e0a7b34",
"timestamp": "2026-10-06T04:00:00.000000+00:00"
}

Two labels to read every figure by​

Every number in the response is one of these, and the field name says which:

  • Annual versus per employee per month. *_annual fields are dollars a year for the whole employer; typical_*_cost, medical_from and medical_to are dollars per claim. per_employee_per_month is an annual figure divided by covered_employees and by 12; only plausibility uses it.
  • Rate versus dollars. share_of_*, wage_replacement, legal, osha_ratio, savings_rate and savings.rate are rates (a fraction or a multiple), never dollars. Multiply a savings rate by the dollars it applies to.

Rounding​

Dollar figures are rounded to cents; count_annual and injuries_annual to two places; indirect.osha_ratio, savings.rate and readiness.completeness to four; per_employee_per_month to four; share_of_claim_spend to six. Every figure is computed at full precision first and rounded only on output, so recomputing from rounded figures can differ in the last place.

Response fields​

FieldTypeAlways presentDescription
operationstringYesAlways basis.
claims.count_annualnumber | nullYesClaims opened a year. null when claims.source is none.
claims.sourcestringYesWhere the count came from: stated, industry_rate, default or none. See Claims a year.
claims.industryobject | nullYesThe published BLS rate found for naics_code, whichever source set the count. null when no naics_code was sent or no rate is published for it or any code above it.
claims.industry.naics_code_usedstringWhen industry is presentThe code whose rate was found. Shorter than the code sent when the full code has no published rate.
claims.industry.industry_namestringWhen industry is presentIndustry name for naics_code_used.
claims.industry.yearintegerWhen industry is presentSurvey year of the published rate.
claims.industry.total_recordable_ratenumberWhen industry is presentRecordable injuries per 100 full-time workers a year.
claims.injuries_annualnumber | nullYesInjuries a year, including those that never become a claim: count_annual / 0.35. null when count_annual is.
cost.claim_costs_annualnumber | nullYesClaim costs, dollars a year: stated, split from the total, or estimated from claims.
cost.administration_annualnumber | nullYesAdministration, dollars a year: stated or split from the total. Never estimated from claims.
cost.premium_annualnumber | nullYesPremium, dollars a year: stated or split from the total. Never estimated from claims.
cost.total_annualnumber | nullYesThe three parts summed, dollars a year; null when any part is unknown.
cost.sourcestringYesstated, split_from_total, estimated_from_claims or none. See Cost parts.
indirect.osha_rationumberYesIndirect dollars per direct dollar, blended over the claim-spend mix.
indirect.indirect_annualnumber | nullYesIndirect cost, dollars a year: claim_costs_annual x osha_ratio.
movable_annualnumber | nullYesDirect plus indirect claim cost, dollars a year: the cost a programme can act on. Excludes administration and premium.
claim_sizesobject[]YesExactly five entries, smallest first.
claim_sizes[].sizeintegerYes1 to 5.
claim_sizes[].labelstringYesMinor, Moderate, Significant, Serious or Catastrophic.
claim_sizes[].share_of_claimsnumberYesFraction of claims that fall in this size.
claim_sizes[].typical_medical_costnumberYesTypical medical cost of one claim in this size, dollars.
claim_sizes[].medical_fromnumberYesLower bound of the size's medical-cost range, dollars per claim.
claim_sizes[].medical_tonumber | nullYesUpper bound, dollars per claim. null on size 5, which is open-ended.
claim_sizes[].wage_replacementnumberYesWage replacement as a multiple of medical cost.
claim_sizes[].legalnumberYesLegal cost as a multiple of medical cost.
claim_sizes[].typical_direct_costnumberYesAll direct cost of one typical claim, dollars: medical x (1 + wage_replacement + legal).
claim_sizes[].osha_rationumberYesOSHA's indirect-cost ratio at typical_direct_cost.
claim_sizes[].share_of_claim_spendnumberYesFraction of claim dollars in this size. Differs from share_of_claims because large claims cost more.
claim_sizes[].claim_spend_annualnumber | nullYesThis size's claim costs, dollars a year: claim_costs_annual x share_of_claim_spend.
claim_sizes[].savings_ratenumberYesThe fraction of this size's cost the basis assumes the programme saves: your savings_rates entry, or the engine's default.
savings.ratenumberYesThe per-size savings_rate values weighted by share_of_claim_spend. A fraction, not dollars.
savings.sourcestringYescaller when you sent savings_rates, default when the engine's defaults were used. See Savings rates are yours.
plausibilityobject[]YesOne entry per cost part you sent. Empty when you sent none, or sent no covered_employees. Advisory only; never blocks.
plausibility[].keystringYesThe request field checked: claim_costs_annual, administration_annual or premium_annual.
plausibility[].per_employee_per_monthnumberYesThe figure you sent divided by covered_employees and by 12, dollars.
plausibility[].rangenumber[2]YesThe plausible per-employee-per-month range for that part, dollars.
plausibility[].withinbooleanYesfalse flags a likely unit error or an unusual employer.
readiness.completenessnumberYes0 to 1. Tier-weighted share of the request fields supplied. See Readiness.
readiness.releasestringYesreleased, directional_only, internal_only or suppressed.
readiness.still_neededobject[]YesMissing tier 1 and tier 2 fields, tier 1 first, then by key. Empty when none is missing.
readiness.still_needed[].keystringYesThe request field name. Stable.
readiness.still_needed[].tierintegerYes1 or 2. Tier 3 fields are never listed.
readiness.still_needed[].blocks_pricingbooleanYestrue when tier is 1, except for a missing cost part while total_cost_annual is sent: the total already prices it, so it is still listed (the real figure replaces the split) but does not block.
confidence.overallnumberYes0 to 1, capped. See Confidence is capped.
confidence.tierstringYeslow, medium or high.
confidence.breakdown.input_completenessnumberYesThe completeness term, at full precision. The only term that runs here.
confidence.breakdown.evidence / .imputation_variance / .model_certaintynullYesAlways null on this operation: there is no question bank, imputation or model, only arithmetic.
notesstring[]YesPlain-English statements of what was estimated, assumed, split or capped, and why. Read these.
provenance.calibration_versionsobjectYesThe calibration versions this answer was computed on.
provenance.naics_yearinteger | nullYesSurvey year of the industry rate found; null when none was found.
provenance.catalog_versionstringYesThe engine catalog version that served the call.
provenance.content_hashstringYesHash over the request, the calibration versions, the industry rate year and, when you sent no savings_rates, the default rates used. See Determinism.
provenance.correlation_idstring | nullYesYour opaque_correlation_id, echoed; null when you sent none.
request_idstringYesCorrelation ID. Echo back in support tickets.
timestampstring (ISO 8601)YesServer timestamp at response emission.

Status codes​

CodeMeaningWhen
200OKRequest succeeded. Including when the body was sparse or empty, and when a cost figure is implausible.
400ValidationErrorA supplied field breaks its own constraint: malformed JSON, a field not in the body table, covered_employees or states not a whole number above zero, a negative money figure or claim_count_annual, a naics_code that is not 2-6 digits, an ownership off the list, a savings_rates that is not exactly five numbers between 0 and 1, or an opaque_correlation_id that is not a UUID or 8-64 hex characters. field names the offender. Never returned for a missing field.
400DeIdentificationViolationRequest body contained an identifier. field names the offending path.
401UnauthenticatedMissing or invalid API key.
402PaymentRequiredOut of credits, or a per-key or account budget is exhausted. See credits.
404NotFoundPath does not exist or the method is wrong.
413PayloadTooLargeBody exceeds the 512 KB limit.
415UnsupportedMediaTypeContent-Type is not application/json.
429RateLimitedPer-consumer rate limit exceeded. See Retry-After.
500InternalErrorUnexpected server-side failure. Include request_id when reporting.
504UpstreamTimeoutThe analytical layer timed out.

Error envelope​

All non-2xx responses use a uniform envelope:

{
"timestamp": "2026-10-06T04:00:00.000Z",
"error": "ValidationError",
"message": "covered_employees: Input should be greater than 0",
"request_id": "3f6b2a1e-8c4d-4e7a-9b1f-2d5c8e0a7b34",
"field": "covered_employees"
}

See errors for the full list of error codes and extras.

The arithmetic​

Every figure is plain arithmetic over your input, the active calibration and, when you send naics_code, the published BLS rate. No model, no randomness.

Claims a year​

The first of these that applies:

  1. Stated. claim_count_annual, as sent. source is stated.
  2. Industry rate. covered_employees x the industry's total recordable rate / 100 x claims per recordable injury. Claims per recordable injury is currently 1: an injury recordable under OSHA needs treatment beyond first aid, which almost always opens at least a medical-only claim. source is industry_rate.
  3. Default. covered_employees x 4 / 100 (four claims per 100 employees a year). source is default.
  4. None. No claim count and no covered employees: count_annual and injuries_annual are null. source is none.

The industry rate. When you send naics_code, the engine looks up the latest published BLS total recordable rate for that code and ownership. When BLS publishes no rate for the full code, it drops the last digit and tries again (238220, 23822, 2382, 238, 23) and uses the first published rate; a note names the code it used. The manufacturing, retail and transportation sectors, which BLS publishes as 31-33, 44-45 and 48-49, are matched at two digits. The rate found is returned in claims.industry even when claim_count_annual set the count, so you can see the industry rate beside your own figure.

The BLS rate is per 100 full-time workers. Covered employees stand in for full-time equivalents, and a note says so; for a heavily part-time workforce, send claim_count_annual instead.

injuries_annual is count_annual / 0.35: roughly one injury in three becomes a claim, the rest resolve without one.

Cost parts​

A part you send is never overwritten. The first of these that applies:

  1. All three parts sent (claim_costs_annual, administration_annual, premium_annual): used as sent, total_annual is their sum, and total_cost_annual is ignored. source is stated.
  2. Some parts missing, and total_cost_annual sent: total_cost_annual minus the parts you sent is split over only the missing parts, in proportion to their calibrated weights (claim costs 0.70, administration 0.12, premium 0.18). If the parts you sent already exceed the total, the missing parts are 0 and a note says so. source is split_from_total.
  3. No total, claim_costs_annual sent, another part missing: the parts you sent are returned, the missing ones are null, and total_annual is null. source is stated.
  4. No claim costs and no total, but the claim count is known (stated or estimated): claim_costs_annual = count_annual x the typical direct cost of an average claim (each size's share_of_claims x its typical_direct_cost, summed: about $11,847 per claim). Administration and premium are returned only if you sent them, otherwise null; they are never estimated from claims. source is estimated_from_claims.
  5. Otherwise claim costs are null, and so is every figure built on them. source is none.

Examples of case 2, with the weights renormalised over what is missing:

  • Only total_cost_annual = 1,000,000: claim costs 700,000, administration 120,000, premium 180,000.
  • claim_costs_annual = 600,000 and total_cost_annual = 900,000: the remaining 300,000 splits 0.12 : 0.18, so administration 120,000 and premium 180,000.

Per claim size​

  • typical_direct_cost = typical_medical_cost x (1 + wage_replacement + legal).

  • osha_ratio comes from OSHA's indirect-cost table at that direct cost, rounded to the whole dollar:

    Direct cost of a claimIndirect dollars per direct dollar
    up to $2,9994.5
    $3,000 - $4,9991.6
    $5,000 - $9,9991.2
    $10,000 and over1.1
  • share_of_claim_spend = share_of_claims x typical_direct_cost, divided by the same product summed over all five sizes.

  • claim_spend_annual = claim_costs_annual x share_of_claim_spend.

Indirect and movable cost​

  • indirect.osha_ratio = each size's osha_ratio weighted by its share_of_claim_spend.
  • indirect_annual = claim_costs_annual x osha_ratio.
  • movable_annual = claim_costs_annual x (1 + osha_ratio).

Administration and premium are never movable: a care-management programme changes injuries and claims, not what an insurer or administrator charges.

Savings​

Each size's savings_rate is the matching entry of your savings_rates, or the engine's default for that size when you sent none. savings.rate is each size's savings_rate weighted by its share_of_claim_spend. Because large claims carry most of the dollars, the blended rate follows the rates you set for the large sizes far more than the rate you set for minor claims.

Worked check against the example​

Claims: 850 x 3.0 / 100 x 1 = 25.5; injuries 25.5 / 0.35 = 72.86. With claim_costs_annual = 410,000:

Sizetypical_direct_costshare_of_claims x directshare_of_claim_spendosha_ratioclaim_spend_annual
1731 x 1.06 = 774.86585.160.0493914.520,250.47
24,584 x 1.45 = 6,646.80654.580.0552511.222,652.92
318,946 x 1.82 = 34,481.724,222.600.3564141.1146,129.78
492,556 x 2.15 = 198,995.404,439.390.3747131.1153,632.19
5531,769 x 2.35 = 1,249,657.151,945.720.1642311.167,334.65
11,847.4511.2735 blended410,000.01 (cents rounding)
  • indirect_annual = 410,000 x 1.27345583 = 522,116.89.
  • movable_annual = 410,000 x 2.27345583 = 932,116.89.
  • savings.rate = 0.049391 x 0.70 + 0.055251 x 0.59 + 0.356414 x 0.48 + 0.374713 x 0.36 + 0.164231 x 0.25 = 0.4142.
  • plausibility for claim costs: 410,000 / 850 / 12 = 40.1961, inside 0.5 to 60.
  • readiness.completeness: 13 of 14 weight points (see Readiness) = 0.9286.

What you do with it​

Pricing is your arithmetic over this response. A typical shape: apply savings.rate to movable_annual (or to claim_costs_annual if you price on direct cost only), then apply your own ramp-up, costs, margin and contract terms. None of those enter the engine.

Savings rates are yours​

How much of each claim size's cost a programme saves is a judgement about your own programme, so the engine takes it as input rather than supplying it. Send savings_rates: five fractions, smallest claim size first, each the share of that size's cost you are confident your programme saves once it has taken hold. Every savings figure in the response is priced on them, and savings.source is caller.

When you send no savings_rates, the engine prices on its defaults and savings.source is default:

SizeLabelDefault savings_rate
1Minor0.70
2Moderate0.59
3Significant0.48
4Serious0.36
5Catastrophic0.25

The defaults are a starting point, not evidence: they are not sourced to published outcome data, and a note says so on every response that uses them. The claim-size shares and typical medical costs are observed; the wage-replacement and legal multiples are modelled. Send your own rates for anything you price on.

Readiness​

Each request field has a tier:

TierWeightFieldsMeaning
13covered_employees, claim_costs_annual, administration_annual, premium_annualThe spend every figure is measured on and the headcount every per-employee figure divides by. blocks_pricing: true, except a cost part that a sent total_cost_annual already prices.
21claim_count_annual, naics_codeSets the claim count. blocks_pricing: false.
30total_cost_annual, ownership, statesNot counted toward completeness and never listed in still_needed. total_cost_annual still fills missing cost parts when sent.

readiness.completeness is the weight of the fields present divided by the total weight, 14. total_cost_annual counts as present whenever all three cost parts are. readiness.release bands it:

releaseCompletenessMeaning
released>= 0.95Fit to price on.
directional_only>= 0.85Fit to show as a direction, not a figure to commit to.
internal_only>= 0.70Fit for your own analysis only.
suppressedbelow 0.70Too little to rely on.

readiness.still_needed[].key values are the request field names in this document. They are stable: map your own questions to them by key. A rename would be a breaking change and would be announced as one.

Partial information never fails​

No field is required. A body with one number in it returns 200, and so does {}. Missing input lowers readiness.completeness and confidence.overall, lists the missing fields in readiness.still_needed, and leaves the figures that depend on it null. claims.source and cost.source say exactly how each figure was obtained, and notes say what was estimated or split.

A cost figure far outside its plausible range is reported in plausibility with within: false and still returns 200. The only 400s are a malformed body, an unknown field, and a supplied field that breaks its own constraint.

Confidence is capped​

confidence.overall is the completeness term, capped at 0.85. A complete submission is still priced on a typical claim mix rather than the employer's own claims, and on savings rates whose uncertainty nothing here measures; the cap is the honest statement of that, and a note reports it whenever it applies. In the example, completeness 0.9286 is capped to 0.85. Sending savings_rates does not change completeness or confidence.

Determinism​

The same body, against the same calibration versions and the same BLS survey year, returns the same response apart from request_id and timestamp. There is no randomness and no dependence on the time of the call. provenance.content_hash is computed over the request (your savings_rates included), the calibration versions, the BLS survey year and, when you sent no savings_rates, the default rates used: store it with any price you build, and an unchanged hash on a later call means the basis under that price has not moved.

Examples​

curl — success​

curl -sS -X POST "https://engine.tripod-health.com/api/underwriting/basis" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"covered_employees": 850,
"naics_code": "238220",
"states": 2,
"claim_costs_annual": 410000,
"administration_annual": 52000,
"premium_annual": 118000,
"savings_rates": [0.7, 0.59, 0.48, 0.36, 0.25]
}'

Returns the response shown under Success.

curl — a sparse body still returns 200​

curl -sS -X POST "https://engine.tripod-health.com/api/underwriting/basis" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"covered_employees": 2400}'

Returns 200 with:

  • claims.source default, count_annual 96 (2,400 x 4 / 100);
  • cost.source estimated_from_claims, claim_costs_annual 1137354.9 (96 x 11,847.45), administration, premium and total_annual null;
  • plausibility empty, since no cost part was sent;
  • savings.source default, since no savings_rates were sent, with a note saying so;
  • readiness.completeness 0.2143 (3 of 14), release suppressed, and still_needed listing administration_annual, claim_costs_annual, premium_annual (tier 1) then claim_count_annual, naics_code (tier 2).

curl — validation error​

curl -sS -X POST "https://engine.tripod-health.com/api/underwriting/basis" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"covered_employees": 0}' \
-w "\nHTTP %{http_code}\n"

Returns 400 ValidationError with field set to covered_employees, as shown under Error envelope.

Python — requests​

import os, requests

resp = requests.post(
"https://engine.tripod-health.com/api/underwriting/basis",
headers={"apikey": os.environ["ENGINE_API_KEY"]},
json={
"covered_employees": 850,
"naics_code": "238220",
"claim_costs_annual": 410000,
"administration_annual": 52000,
"premium_annual": 118000,
"savings_rates": [0.7, 0.59, 0.48, 0.36, 0.25],
},
timeout=30,
)
resp.raise_for_status()
basis = resp.json()

rate = basis["savings"]["rate"]
movable = basis["movable_annual"]
if movable is not None:
print(f"Planned savings: ${movable * rate:,.0f} a year")
for item in basis["readiness"]["still_needed"]:
print(f" still needed: {item['key']} (tier {item['tier']})")

Node — fetch​

const resp = await fetch(
"https://engine.tripod-health.com/api/underwriting/basis",
{
method: "POST",
headers: {
apikey: process.env.ENGINE_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
covered_employees: 850,
naics_code: "238220",
total_cost_annual: 580000,
}),
},
);
if (!resp.ok) throw new Error(`Engine ${resp.status}: ${await resp.text()}`);
const basis = await resp.json();
console.log(basis.cost.source); // "split_from_total"

Intended use​

  • Scoping a workers' compensation programme for an employer from whatever they have told you, before any commercial decision.
  • The input to your own pricing: send your savings rates, then apply your costs, margins, ramp-up and contract terms to movable_annual, claim_sizes and savings.
  • Driving an intake questionnaire: call with what you have, ask the employer for the keys in readiness.still_needed, call again.
  • Catching unit errors before they reach a price: a plausibility entry with within: false usually means a monthly or multi-year figure was sent as annual.
  • Detecting a stale price: compare a stored provenance.content_hash to a fresh call's.

Mistaken use​

  • Do not read a savings rate as dollars. It is a fraction; multiply it by movable_annual or claim_costs_annual.
  • Do not price on the default savings rates. They are unsourced starting points; send your own savings_rates instead.
  • Do not send savings rates as percentages. 70 fails validation; send 0.7 instead.
  • Do not apply savings to cost.total_annual. Administration and premium are not movable; use movable_annual or claim_costs_annual instead.
  • Do not send monthly, per-employee or multi-year figures in the *_annual fields. Convert to dollars a year first; plausibility judges only the parts you send, and only against a broad range.
  • Do not read an empty plausibility list as a pass. Split and estimated parts are not judged; check cost.source instead.
  • Do not put an employer name or any identifier in the body. Use opaque_correlation_id instead.
  • Do not use this operation to compare an employer's injury rate with its industry. Use POST /api/lookup/naics/calculate instead.
  • Do not use this operation to cost a single claim. Use POST /api/claimcost/classify instead.
  • Do not retry a sparse 200 as if it failed. Read readiness.still_needed, collect the missing fields, and call again.

Debugging​

  1. Capture the request_id from the response body (or, if the response has no body from the engine, the X-Request-ID response header). Every support ticket starts here.
  2. 400 ValidationError naming an unexpected field — the body accepts exactly the fields listed under Body fields; remove any other key.
  3. 400 DeIdentificationViolation — field names the offending path. opaque_correlation_id accepts only a UUID or 8-64 hex characters.
  4. claims.source is none — send claim_count_annual or covered_employees.
  5. cost.source is none — send cost figures, or a claim count (or covered_employees) so claim costs can be estimated.
  6. claims.industry is null though you sent naics_code — no rate is published for the code or any code above it under that ownership. A note says so. Check the code with GET /api/lookup/naics/get, or send claim_count_annual.
  7. plausibility[].within is false — check the units of that field before pricing on it.
  8. 402 PaymentRequired — fund the balance or raise the budget; see credits.
  9. 5xx with no clear cause — file a ticket with the request_id, the UTC timestamp from the response, and a minimal reproducer body.

Credits​

Each successful call costs 450 credits, fixed regardless of how complete the body is. Failed calls are not billed. See credits for metering, the X-Credits-* headers and 402.

Rate limits​

Per-consumer limits are enforced at the gateway. See the rate limits reference.

Change history​

DateChange
2026-10-06Initial publication.
2026-10-06Savings rates are now sent by the caller as savings_rates, with engine defaults when absent. claim_sizes[].savings is replaced by claim_sizes[].savings_rate, and the blended savings range and provisional flag by savings.rate and savings.source.
2026-10-14The engine assigns every request ID; an X-Request-ID request header is not used. The X-Request-ID response header is the gateway's trace ID for the request and does not match request_id; quote request_id to support.