{
  "name": "Cardiology Practice Tracker",
  "purpose": "Independent cardiology practices in all 50 states and the District of Columbia: address, roster, echocardiogram volume, website, email, EHR and ownership. California, Texas and Florida carry finished web research; the rest carry structural data (roster, echo, tafamidis, Medicare group). Built from the CMS NPPES registry, each practice's own website, and the CMS Medicare claims summary. Read this document first, then read before you write.",
  "base_url": "same origin as this document; use relative /api/... paths",
  "auth": {
    "reads": "Gated on this deployment. Sign in at POST /api/login, or send x-tracker-token.",
    "writes": "Every POST, PATCH and DELETE needs a credential. An agent sends two headers on every request: 'x-tracker-token: <TRACKER_WRITE_TOKEN>' and 'x-tracker-editor: <who you act for> via Claude'. The token is a deployment secret; it is not in the repo, never goes in a URL, and must never be committed. A person signs in instead and gets an HttpOnly 'tracker_session' cookie.",
    "audit": "Every create, update and delete appends one audit_log row per changed field, with the actor, via (ui|claude|api) and the old and new value. Read it at GET /api/audit."
  },
  "endpoints": [
    {
      "method": "GET",
      "path": "/api/wainua",
      "desc": "CMS Wainua claims and researched affiliations. ?refresh=1 bypasses cache; ?format=csv downloads system totals. Dated snapshot returned when CMS unavailable."
    },
    {
      "method": "GET",
      "path": "/api/bootstrap",
      "desc": "Everything the single-page UI needs: every practice with its roster and CPT split, the excluded list, meta with the latest repository/API update datetime, and the national tafamidis prescriber list.",
      "returns": "{ practices, excluded, meta, tafamidis_prescribers }"
    },
    {
      "method": "GET",
      "path": "/api/practices",
      "desc": "List practices. Filters: state, city, zip, ownership, mso, min_cardiologists, min_echo, min_tafamidis, q (free text over name, city, ZIP, website, email, physician name and NPI), has=website|email|echo|echo_lab|tafamidis|split_roster, cms_group (substring of the Medicare billing group). Sort: sort=cardiologists|echo|tafamidis|name|city|state|updated & dir=asc|desc. Paging: limit (max 1000), offset. Add full=1 to include rosters.",
      "returns": "{ total, limit, offset, practices }"
    },
    {
      "method": "POST",
      "path": "/api/practices",
      "desc": "Create a practice. id and practice_name are required; any other writable field may be set. Unknown keys are ignored.",
      "returns": "{ practice } (201)"
    },
    {
      "method": "GET",
      "path": "/api/practices/:id",
      "desc": "One practice with its roster, CPT split and custom fields. :id is the natural key and must be URL-encoded; the numeric pid also works.",
      "returns": "{ practice }"
    },
    {
      "method": "PATCH",
      "path": "/api/practices/:id",
      "desc": "Update any writable field. Body is { field: value, ... }: scalar columns, JSON columns (send the whole array/object), the roster (physicians: [...]) and the CPT split (echo_by_code: [...]) which each replace the whole collection, plus an optional { fields: { any_key: value } } for attributes the schema does not have. Set a custom field to null to delete it. Each change is audited; no-ops are skipped.",
      "returns": "{ practice, changed, ignored }"
    },
    {
      "method": "DELETE",
      "path": "/api/practices/:id",
      "desc": "Delete a practice and its roster, CPT rows and custom fields.",
      "returns": "{ deleted }"
    },
    {
      "method": "GET",
      "path": "/api/physicians",
      "desc": "Roster across every practice. Filters: q (name or NPI), state, practice_id, subspecialty.",
      "returns": "{ total, physicians }"
    },
    {
      "method": "GET",
      "path": "/api/tafamidis-prescribers",
      "desc": "National prescribers. Optional npis query is a comma-separated list of up to 100 NPIs.",
      "returns": "{ prescribers }"
    },
    {
      "method": "GET",
      "path": "/api/tafamidis-prescribers/reconcile-links",
      "desc": "Preview stale national tracker links using exact current roster NPIs. Ambiguous multi-practice NPIs are skipped. No ownership or prescribing inference.",
      "returns": "{ updates: [{ npi, before, after }] }"
    },
    {
      "method": "POST",
      "path": "/api/tafamidis-prescribers/reconcile-links",
      "desc": "Apply a subset of the exact GET preview in { updates: [...] }; use five-row batches. Only in_tracker, practice_id and segment change. Current roster membership and old values are rechecked, each changed field is audited, and conflicts return 409. Claims, beneficiary counts and ownership remain unchanged.",
      "returns": "{ successes, conflicts, partial }"
    },
    {
      "method": "PATCH",
      "path": "/api/tafamidis-prescribers",
      "desc": "Correct beneficiary counts only. Body { updates: [{ npi, beneficiaries, expected_beneficiaries, source, reason }] }, at most 100 rows; use batches of 20. Counts are nonnegative integers or null for unavailable. Requires an HTTPS CMS source and reason. Stale values conflict; repeated values are no-ops. Writes use optimistic preconditions and are individually audited. A concurrent conflict can return 409 with partial successes; read back before retrying. Claims and ownership are not writable here.",
      "returns": "{ successes, conflicts, missing }"
    },
    {
      "method": "GET",
      "path": "/api/excluded",
      "desc": "Sites removed from the tracker, each with the reason. Filters: state, q.",
      "returns": "{ total, excluded }"
    },
    {
      "method": "POST",
      "path": "/api/excluded",
      "desc": "Add a site to the excluded list. id and practice_name are required; any other excluded writable field may be set.",
      "returns": "{ excluded } (201)"
    },
    {
      "method": "GET",
      "path": "/api/excluded/:id",
      "desc": "One excluded entry. :id is the natural key and must be URL-encoded; the numeric pid also works.",
      "returns": "{ excluded }"
    },
    {
      "method": "PATCH",
      "path": "/api/excluded/:id",
      "desc": "Correct an excluded entry's fields. Body is { field: value, ... }. Each change is audited; no-ops are skipped.",
      "returns": "{ excluded, changed, ignored }"
    },
    {
      "method": "DELETE",
      "path": "/api/excluded/:id",
      "desc": "Remove an entry from the excluded list.",
      "returns": "{ deleted }"
    },
    {
      "method": "GET",
      "path": "/api/meta",
      "desc": "Build provenance, counts, and the newest material update resolved from repository metadata and the API audit log. Add recount=1 to recompute counts.",
      "returns": "{ meta, live_counts }"
    },
    {
      "method": "PATCH",
      "path": "/api/meta",
      "desc": "Set a meta key. Value is stored as JSON.",
      "returns": "{ changed }"
    },
    {
      "method": "GET",
      "path": "/api/audit",
      "desc": "Change log, newest first. Filters: entity, entity_id, actor, limit.",
      "returns": "{ entries }"
    },
    {
      "method": "POST",
      "path": "/api/login",
      "desc": "Human sign-in { username, password }. Sets the session cookie. Agents use x-tracker-token instead.",
      "returns": "{ ok, user, must_change }"
    },
    {
      "method": "POST",
      "path": "/api/password",
      "desc": "Change your own password { current_password, new_password }, minimum 8 characters.",
      "returns": "{ ok }"
    },
    {
      "method": "GET",
      "path": "/api/me",
      "desc": "Who the caller is.",
      "returns": "{ user, public_read }"
    }
  ],
  "writable_fields": [
    "address_line1",
    "address_line2",
    "cardiologists",
    "city",
    "clinicians_in_building",
    "cms_agreement",
    "cms_enrolled",
    "cms_group",
    "cms_group_members",
    "cms_group_pac_id",
    "corporate_signature",
    "echo_addon_services",
    "echo_allowed_usd",
    "echo_billing_npis",
    "echo_ffs_beneficiaries",
    "echo_ffs_facility",
    "echo_ffs_office",
    "echo_ffs_studies",
    "echo_gross_up",
    "echo_ma_share",
    "echo_market_area",
    "echo_medicare_est",
    "ehr_source",
    "email",
    "email_confidence",
    "fax",
    "locations",
    "mso",
    "name_from",
    "notes",
    "nppes_name",
    "org_npi",
    "owner_name",
    "ownership",
    "ownership_confidence",
    "ownership_reason",
    "phone",
    "physicians_listed",
    "practice_name",
    "registry_updated",
    "research_confidence",
    "researched",
    "site_error",
    "site_fetched",
    "state",
    "status",
    "taf_propensity",
    "tafamidis_claims",
    "tafamidis_prescribers",
    "website",
    "website_confidence",
    "website_proof",
    "zip"
  ],
  "writable_json_fields": {
    "why": "Every JSON column is writable. Send an array (an object for echo_by_group); it replaces the field. Send [] or {} to clear it. The technology and contact fields keep a checked shape; the aggregates accept any JSON of the right container. At most 500 entries per field.",
    "fields": [
      "echo_by_group",
      "ehr",
      "endpoint_employers",
      "endpoint_vendors",
      "front_office",
      "other_emails",
      "pacs",
      "portal_links",
      "same_phone",
      "sources",
      "subspecialties",
      "tafamidis_by_npi"
    ],
    "shapes": {
      "ehr": "[{ vendor, product?, evidence?, page? }, ...]  — vendor required, each a string",
      "pacs": "[{ vendor, product?, evidence?, page? }, ...]  — vendor required",
      "front_office": "[{ vendor, page? }, ...]  — vendor required",
      "portal_links": "[{ href, text? }, ...]  — href an absolute http(s) URL",
      "other_emails": "[\"info@practice.com\", ...]  — each a valid address",
      "subspecialties": "[\"Interventional Cardiology\", ...]  — the roster's subspecialty aggregate",
      "endpoint_vendors": "[{ ... }, ...]  — registry Direct-endpoint vendors",
      "endpoint_employers": "[{ ... }, ...]  — registry Direct-endpoint employers",
      "echo_by_group": "{ ... }  — echo volume grouped, an object not an array",
      "tafamidis_by_npi": "[{ npi, name?, claims? }, ...]  — per-prescriber tafamidis claims",
      "same_phone": "[ ... ]  — other practices sharing this phone",
      "sources": "[\"https://...\", \"citation note\", ...]  — source URLs or citation strings"
    },
    "note": "When you set ehr from a portal, also set ehr_source to 'website'. These aggregates depend on the roster and the pipeline; if you change them, keep them consistent with the fields they summarise."
  },
  "excluded_writable_fields": {
    "why": "The excluded list is writable too, through POST/PATCH/DELETE /api/excluded. An entry has no roster and no JSON — just its name, address, the roster size measured at exclusion, and the ownership and reason it was removed. `id` is the natural key and never changes.",
    "fields": [
      "address_line1",
      "address_line2",
      "cardiologists",
      "city",
      "ownership",
      "ownership_reason",
      "practice_name",
      "state",
      "zip"
    ]
  },
  "child_collections": {
    "why": "The roster and the CPT split live in their own tables. PATCH (or POST) the practice with the whole collection and it replaces what is there. Each replacement is one audit row.",
    "physicians": "physicians: [{ npi (10 digits, required), name?, credential?, primary_taxonomy?, subspecialties?: [string], updated? }, ...]. Changing the roster does not move the cardiologists count on its own — set it too.",
    "echo_by_code": "echo_by_code: [{ code (required), desc?, services?: number }, ...]."
  },
  "custom_fields": "Anything not in writable_fields goes in the `fields` object on PATCH /api/practices/:id. A key is 1-40 characters of letters, digits or underscore. It is stored against the practice and returned under `fields`. This is how the tracker carries information it was never built to hold — an owner, a call outcome, a next step — with no migration and no deploy.",
  "read_only_fields": {
    "why": "Only the identity and the audit timestamps are fixed. `id` is the natural key and the URL, and the roster and CPT rows point at it, so it cannot change — make a new practice and delete the old one instead. Everything a practice shows is now writable, so the live database can be corrected in place. A measurement (echo volume, tafamidis, CMS, the cardiologist count) is editable too; the method page still states how the pipeline derived it, and every write is audited.",
    "fields": [
      "pid",
      "id",
      "created_at",
      "updated_at"
    ]
  },
  "enums": {
    "website_confidence": [
      "verified",
      "detected",
      "reported",
      "inferred",
      "unknown"
    ],
    "email_confidence": [
      "verified",
      "detected",
      "reported",
      "inferred",
      "unknown"
    ],
    "ownership_confidence": [
      "verified",
      "detected",
      "reported",
      "inferred",
      "unknown"
    ],
    "ownership": [
      "independent",
      "system",
      "government",
      "safety-net",
      "non-cardiology"
    ],
    "status": [
      "active",
      "closed",
      "unclear"
    ],
    "research_confidence": [
      "high",
      "medium",
      "low"
    ],
    "name_from": [
      "org_npi",
      "registry_org_name",
      "physician",
      "research"
    ]
  },
  "rules": [
    "Never invent a value. Leave a field empty rather than guess. An invented email or website reaches a real practice.",
    "A website must be an absolute http(s) URL and must carry a website_confidence. The write is refused without one, because a published address and a guessed one look the same.",
    "An email must be published by the practice. Never build one from a name pattern.",
    "Say where a fact came from. Put the source in `sources` or in ownership_reason, and set the matching confidence marker.",
    "Read the practice before you write it. A PATCH that repeats the current value is a no-op and is not audited, so a diff you did not intend means you were working from stale data.",
    "Update the live database through this API, not by re-seeding. A re-seed replaces the whole database and is for a schema change or a full pipeline regeneration. A correction, an enrichment or a batch of edits goes through PATCH, so the change is audited and the page updates at once. Run `npm run pull` afterwards to carry the edits back into the repository.",
    "You own the fields that depend on the one you change. The roster drives the cardiologist count and the subspecialty aggregate; the CPT split drives the echo totals. Change a source and set the fields that summarise it in the same request, or they disagree."
  ],
  "medicare_group": {
    "source": "CMS Doctors and Clinicians National Downloadable File",
    "what": "cms_group is the Medicare group the cardiologists at this address reassign their billing to, cms_group_members is how many clinicians bill under it, cms_enrolled is how many of this roster are enrolled at all, and cms_agreement is the share of them on the lead group. CMS keeps it from live enrolment records, so it names an employer years before the self-reported registry does.",
    "why_it_matters": "A practice whose cardiologists bill under a health system's physician network is employed by that system, whatever the door says. That signal removed 183 practices the registry showed as independent. An agreement below about two thirds means the roster is split, which usually means an acquisition part way through — filter has=split_roster.",
    "caveat": "A cardiologist who reads studies for a third party reassigns billing to that reading service too. Those groups are skipped when picking the lead, because they name a side job and not an employer."
  },
  "echo_volume": {
    "source": "CMS Medicare Physician & Other Practitioners, by Provider and Service, data year 2024",
    "counted": "Eleven CPT codes count as one study each: 93303, 93304, 93306, 93307, 93308 (transthoracic); 93312, 93314, 93315, 93317, 93318 (transoesophageal); 93350, 93351 (stress). Doppler (93320, 93321), colour flow (93325), contrast (93352), strain (93356) and probe placement alone (93313, 93316) are add-on codes that ride on a base study. They are counted in echo_addon_services and never as studies.",
    "office_vs_facility": "echo_ffs_office is billed from the practice's own office, so the practice owns the machine. echo_ffs_facility is a hospital study the practice read.",
    "not_counted": "Medicare Advantage, commercial insurance, Medicaid, self-pay and the Veterans Administration. CMS also suppresses any provider-and-code pair under 11 beneficiaries. Every count is a floor.",
    "estimate": "echo_medicare_est is echo_ffs_studies times echo_gross_up, the practice county's ratio of all Medicare beneficiaries to Original Medicare ones. It assumes an Advantage patient has an echocardiogram at this practice at the fee-for-service rate, which a capitated Advantage contract breaks. It is inferred, not measured."
  },
  "tafamidis": {
    "source": "CMS Medicare Part D Prescribers — by Provider and Drug, data year 2024",
    "what": "tafamidis_claims is the number of tafamidis (Vyndaqel/Vyndamax) claims billed to Medicare Part D in 2024, summed over the cardiologists on this roster. tafamidis_prescribers is how many of them prescribed it, and tafamidis_by_npi lists each one with their NPI and claim count. Reported prescribing is an amyloid-care signal, not proof of a dedicated practice service. Missing beneficiary counts are unavailable.",
    "joined_by": "Exact NPI on the current roster, never a name match; historical practice affiliation is not established by this join.",
    "not_counted": "Medicare Part D only, including standalone and Medicare Advantage drug plans. Not all Medicare patients have Part D; commercial insurance, cash and VA fills are excluded. CMS suppresses prescriber/drug rows with fewer than 11 claims. No published match is not evidence of zero prescribing. Claims include refills and are not unique patients.",
    "national_list": "GET /api/bootstrap also returns tafamidis_prescribers: every prescriber NPI in the Part D file, in any state and at any kind of practice, with the name, practice address and phone from NPPES. in_tracker (with practice_id) marks the ones that are independent practices in this tracker. This is the whole amyloid-treating market, not just the tracker states."
  },
  "example": "curl -X PATCH https://<host>/api/practices/$(python3 -c \"import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))\" '18450 HIGHWAY 59 N||HUMBLE|77338') -H 'content-type: application/json' -H \"x-tracker-token: $TRACKER_WRITE_TOKEN\" -H 'x-tracker-editor: David via Claude Code' -d '{\"email\":\"info@vitalheartandvein.com\",\"email_confidence\":\"verified\",\"fields\":{\"owner\":\"David\",\"next_step\":\"demo booked 2026-10-02\"}}'"
}