Insights / data-outUpdated October 20, 2026

Reading the service & Rx fields

Almost every benchmark field uses the same key in and out. The medical service copays and Rx tiers are the exception: several input fields collapse into one derived key. Read them wrong and your copay UI shows nothing.

Context

The one place in and out differ

The Insights contract is “same vocabulary in and out” for almost everything: deductibles, out-of-pocket maximums, in/out/Rx coinsurance, rates (t1_ee…t4_gross), HSA funding (t1_ercdhp…), and all life, disability, stop-loss, and vision metrics carry the same key when you POST a plan and when you read a benchmark back.

The medical service copays (office visits, hospital) and Rx tiers are the exception. On the way in they are componentized, two or three fields per service. The benchmark collapses each service into a single derived key. So the field you write is not the field you read.

The trap

A partner who writes pcp_copay and then looks for pcp_copay in the benchmark response finds nothing. The response key is pcp. This guide is the mapping.

These fields are also non-HDHP only. HDHP plans carry no service/Rx design (everything runs against the deductible), so pcp, rx1, and friends appear only for HMO and PPO/POS/EPO plan types.

Step 1

The in → out mapping

Each service is a copay plus a deductible-applies flag on data-in; each Rx tier adds a coinsurance field. On data-out they are one key.

Medical services

Data-in (you POST)Data-out (you read)
pcp_copay, pcp_ded_applypcpPrimary care physician
sp_copay, sp_ded_applyspSpecialist physician
er_copay, er_ded_applyerEmergency room
uc_copay, uc_ded_applyucUrgent care
ip_copay, ip_ded_applyipInpatient hospital
op_copay, op_ded_applyopOutpatient hospital
lx_copay, lx_ded_applylxLab & X-ray

Prescriptions (retail + mail, per tier)

Data-in (you POST)Data-out (you read)
rx1_copay, rx1_coin, rx1_ded_applyrx1Rx tier 1
rx2_copay, rx2_coin, rx2_ded_applyrx2Rx tier 2
rx3_copay, rx3_coin, rx3_ded_applyrx3Rx tier 3
rx4_copay, rx4_coin, rx4_ded_applyrx4Rx tier 4
rx1_mail_copay, rx1_mail_coin, rx1_mail_ded_applyrx1_mailRx tier 1 (mail)
rx2_mail_copay, rx2_mail_coin, rx2_mail_ded_applyrx2_mailRx tier 2 (mail)
rx3_mail_copay, rx3_mail_coin, rx3_mail_ded_applyrx3_mailRx tier 3 (mail)
rx4_mail_copay, rx4_mail_coin, rx4_mail_ded_applyrx4_mailRx tier 4 (mail)

The _ded_apply input

On data-in, {service}_ded_apply says how the deductible and coinsurance apply: FALSE (neither), FALSE_COIN (coinsurance only), TRUE (deductible only), TRUE_COIN (both). The benchmark folds the copay, coinsurance, and that flag together into the single derived key, you don’t reassemble it yourself.
Step 2

The stitched p50 string

The derived key’s value is a single display string, stitched by the benchmark from the cost-share descriptor plus the copay amount. There is no _cs/_copay split and no p25/p75 spread, just the market median.

cost_share descriptorcopaystitched value
No Ded.$30"$30 Copay"
After Ded.$40"After Ded., $40 Copay"
$500 Ded.0"$500 Ded."

So the market cut gives you { "p50": "$30 Copay" }; a scored plan adds the plan’s own stitched value: { "plan": "$40 Copay", "p50": "$25 Copay" }.

It lives in the one metrics object

There is no separate plan_design object anymore, copay fields sit inside the same metrics (market) / positions (scored) object as the numeric fields. Use the catalog kind (plan_design) to tell a stitched copay string from a {p25,p50,p75} distribution field.
Step 3

Which fields carry a rank

In a scored plan, only the weighted service/Rx fields contribute to the score, and only those carry a rank (and a weight_percent). The rest are returned for display but are weight-0, so they have no rank. Don’t build a percentile bar for a field that has no rank.

Ranked (weighted)

pcp, sp, er, ip, op, rx1, rx2, rx3

Rank-less (weight-0)

uc, lx, rx4, rx1_mail, rx2_mail, rx3_mail, rx4_mail

The metric catalog flags the weighted ones, each ranked CPT field carries ranked: true at /insights/metrics/medical, so you can drive this off the catalog rather than hard-coding the list.

Step 4

Reading both faces

Same key, two shapes. Here is pcp (a ranked service) in each face, next to the data-in fields it came from.

What you POST (data-in)

{
  "pcp_copay": 30,
  "pcp_ded_apply": "FALSE"        // copay applies, no deductible/coin
}

Market cut (plan_id absent): one stitched p50 string, no rank

"pcp": { "p50": "$30 Copay" }

Scored plan (plan_id present): adds the plan value, rank, weight

"pcp": {
  "plan": "$40 Copay",     // this plan, stitched
  "p50":  "$25 Copay",     // market median, stitched
  "rank": "46",            // string, masked to ">90"/"<10" at the extremes
  "weight_percent": 5      // this field's share of the plan score
}

rank is a string

Per-field rank is a string, masked to ">90" / "<10" at the ends where the exact percentile is noisy. Parse it as text, not a number. A weight-0 field (e.g. uc, lx, mail-order Rx) has no rank or weight_percent at all.
Step 5

Gotchas checklist

1

Read the derived key, not the input key

Look for pcp / rx1 in the response, never pcp_copay / rx1_coin. Those exist only on data-in.

2

Expect them only on non-HDHP plan types

HMO and PPO/POS/EPO carry service/Rx design; HDHP does not (it adds HSA funding instead). Absent CPT keys on an HDHP plan are correct, not missing data.

3

It's one stitched string, in the metrics object

Read the single p50 string (market) or plan + p50 strings (scored), there is no _cs/_copay split. These fields sit in the same metrics / positions object as the numeric ones; there is no separate plan_design.

4

Only build percentile UI for ranked fields

pcp, sp, er, ip, op, rx1, rx2, rx3 carry a rank + weight_percent when scored. The weight-0 fields are display-only; show the value, skip the bar.