Header matching
Which column headers in a broker's file map to each Plansight field. Matching is case-insensitive and ignores _ - . and extra spaces. Add more spellings on the “Add a rule” tab.
Value conversions
How messy cell values become canonical values. Every rule carries provenance — the carrier/file that taught it.
Format conversions
Dates, ZIPs, money and numbers aren't synonym lists — they're interpreted by format. However a broker types one of these, Roster reads it to a single canonical value (and flags what it can't read, rather than guessing). These apply to every schema field of that kind, shown below.
Coverage tier by relationship
A coverage tier is only valid for certain relationships. The subscriber carries the family election; a spouse can only be a covered or waived spouse; a child or ward a covered or waived dependent. A spouse can never be “employee only”. The census grid limits each row's tier dropdown to its allowed set and flags an off-list value from a file.
Coverage tier vs. who is actually covered
The employee's tier is a claim about the family; the dependent rows are the evidence. They can't contradict each other, so Roster derives the tier the covered composition implies and compares it to the one on the file. Reading the table: a dependent with a blank tier counts as covered (if a broker listed someone, assume they're on the plan, rather than silently dropping them from the quote); a waived dependent (WS/WD) never counts toward the employee's tier, so a waived spouse doesn't make an employee “employee + spouse”. One flag per family, on the employee's tier cell. A dependent removed for aging out never triggers a flag: that's Roster's edit, not the broker's mistake, and that row already carries its own reason.
When the tier and the rows disagree, which one is wrong decides what happens. If every covered dependent is already accounted for by the tier — a spouse-only family typed EF, an EC2 carrying one child — the tier is just reaching for someone who isn't in the file, so Roster corrects it to what the rows show and lists it under “What we changed automatically.” Nobody is enrolled by that: the quote's dependents come from the rows, never from the tier. If instead a covered dependent is not accounted for by the tier — a spouse row under an EE — Roster flags it and changes nothing, because the dependent row is just as likely to be the mistake, and quietly raising the tier would enroll a real person and add premium. A declared tier with no dependent rows at all is always flagged, never corrected: an empty family is as likely to mean the dependent rows never made it into the file.
Household rule — same_household → Ideon
Ideon requires a same-household flag on every dependent. We set it per relationship — not from the broker's yes/no — so a spouse/partner is never accidentally dropped from the quote.
Add a rule
Compose a new header spelling or value conversion here — no code. It builds the exact rule and lets you download the updated rules file to hand to a developer (or drop into the repo). Live in-app editing is the next step once the rules store lands.
Ideon business rules — what they are & how Roster uses them
The small-group enrollment rules Ideon applies at quote time, extracted from the carrier business_rule_sets feed (2026 Q3, all 51 states). This is the census-side view of the master catalog aca-data-project/BUSINESS-RULES-AND-SELF-QUOTING.md. Enforce = Roster acts at import · Flag = warns, never blocks · Info = shown for reference · Rate = matters only when premiums are built (passed through untouched).
Per-state envelope
The extremes across every carrier/plan in a state — what Roster can assert before a specific carrier is chosen. Rules resolve per plan at quote time; a dependent over the age cap can't enroll in any plan, which is why Roster ages them off at import.
Statutory disability & paid family leave
States where employers have statutory obligations — mandated disability insurance and/or paid family & medical leave. Census members living in these states are flagged in the importer's residency panel (and carried in the payload), because obligations follow where members live — and Plansight carries the matching plan types (nyDisability, spfl).
Carrier composite factors
For small-group community-rated quoting: which carriers in a state can bill on composite (one blended rate per tier) vs list-billed (per member) only. A carrier that publishes composite factors can do composite; if it has none, it's list-billed only. Factors are the 4-tier multipliers relative to Employee only = 1.0. Source: the Ideon issuer feed (latest quarter per state).
Connect Roster to V1
Roster is a modal overlay, like Flatfile/Scout: one script tag, one call, one callback. Roster hosts no data — the widget parses in the browser and hands the clean payload back.
1. Mint a short-lived JWT (server-side)
use Firebase\JWT\JWT;
JWT::encode([
'iss' => 'plansight-v1', 'aud' => 'plansight-roster',
'exp' => time() + 300, // 5 min
'context' => ['rfpId' => $rfpId, 'groupId' => $groupId],
], env('ROSTER_SIGNING_KEY'), 'HS256');
HS256, shared secret, ~5-min TTL, audience plansight-roster, issuer plansight-v1. Never ship the key to the browser.
2. Drop in the launcher
<script src="https://plansight-roster-stage.vercel.app/embed.js"></script>
<script>
PlansightRoster.open({
token: "{{ mintRosterJwt() }}",
schema: "aca-medical-census", // or "disability-census" / "carrier-rep-import"
state: "{{ $group->state }}", // census: employer state (drives age-out rules)
effectiveDate: "{{ $rfp->effective_date }}", // census: YYYY-MM-DD
planOptions: {{ $group->plan_options }}, // census: available medical plan options
offices: {{ $brokerage->office_names }}, // carrier-rep: the brokerage's offices
onSuccess: (payload) => { // census: payload.employees (EN shape)
// carrier-rep: payload.records (RfpDistributionListDefaults shape)
},
onCancel: () => {},
});
</script>
3. Endpoints
4. The payload & Ideon mapping
Roster returns the Employee Navigator Quote Census v1 shape — the same shape V1's EN sync already consumes. V1 derives the Ideon /quotes request from it.
| Ideon field | From our payload |
|---|---|
| external_id | plansightMeta.eeId |
| date_of_birth | demographics.birthDate (YYYY-MM-DD) |
| gender | demographics.sex → M / F (Unspecified must be resolved) |
| zip_code | demographics.addresses.homeAddress.postalCode |
| fips_code / location_id | V1 computes from ZIP/state |
| cobra / retiree | plansightMeta.cobra / .retiree |
| last_used_tobacco | demographics.tobaccoUseInformation.lastUsedDate |
| dependents[].relationship | Spouse→spouse, Child→child, LegalDependent→ward |
| dependents[].same_household | Per relationship (see Household rule): Spouse/partner → true, all others → false |
5. Error events (via the callback)
token_expired — mint a fresh JWT · schema_unknown — bad schema id · network_failed — Roster API unreachable.
Preview the embed theme
Roster restyles itself to match the app it's embedded in — it's all CSS design tokens (color, corners, font), so no host CSS is touched. Pick a host style or nudge the knobs; the live preview below is the real importer in that theme. The host applies whatever you land on via PlansightRoster.open({ theme: … }) — copy the snippet.
The preview below is live and clickable — or hit Open full importer to demo to launch it full-screen in its own tab (upload a census, click through, print) with these exact theme settings. That URL is also shareable for a demo: