Securing Point-of-Care and Post-Visit Patient Payments: A HIPAA-Safe Architecture
Every patient balance request is a disclosure. The moment your system asks a patient for $55.00, it has told someone that this person received care, from you, on a date, at a price. That is protected health information, and it is going to travel over a channel you do not control — a carrier's SMS network, a consumer email provider, a phone sitting face-up on a kitchen counter.
Most practices resolve this by routing everything through the patient portal and treating any link sent by text as the unsafe option. That instinct is half right. A portal login is an authentication control. It answers "is this the right person?" It does not answer "how much did we just expose, and to whom, if this goes wrong?" That second question is a disclosure-scope question, and you can answer it well or badly with or without a login.
This article specifies the architecture that answers it: what belongs in the notification, what belongs behind a step-up, how the token should be built, which channel to send it on, where the trigger fires, and how the money finds its way back to the right encounter.
The portal login is an authentication control, not a disclosure control
Two problems with "just use the portal."
The first is operational. Enrollment and password recovery sit between the patient and the payment. Every balance ask that begins with a credential the patient does not currently possess has a drop-off point before the payment page ever renders. Measure your own portal auth-wall abandonment before assuming it is small [verify].
The second is architectural, and it is the more interesting one. A portal session unlocks the whole record — the chart, the billing history, every prior encounter. A balance ask needs exactly one encounter's payment data. Sending a patient through a broad-scope credential to view a narrow-scope fact is a mismatch, and the mismatch is what makes portal password-reset emails such a common weak point: the reset link is frequently a single-factor bearer credential to everything.
The right unit of comparison is not "login vs. link." It is what a leaked artifact unlocks.
| Portal login | Unauthenticated pay link | Tokenized link + step-up | Request for payment (RTP/FedNow) | |
|---|---|---|---|---|
| Credential patient needs | Enrolled account + password | None | Something they know (DOB) | Their existing bank app |
| What a leaked URL alone exposes | Nothing (login gate) | Full itemization, indefinitely | Nothing — step-up required | N/A, no URL |
| Scope unlocked on success | Entire chart + billing history | One balance | One encounter, one TTL | One request |
| Data crossing your perimeter | Notification only | Balance + itemization | Opaque token only | Amount + remittance string |
| Where patients drop off | Password reset | — | Step-up mismatch | Bank doesn't receive RfP [verify coverage] |
| Audit granularity | Session-level | Usually none | Per-token, per-view, per-attempt | Per-message status |
| Revocation | Disable account | None — URL lives forever | Expire or burn the token | Cancel the request |
The tokenized link and the request-for-payment rail are the two designs where a compromised artifact has a bounded blast radius. That is the property to build for.
What the patient is allowed to see (worked example)
Synthetic values throughout — no real identifiers, no patient data.
A two-line professional claim comes back on the 835:
CLP*PCN-8823*1*207.00*94.00*55.00*12*ICN-2026-0731-4417*11~
SVC*HC:99213*165.00*94.00**1~
CAS*CO*45*46.00~
CAS*PR*3*25.00~
SVC*HC:85025*42.00*0.00**1~
CAS*CO*45*12.00~
CAS*PR*1*30.00~The service-line balancing rule — SVC02 = SVC03 + the sum of that line's CAS amounts — holds on both lines: 165.00 = 94.00 + 46.00 + 25.00 and 42.00 = 0.00 + 12.00 + 30.00. At the claim level, CLP03 − CLP04 = 207.00 − 94.00 = 113.00, which equals the sum of all four CAS amounts. The PR group totals 25.00 + 30.00 = 55.00, matching CLP05.
So: 58.00 in CO-45 contractual adjustments, which are **not billable to the patient under any circumstances**, and 55.00 of genuine patient responsibility — a 25.00 copay (PR-3) and 30.00 applied to deductible (PR-1).
The front desk collected the 25.00 copay at check-in. Net billable is **30.00, not 55.00. Asking for 55.00 here is not a rounding error; it is an over-collection that creates a refund obligation and burns the patient's trust in every future number you send them.
Now the disclosure design. Three payloads, three different exposure profiles:
1. The notification (SMS or email body). Crosses a network you do not control, lands on a lock screen, may be read by someone else.
Ridgeline Family Medicine billing: a balance from your 07/22
visit is ready to view. https://pay.example.com/r/7Kq2vX9m
Reply STOP to opt out.No amount. No procedure. No diagnosis. No account number. Note that even the practice name is a judgment call — for a family medicine practice it is routine, for an oncology or behavioral health practice the name alone discloses a condition, and a confidential-communications request under 45 CFR 164.522(b) may require you to use a neutral sender [verify with counsel].
2. The URL. An opaque token and nothing else. No encounter ID, no MRN, no amount, no name — not in the path, not in a query string, not base64'd where it looks opaque but isn't.
3. The page, after step-up. This is where the itemization lives:
Visit 07/22/2026 · Ref ENC-8823
Office visit $25.00 copay — paid at check-in
Lab $30.00 applied to deductible
─────────
Amount due $30.00Adjustment category is shown; procedure detail is not, and the $58.00 in CO adjustments never appears in patient-facing output at all. Disclosures to the individual themselves are formally exempt from the minimum-necessary standard — but the channel is not a private channel, so design for the unintended recipient, not the intended one [verify with counsel].
Designing the token
The token is the whole security boundary. Specification:
- Opaque and high-entropy. 128 bits or more from a CSPRNG. Not a sequential ID, not a hash of the account number, not anything an attacker can enumerate or derive.
- Resolved server-side. The token is a lookup key into a table you control. It carries no payload. Rotating or revoking is a row update.
- Scoped to one encounter. Not to a patient, not to a guarantor account. A token that resolves to "everything this person owes" has re-created the portal's scope problem with none of its authentication.
- Time-bounded, with re-issue instead of extension. Expiry sends the patient back through your notification flow to get a fresh token — a mildly worse UX and a substantially better security posture.
- Step-up before any PHI renders. One factor the patient knows: date of birth, or the last four of an account reference. Constant-time comparison, rate limiting per token and per IP, lockout after a small number of failures, and a generic error message that does not confirm whether the token or the answer was wrong.
- Transport hygiene.
Cache-Control: no-store,Referrer-Policy: no-referrerso the token never rides an outboundRefererheader, and no third-party analytics or marketing tags on the payment page — a session-replay script on a page rendering a balance is a disclosure to a vendor you probably have no BAA with. - Log every event. Token issued, notification sent (channel + destination-hash), each view attempt with outcome, payment initiated, payment settled. This is the same accounting you would expect for portal access, and you will want it the first time a patient asks who saw their bill.
Delivery and the trigger that fires it
Channel choice is a real tradeoff and the honest answer is that it is population-dependent — SMS is generally faster to open, email carries more detail and a better audit trail, and the efficacy split for your specific patient panel is something to measure rather than inherit from someone's benchmark [verify].
The constraints that are not negotiable:
- SMS. No clinical content, ever, and no PHI in the URL. Automated billing texts implicate TCPA consent independently of HIPAA, so capture explicit consent at check-in, honor STOP immediately and permanently, and respect quiet hours [verify with counsel].
- Email. No PHI in the subject line — subjects surface in notification previews and sync to devices you have never heard of. OCR guidance permits unencrypted email where the individual has been warned of the risk and still requests it; document that request [verify with counsel].
- Both. Verify the destination against what the patient gave you at the most recent check-in. A stale mobile number is how a balance reaches a stranger.
The trigger belongs on 835 ingest, not on a nightly or monthly batch. When the remittance parses, the PR lines sum, the CO/PI/OA groups are structurally suppressed from patient-facing output, front-desk collections net against PR-3, and the request dispatches — in that order, as code, not as a checklist someone remembers. Guardrails before you automate: an amount threshold above which a human reviews, suppression for accounts in dispute or in financial assistance, a dedupe key so a corrected 835 does not produce a second ask, and a payment-plan path for balances above what a single request should carry.
Once the patient approves, what happens to the money is a separate architecture — see FedNow and RTP: Implementing Real-Time Settlement for Independent Practices for the settlement side [verify link target].
Closing the loop: reconciliation
An unmatched payment is an operational problem that becomes a compliance problem the moment someone starts emailing spreadsheets around to resolve it.
- Match on the encounter key carried by the token, never on amount. Two patients owing $30.00 on the same day is not an edge case.
- Make the settlement callback idempotent. Key on the payment reference. Networks retry; your ledger should absorb a duplicate without double-posting.
- Handle partials explicitly. A 30.00 request paid at 20.00 leaves an open $10.00 balance and needs a defined re-ask rule, not a silent write-off.
- Handle the secondary-payer race. A patient can pay while a secondary 835 is in flight that reduces their responsibility to zero. Detect it, refund promptly, and log the refund against the same encounter.
- Reconcile against `CLP05` minus prior collections every time. That figure is the only defensible number, and it is the one you can walk a patient through line by line if they call.
Try it in the shteg.ai sandbox
Load a de-identified 835 and the sandbox returns the full patient-facing path: PR lines separated from CO/PI/OA, copay netted against front-desk collections, the exact billable amount per encounter, and the three rendered payloads — notification body, tokenized URL, and post-step-up itemization — exactly as the patient would receive them.
Bring one file. The gap between what your system knows and what your patient can actually verify is usually the whole finding.
[Open the shteg.ai sandbox →](/sandbox) [verify link target before publish]
FAQ
Can we just text the amount directly and skip the link? You can, and some practices do, but you have then put PHI on a lock screen with no step-up, no expiry, and no revocation. The amount is the sensitive part in a way the practice name usually is not — it implies service intensity. A notification that says "a balance is ready" plus a bounded token gives you the same conversion path with a blast radius you can actually describe in a risk analysis.
Is our payment vendor a business associate? It depends on what they touch and in what role. HHS has long distinguished financial institutions performing payment-processing activities from business associates, but a vendor that renders your itemization, stores your encounter references, or hosts the payment page is doing considerably more than moving money. Map each vendor's actual data access against the definition and get a BAA where it applies — do not assume the answer from the vendor's marketing page [verify with counsel].
A patient says they never got the text. What is the safe recovery path? Re-issue a fresh token to the contact point on file — never read the balance to an unverified caller and never resend to a number supplied during that call without verifying identity through your normal process. If the contact point itself is wrong, that is a demographics update, and it should go through the same verification you would use for any other change to the record. Log both the failed delivery and the re-issue.
Educational content for physicians, practice administrators, and revenue-cycle staff. Not legal, billing, coding, or compliance advice. All segment values, claim identifiers, dollar amounts, tokens, URLs, and practice names in this article are synthetic illustrations and contain no patient data. HIPAA, TCPA, and state privacy requirements are fact-specific and change over time — consult counsel for your jurisdiction and your vendor arrangements. Payment network capabilities and operating rules change; verify current specifications with your financial institution and clearinghouse before making operational decisions.
Draft notes: ~1,640 words. 5 body H2s + FAQ H2 (6 total), CTA as H3. Worked example carries the 835 arithmetic verified at both levels (SVC02 = SVC03 + ΣCAS on both lines; CLP03 − CLP04 = 113.00 = ΣCAS; CLP05 = 55.00 = PR total), plus the PR-3 netting that reduces the actual ask to $30.00. One comparison table (access models by blast radius). Seven [verify] flags: portal abandonment, RfP coverage, SMS/email efficacy, sender-name confidentiality, minimum-necessary interpretation, TCPA consent, BAA boundary, and two link targets. No statistics asserted. Internal link to brief #2 placed per the brief's linking strategy; the brief's #3 link is carried by the existing patient-payments-1 draft instead, to avoid both drafts pointing the same way.
Distinct from patient-payments-1-mssk80vl.md: that draft argues latency (statement cycle vs. RfP); this one argues disclosure scope and token architecture. Different claim, different dollar figures, no shared table.
Want me to save this as examples/seo-presence/workspace/drafts/patient-payments-1-<id>.md? I held off because two patient-payments-1-* drafts already exist there and picking the id is a pipeline decision, not mine.
<!-- generated_by: claude-draft-1 -->