Insights / data-outUpdated October 20, 2026

Building an employer benchmarking view

The Insights API scores plans, not employers. An employer view is something you compose from per-plan scores, the same way the Bnchmrk app builds it internally. Here is the recipe.

Step 1

List the employer's plans

You already loaded the employer and its plans through the Management API. Start from that list: for each plan you want in the view, you need its plan_id and its benefit line (medical, dental, vision, basiclife, voluntarylife, std, ltd, stoploss). You will score each plan one at a time.

Scope the set

Include the plans you want to present together, typically all of an employer’s active plans. Scoring is per plan, so the set is entirely up to you, there is no “all plans” shortcut on the API.
Step 2

Choose one cohort (query)

Define the peer group once as a query filter set and reuse it for every plan, so each plan is measured against the same employers and the scores are comparable. The query is a multi-dimension filter: state[], industry[], size[], and other[] (all optional, all multi-select).

const query = {
  state: ["CA", "OR", "WA"],
  industry: ["services_legal_services"],
};

Cohort rules

  • The cohort must resolve to between 20 and 2000 peer employers. Too few and a plan cannot be scored; too broad and you are asked to narrow it (a 422 cohort_out_of_range at request time).
  • For stoploss, only the size values apply, state and industry are ignored. With no size, stop loss comes back insufficient (“needs a size band”) rather than falling back to the whole market.
  • Resolve the dimension keys from GET /insights/dimensions; industry and other are served live.
Step 3

Score each plan

For each plan, create a score with POST /insights/scores and poll it. Scoring is an on-demand, live benchmark, so it is always async: the create call returns 202 with a score_id, and you poll GET /insights/scores/{score_id} until the status is no longer processing.

const results = [];
for (const plan of employer.plans) {
  // plan_id must be one of YOUR plans (ownership-checked; 404 otherwise)
  const { score_id } = await post("/insights/scores", {
    benefit: plan.benefit,
    plan_id: plan.id,
    query,                       // the shared cohort from step 2
  });

  let result;
  do {
    result = await get(`/insights/scores/${score_id}`);
  } while (result.status === "processing");

  results.push(result);
}

Run them in parallel

The plans are independent, so you can fire all the POSTs and poll the score_ids concurrently rather than strictly one after another. The result is transient (short TTL, retrieved once), so read each score when it completes and keep it.
Step 4

Handle the not-scorable cases

Not every plan ends with a score. Surface these outcomes to the user rather than dropping the plan, each one tells them exactly what to fix.

1

Cohort out of range (at request time)

POST /insights/scores returns 422 cohort_out_of_range when the query resolved to fewer than 20 or more than 2000 peer employers. Adjust the query, broaden a too-thin cohort or narrow a too-broad one, and retry.

2

Plan missing required fields

The score completes with scorable: false and a missing_fields: [{key, label}] list when the plan lacks data needed to score it. Prompt the user to complete those fields, then re-score.

3

Cohort too thin to rank

The score comes back with status insufficient when the cohort has data but too few comparable plans for this plan’s plan-type to produce a stable ranking. This is distinct from the 20/2000 employer gate, widen the cohort so more comparable plans qualify.

Step 5

Present the results together

Stack the scored plans into one view, a card per plan, exactly like the app. Each result gives you the headline and the drivers:

  • score (0–100, exact) and band (Limited / Balanced / Robust), the headline for the plan.
  • positions, keyed by field: distribution fields carry plan, p25/p50/p75, and a rank; copay/CPT fields are a single stitched plan + p50 string, and the weighted ones also carry a rank.
  • weight_percent on each scored field, its share of the score. Use it to show the drivers (“this field drives N% of the score”). Booster fields instead carry booster_points, flat points added outside the weighted average.

Rank is masked at the extremes

A per-field rank is a string, masked to ">90" or "<10" at the ends where the exact percentile is noisy. Render the string as-is. The headline score is never masked.

Want the surrounding distribution too? The market face of the same model, GET /insights/{benefit}/{dimension}/{value} (no plan_id), returns the pre-computed weekly cut you can show alongside a plan.

Why compose it

More flexible than one employer endpoint

Composing the view from plan scores is not a workaround, it is the more capable design. A few reasons it beats a single employer call:

  • The plan is the unit of decision. You see exactly which plans are strong and which are weak, and why, instead of one blended number that hides the detail.
  • You control the peer group. Pick any cohort and apply it consistently across the plans, or re-run the same plans against a different cohort for a what-if, all from the same call.
  • You own the presentation. Order, group, weight, and emphasize however your product needs, nothing is locked into a fixed, server-rendered rollup.
  • One primitive, many views. The same scoring call powers an employer view, a book-of-business comparison, or a side-by-side of plan options, with no new endpoints to learn.
  • Full transparency. Every score ships with its drivers (weight_percent, per-field ranks), so you can explain the number to a client rather than hand them a black box.