MyAtriumHealth via fpx
Atrium Health's patient portal is Epic MyChart at my.atriumhealth.org. Its web app
talks to a clean JSON API, and this skill drives that same API through your signed-in
browser tab.
Why the browser is required: every MyChart cookie is HttpOnly, so the session
cannot be read or reproduced outside the browser, and login is MFA-gated. Requests are
relayed through the tab; the session cookie is never extracted or stored.
This reads personal health information. It only ever touches the signed-in user's own account.
One-time setup
npm i -g @fetchproxy/cli # also needs the ContextMint Bridge extension
ContextMint Bridge comes from
https://github.com/nullnet-app/contextmint-bridge/releases (Chrome: load the chrome
zip unpacked; use Chrome for now — Safari will ship inside the ContextMint app, which
has no public download yet). It is the fetchproxy extension renamed, same maintainer;
source is public there — build it, or verify a zip with shasum -a 256 -c <zip>.sha256.
Create the profile with its full scope declared up front — widening scope later invalidates the grant and forces a re-pair:
fpx profile add myatriumhealth --domain atriumhealth.org
fpx profile declare myatriumhealth \
--cookie '_Host-MyChart_Session' \
--cookie '__RequestVerificationToken_L215YXRyaXVtaGVhbHRo0' \
--cookie '_Host-MyChartLocale' --cookie 'MYCPERS' --cookie 'p-MYC-LBPersistence' \
--capture-header 'cookie@my.atriumhealth.org'
Then sign in to MyAtriumHealth in Chrome and run any command below. The first one prints a pair code — approve it in the ContextMint Bridge popup. The trust persists.
Use it
source references/mah.sh
mah_signed_in || echo "sign in to MyAtriumHealth in Chrome first"
export MAH_TOKEN="$(mah_token)" # cache once per shell
mah_api allergies/LoadAllergies | jq '[.dataList[].allergyItem | {name, severe: .isSevere}]'
mah_api <area/Action> [json-body] posts to the modern api/… endpoints (body
defaults to {}, which is all most of them need). mah_legacy <Area/Controller/Action> [querystring] handles the older form-encoded ones (visits).
Every call needs the ASP.NET antiforgery token — mah_token scrapes it from a
signed-in page and the helpers attach it. Responses are large (test results 33 KB,
medications 30 KB); always project with jq.
See references/endpoints.md for the full endpoint list with ready-to-run,
live-verified jq recipes, and references/mah.sh for the helpers.
Check the session, not the status code
MyChart answers an expired session with HTTP 200 whose body is the login page, and
the JSON endpoints then quietly return {}. A status check would report success
forever. mah_signed_in tests the body — use it when results come back empty. Sessions
are short-lived; expect to sign in again between uses.
There is a second, sneakier false green: an empty body. fetchproxy issues requests
from inside a tab on the target host, so with no my.atriumhealth.org tab open it
relays nothing — and empty text contains no login marker, so a naive "is this the login
page?" check reports SIGNED IN for a response that never happened. mah_signed_in
checks emptiness first and returns 2 with the real remedy (exit 1 means signed out,
0 means signed in).
Exit codes (fpx)
0 ok · 1 usage · 2 bridge unavailable (extension not connected, or pairing not
approved) · 3 bot wall · 4 upstream HTTP error.
Data goes to stdout, pair codes and status to stderr — so | jq stays clean.
Messages
mah_messages [folderTag] reads the Message Center. It is the one endpoint that cannot
be called with {}: it needs a five-key body whose PageNonce is the CSP nonce of an
/app/* page, and whose externalLoadParams must list the non-local organizations
only (filter api/conversations/GetOrganizations on the explicit isLocal flag —
passing the local org returns HTTP 500). The helper handles all of that.
api/item-feed/FetchItemFeed still needs parameters that have not been captured.
