Most claim denials are not clinical disputes. They are answers to questions nobody asked before the visit: is this coverage still active, is this the right payer, does this plan cover this service, and what does the patient owe?
Eligibility verification asks those questions in advance. It is the cheapest denial prevention available — a check that costs cents and prevents rework that costs hours — and it is routinely built as a manual screen someone is supposed to remember to use.
This guide covers automating it properly: the transaction pair, what the response actually contains, how to correlate answers to questions, and the scheduled work that keeps coverage accurate as it changes underneath you.
The transaction pair
Eligibility runs on two X12 transactions. You send a 270 — an inquiry naming the patient, the payer, the provider and optionally the service type. The payer returns a 271 containing the benefit picture.
Unlike claims, this pair is usually real-time. Response times are seconds, which means it can run during scheduling or check-in rather than as an overnight batch. That single property is what makes it useful at the point where it prevents problems.
What a 271 actually tells you
Teams expect a yes/no and get something richer and messier. A 271 carries a set of benefit statements, each scoped to a service type, a coverage level and a time period:
- Coverage status — active, inactive, or the payer has no record of this member
- Plan details — plan name, group number, the payer's own member identifier, which frequently differs from the card the patient handed over
- Copay — often per service type, so mental health and primary care differ
- Coinsurance — a percentage rather than an amount
- Deductible — total and, critically, remaining
- Coverage dates — when this plan started and when it ends
Two things bite here. First, the same field can appear multiple times at different scopes — a plan-level copay and a service-specific copay. Pick the most specific match, not the first one you encounter. Second, payers vary in what they return. Some send a full benefit breakdown; some send little more than "active". Your model must tolerate partial answers rather than assuming a complete response.
Correlating answers to questions
Every verification is a request that expects a specific reply, and replies arrive asynchronously even on a real-time rail — retries, timeouts, and batch fallbacks all break the neat request/response assumption.
The correlation key is the trace number you place on the outbound 270 and the payer echoes back on the 271.
# Outbound: stamp a trace number tied to YOUR verification record
verification = create_verification(patient_id, insurance_id, trigger="pre_appointment")
trace_number = f"VER{verification.id}"
send_270(patient, payer, provider, trace_number=trace_number)
# Inbound: resolve the answer back to the question that asked it
trace = extract_trace(response_271)
verification = find_by_trace(trace)
if verification is None:
park_for_manual_review(response_271) # never discard
return
apply_benefits(verification, parse_271(response_271)) Never discard an unmatched response. Park it for triage. A 271 you cannot match is usually a correlation bug, and silently dropping it hides the bug while losing the answer.
Deriving a status your application can act on
Raw benefit statements are not directly usable by a scheduling screen. You need a single derived status, and the derivation should live in exactly one place:
| Derived status | Meaning | What the product does |
|---|---|---|
| Verified active | Coverage confirmed for the service | Proceed; show expected patient cost |
| Inactive | Payer says coverage is not in force | Route to self-pay or updated insurance |
| Not found | Payer has no record of this member | Almost always a data problem — recheck the ID |
| Needs info | Payer rejected the inquiry as incomplete | Fix the request and resend |
| Pending | Sent, no response yet | Retry on a schedule; escalate if stale |
| Expired | Previously verified, now past the recheck window | Reverify before the next visit |
"Not found" deserves special attention. It usually means a transposed member ID, a patient using a maiden name, or the wrong payer selected from a list of similarly named plans. It is a data-quality signal, not a coverage answer, and treating it as "inactive" pushes insured patients into self-pay.
Verification is not an event, it is a schedule
The most common design failure is treating eligibility as something checked once when the patient is registered. Coverage changes constantly — people change jobs, plans renew, employers switch carriers, and none of that generates a notification to you.
A production implementation runs a recurring sweep that handles four jobs:
- Pre-appointment reverification. Any upcoming appointment whose coverage has not been verified inside the recheck window gets queued. This is the highest-value job in the whole system.
- Recheck cadence. Coverage verified 90 days ago is not evidence of coverage today.
- Expiry transitions. Coverage with a termination date in the past moves to expired automatically, rather than waiting for a denial to reveal it.
- Stuck-request escalation. Inquiries pending too long are retried, then escalated for human review.
Make every phase idempotent — skip anything with a live pending request or a recent attempt — so the sweep can run hourly without generating duplicate traffic to payers.
Alerting on a ladder, not a cliff
Coverage ends on a date you already know. Warn on a ladder:
- 30 days out — notify billing staff; there is time to resolve it calmly
- 7 days out — notify the patient; ask for updated insurance
- Expired — block automatic claim generation and route to a self-pay decision
That last one matters: an expired policy must not silently generate an insurance claim. It will be denied, and you will have spent the effort of a claim cycle to learn what you already knew.
Turning benefits into an expected patient cost
The commercial payoff of good eligibility data is telling a patient what they owe before the visit, and collecting it at the visit.
The estimate is a simple waterfall, with judgement required:
if deductible_remaining >= service_cost:
patient_owes = service_cost # deductible not met
elif copay is not None:
patient_owes = copay # flat copay applies
else:
patient_owes = allowed_amount * coinsurance_rate Two rules keep this honest:
- Label it an estimate. Deductible-remaining figures lag real-time — other claims may be in flight. Presenting an estimate as a final amount creates disputes.
- Record the source. Store which verification produced the estimate. When a patient questions a bill months later, you need to show what the payer said at the time.
What to build first
- Send a 270 and parse the 271 for one payer, end to end
- Trace correlation, with unmatched responses parked rather than dropped
- Derived status in one place, consumed everywhere
- Pre-appointment sweep — the single highest-value automation
- Expiry ladder and alerts
- Patient cost estimation, once the benefit data is proven reliable
Steps 1 to 4 prevent most denials. Steps 5 and 6 are what practices actually notice and value.
The short version
Eligibility is the cheapest denial prevention in the revenue cycle, and it works only if it runs on a schedule rather than when someone remembers. Correlate by trace number, never drop an unmatched response, derive status in one place, sweep before appointments, and warn on a ladder well before coverage lapses.
Once eligibility is solid, the claims side gets dramatically easier — our guide to clearinghouse integration covers what happens after the claim is built.
Building eligibility or prior authorization automation into a healthcare platform? Our healthcare interoperability solutions team builds these rails end to end. Talk to our team.



