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
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.
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_apply | pcpPrimary care physician |
| sp_copay, sp_ded_apply | spSpecialist physician |
| er_copay, er_ded_apply | erEmergency room |
| uc_copay, uc_ded_apply | ucUrgent care |
| ip_copay, ip_ded_apply | ipInpatient hospital |
| op_copay, op_ded_apply | opOutpatient hospital |
| lx_copay, lx_ded_apply | lxLab & X-ray |
Prescriptions (retail + mail, per tier)
| Data-in (you POST) | Data-out (you read) |
|---|---|
| rx1_copay, rx1_coin, rx1_ded_apply | rx1Rx tier 1 |
| rx2_copay, rx2_coin, rx2_ded_apply | rx2Rx tier 2 |
| rx3_copay, rx3_coin, rx3_ded_apply | rx3Rx tier 3 |
| rx4_copay, rx4_coin, rx4_ded_apply | rx4Rx tier 4 |
| rx1_mail_copay, rx1_mail_coin, rx1_mail_ded_apply | rx1_mailRx tier 1 (mail) |
| rx2_mail_copay, rx2_mail_coin, rx2_mail_ded_apply | rx2_mailRx tier 2 (mail) |
| rx3_mail_copay, rx3_mail_coin, rx3_mail_ded_apply | rx3_mailRx tier 3 (mail) |
| rx4_mail_copay, rx4_mail_coin, rx4_mail_ded_apply | rx4_mailRx tier 4 (mail) |
The _ded_apply input
{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.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 descriptor | copay | stitched 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
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.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.
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
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.Gotchas checklist
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.
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.
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.
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.