How to determine financial product eligibility programmatically.
Eligibility is whether a person can access a financial product at all. It is decided from the institution’s published access rules — geography (including property you own, a facility your business maintains, and radius or school-district boundaries), employer, contractor, association, school, occupation, military status — evaluated deterministically against facts the person gives you, and returned as one of four verdicts with the evidence attached. It needs no credit pull. Prequalification and underwriting answer a different question (on what terms will this lender approve you) and belong to the lender. This guide is the method behind RateAPI’s eligibility API, with the live state of its rule graph and a worked example from its data.
The state of published eligibility, live
What the rule graph holds right now, read from the graph itself — not from a projection that can only see rules with geography. Every number sits next to the stage it belongs to.
- Institutions with live rules
- 2,942 of 3,721 active US credit unions (79%).
- Live rules
- 43,724, every one stored with the verbatim sentence that justified it — the schema refuses a rule without its quote. 1,566 human-verified, 18,537 machine-validated against a second read.
- Freshness
- Median rule last confirmed 31.2 days ago; 12,263 of 43,724 confirmed within the last 30 days. Most recent confirmation September 21, 2026.
- Geographic reach
- 1,481 institutions name specific counties — 2,438 distinct counties across 51 states, stored as canonical keys (county:NC:mecklenburg) so a ZIP resolves to an exact index hit.
- Employer and affinity reach
- 1,345 institutions name specific employers, resolved to 8,035 canonical organizations; 219 name schools, 868 name places of worship, 79 carry military paths, 11 are open to anyone by a published rule.
- Credit data
- 0 conditions reference a credit score, income or debt. The condition schema has no kind for credit score, income or debt. Eligibility here is access (who may join), decided from published membership criteria only; it is not prequalification or underwriting.
- Geography (live, work, worship or study in a named place)
- 25,411 rules across 1,636 institutions.
- Employment (work for a named employer)
- 11,186 rules across 1,373 institutions.
- Family of a member or eligible person
- 4,219 rules across 2,308 institutions.
- Membership of a named association
- 1,692 rules across 664 institutions.
- Student, alumni or employee of a named school
- 822 rules across 225 institutions.
- Attend a named place of worship
- 239 rules across 93 institutions.
- Military affiliation
- 139 rules across 79 institutions.
- Open to anyone
- 11 rules across 11 institutions.
- Other
- 5 rules across 4 institutions.
unknown with unknown_reason: no_rules, never as ineligible. This Enterprise Routes aggregate is available at GET /v1/eligibility/coverage and refreshes daily.Eligibility is not prequalification
Three questions get called “can I get this loan?”. They need different data, have different owners, and must not share an answer.
- Eligibility (access)
- Can this person hold this product at all? Decided from published access rules and self-reported facts. No credit data. The institution confirms at join time. This is what this guide decides.
- Prequalification
- Would this lender likely approve them, roughly on what terms? Needs a soft credit pull and the lender’s policy. Per lender, downstream of eligibility.
- Underwriting
- Will the lender approve, on exactly what terms? Hard pull, full application, the lender’s authority alone.
What a decision needs
Three inputs. The third is the one most systems skip, and it is why they cannot explain their answers.
- Facts about the person
- Home, work and payroll geography (independent facts — a live-or-work charter is a real thing), plus where the person owns property and where their business maintains a facility, which are separate doors again; employer, contractor relationship, school + relationship, place of worship, associations; occupation, military status and the state that service was in; the kind of applicant (a natural person, or a trust, organization, partnership or corporation); qualifiers the person asserts about an employer door; family relation to someone with any of the above. All self-reported. None require documents to evaluate.
- Published access rules
- What the institution itself says about who may join or hold the product, captured as rules (one way to qualify = one rule) with conditions (ANDed within a rule), each carrying its source URL, verbatim evidence quote and observation time. The condition vocabulary spans state, county, city and ZIP geography plus radius (“within 25 miles of this branch”), school-district and census-tract boundaries; residence, work, payroll, worship, study, business, property-ownership and facility doors; employer, contractor, alumni, association, worship and military doors, the last of which can be scoped to one state; and applicant kind and org qualifiers that narrow a door the employer name alone would overstate.
- A deterministic evaluator
- Code, not a model, decides. Given facts and rules it must return the same verdict every time and be able to name the rule and conditions that produced it. Models are for extracting rules from prose; they are not the thing that decides.
What a rule looks like, as stored
“Evidence on every verdict” is an abstraction until you see one. Three live rules from the graph, exactly as held: the verbatim quote, where it came from, how it was verified, and the canonical condition it became.
- Piedmont Advantage Credit Union (NC)
- “Mecklenburg County” — geography rule, auto-extracted, confirmed 2026-08-22; condition
county:NC:mecklenburg; source: the credit union’s own membership page. - Ocean Financial Credit Union (NY)
- “Employees and Members of Maria Regina School and Parish Office, Seaford, NY” — employment rule, human-verified; condition
employee_of → resolved organization id; source: the credit union’s own membership page. - Miami Firefighters Credit Union (FL)
- “Employees of the City of Miami Firefighters and Police Officers Retirement Trust” — employment rule, human-verified; condition
employee_of → resolved organization id; source: the credit union’s own membership page.
The method
Collect one high-value fact, then decide
Home ZIP resolves server-side to county and state, and community charters are the most common access rule. Ask for it first and evaluate immediately. Every further fact sharpens the verdicts; none is required to start.
$POST /v1/eligibility/searchcurl -X POST "https://api.rateapi.dev/v1/eligibility/search" \-H "X-API-Key: $RATE_API_KEY" \-H "Content-Type: application/json" \-d '{ "home_zip": "28202", "home_state": "NC", "home_county": "Mecklenburg" }'# No credit data in the request. Nothing about income, score or debt.Narrow candidates by index, not by scan
Turn the facts into canonical keys —
county:NC:mecklenburg,org:<employer id>— and look them up in an inverse index over rule conditions. Add the institutions whose rules are open to anyone. Cap the set: a bounded candidate set with an explicitcandidate_set_truncatedflag is honest; an unbounded scan that times out is not.Evaluate each candidate against its FULL rule set
The trap: an institution surfaced by one matching condition is not thereby eligible. Its rule may require that condition AND another the person fails. Scoring on the condition that surfaced the candidate only ever over-reports.
TSDeciding one institution// The shape of a correct eligibility decision. Deterministic all the way down.type Verdict = 'eligible' | 'conditionally_eligible' | 'possibly_eligible' | 'unknown';function decide(rules: Rule[], facts: Facts): { status: Verdict; evidence: Evidence[]; unknown_reason?: string } {if (rules.length === 0) return { status: 'unknown', evidence: [], unknown_reason: 'no_rules' };const outcomes = rules.map((rule) => evaluateRule(rule, facts)); // AND across the rule's conditionsconst matched = outcomes.find((o) => o.status === 'eligible');if (matched) return { status: 'eligible', evidence: [matched.evidence] };const oneStep = outcomes.find((o) => o.status === 'conditionally_eligible');if (oneStep) return { status: 'conditionally_eligible', evidence: [oneStep.evidence] }; // carries join cost + URL// A rule that COULD match if we knew one more fact is not a no.const undecidable = outcomes.find((o) => o.missing_fact);if (undecidable) return { status: 'unknown', evidence: [], unknown_reason: undecidable.missing_fact };// Every held rule was evaluated and failed. Still not "ineligible": we cannot// prove the institution has no other door we have not captured yet.return { status: 'unknown', evidence: [], unknown_reason: 'fom_completeness_unaffirmed' };}Return four verdicts, never a boolean
- eligible
- A rule matched the facts. Carry the rule id, matched conditions and a reason sentence.
- conditionally_eligible
- One documented step away — and the response must NAME the step and its cost (join an association for $3, open a $5 share account).
- possibly_eligible
- A rule plausibly applies but a requirement could not be confirmed from the facts held.
- unknown
- Could not decide. NEVER rendered as ineligible. Always carries a machine-readable unknown_reason.
- and a question back
- A verdict short of “yes” is half an answer. Every returned possibly_eligible and unknown item carries missing_facts[] when a question would move it: the question to ask in the second person, the fact_field an answer populates, and the institution’s verbatim evidence_quote. The response rolls those up into next_questions[] — sorted so the question that opens the most institutions comes first — and states, in unanswerable[], what it deliberately will not ask about and why.
Attach the evidence to every verdict
The response below is what that looks like in practice — real, dated, trimmed. Note the
eligibleentry: a county match, with the rule id and the exact condition key that fired, and how many rules the institution holds in total.JSResponse excerpt (observed 2026-08-27){"counts": { "eligible": 15, "conditionally_eligible": 2, "possibly_eligible": 0, "unknown": 383 },"candidates_evaluated": 400,"candidate_set_truncated": true,"eligible": [{"name": "Piedmont Advantage Credit Union", "state": "NC","status": "eligible", "confidence": 1, "engine": "graph","reasons": ["You appear to be eligible to join. Qualifies: Lives in Mecklenburg County, NC"],"paths": [{"rule_id": 18281, "kind": "geography","conditions_matched": ["county:NC:mecklenburg"], "status": "eligible"}],"coverage": { "state": "published", "rules_held": 7, "last_confirmed_at": "2026-08-22T18:07:43Z" }}],"conditionally_eligible": [{"name": "American 1 Credit Union", "state": "MI","status": "conditionally_eligible", "join_cost_usd": 3,"reasons": ["You can join by completing one additional step. Qualifies after one step: Open to anyone and Qualifying payment of $3"],"paths": [{ "rule_id": 2959, "kind": "open", "conditional_on": { "type": "deposit_or_donation", "cost_usd": 3 } }]}],"unknown": [{"name": "Alta Vista Credit Union", "status": "unknown","unknown_reason": "unmodeled_condition","coverage": { "state": "under_review", "rules_held": 0 }}],"disclosure": "Membership eligibility is guidance based on public charter data and institution websites; final determination is made by the institution."}// Observed 2026-08-27. Trimmed to one entry per bucket; the real response carries up to `limit` per bucket,// plus the `missing_facts` / `next_questions` / `unanswerable` block shown below.Ask the question the response hands you
unknownis only useful if it says why — and better still if it says what to ask. In the response above, 383 of 400 candidates areunknown, most because the one fact supplied (a county) cannot decide an employer- or association-based rule. Rather than making the caller infer the remedy fromunknown_reason, the response carriesnext_questions[]: read the first entry, ask it, call again. What no question can fix is listed separately inunanswerable[]— we hold no rules for the institution, the page names an organization we could not resolve, or the boundary is one we can state but not decide (a census tract, a school district, a radius around an office the page never identifies).JSThe refinement loop// The response tells you what to ask next. You do not have to infer it.// next_questions[] is the cross-institution rollup, already sorted: the question that// opens the most credit unions, with the fewest facts, first.{"next_questions": [{"question": "Who do you work for?","fact_field": ["employer", "relative_employers"],"kind": "employee_of","unlocks_cu_count": 27,"unlocks_cu_ids": ["...", "..."],"example_evidence_quote": "Employees of St. Francis Hospital and members of their immediate families..."}],// What no question can fix. Say this out loud rather than rendering it as a "no"."unanswerable": [{ "reason": "no_rules", "cu_count": 12,"note": "We hold no published membership rules for these institutions." },{ "reason": "unmodeled_condition", "cu_count": 4, "condition_kinds": ["geo_residence"],"note": "A boundary we can state but cannot decide - a census tract, a school district, or a radius around an office the page does not identify." }]}// The loop, in code:const next = result.next_questions?.[0];if (next) {const answer = await ask(next.question); // one question, in the user's wordsreturn search({ ...facts, [next.fact_field[0]]: answer });}// Nothing to ask: render result.unanswerable, never a bare "ineligible".
Worked example, live: Mecklenburg County, NC
The place-based inverse of the same question: which institutions’ published membership criteria reach this county? Answered graph-first — live geographic rules, legacy records only where the graph is silent — at request time.
- Institutions with positive evidence
- 15 credit unions publish criteria that reach Mecklenburg County — 1 name the county specifically, 1 serve all of NC, 13 are open to anyone.
- Result semantics
- positive_evidence_only — the list is non-exhaustive. An institution that is absent is not ineligible; it is undecided.
- Evidence observed
- Most recent membership page verification August 27, 2026.
- Piedmont Advantage Credit Union
- Names 6 counties — evidence: the credit union’s own membership page (verified August 22, 2026).
Where the rules come from
A decision is only as good as its rules. Here is how RateAPI’s are captured — the same standard you should hold any source to, including your own.
- Verbatim evidence
- A rule is stored only with the sentence that justified it, machine-checked to appear in the captured page text. No quote, no rule — enforced at the database, not by convention.
- Identity guard
- The evidence page must belong to the institution it is filed under, cross-checked against the NCUA charter record. Shared vendor pages and look-alike names are the most common way eligibility data goes wrong.
- Closed-world geography
- Counties resolve against the US Census list for that state. A county that does not exist cannot be matched, and a record that names the state but no county serves the whole state rather than being dropped.
- Two-vendor gate on “open to anyone”
- The highest-stakes claim needs agreement from a second model at a different vendor before it is published — which is why so few institutions carry it by rule.
- Supersede, never overwrite
- Rules carry observed_at and superseded_at. A verdict from six months ago stays explainable with the rule that produced it.
Endpoints
/v1/eligibility/coverageThe fleet-wide aggregate rendered above: live rules, institutions, verification, freshness, reach, and a dated headline sentence. Enterprise Routes; no row-level data.
/v1/eligibility/searchThe person-based search: facts in, four buckets of institutions out, each with reasons, rule paths, confidence, coverage and unknown_reason — plus missing_facts on the undecided items, and the response-level next_questions and unanswerable block.
/v1/eligibility/factsThe person’s own prose in, the fields the search accepts out — each with the exact words it was read from and a needs_confirmation flag. An extractor, not a decider: the response has no property in which a status, a bucket or an institution could be returned. Show the person what was read, then send person_search_body to the search.
/v1/eligibility/searchThe place-based inverse: ?state=NC&county=Mecklenburg returns institutions whose published criteria reach that place, with per-row provenance. The data behind the worked example above.
/v1/eligibility/checkVerdicts for up to 50 named institutions against one person — when you already have the list, for instance from a rate search.
Frequently asked
Yes. Eligibility — whether a person can access a product at all — is decided from the institution’s published access rules (geography, employer, association, school, worship, occupation, military status) and facts the person tells you. None of those need a credit report. A credit pull only enters at prequalification or underwriting, which answer a different question: on what terms will the lender approve this person. RateAPI’s eligibility API decides access from published rules only; its rule schema has no condition kind for credit score, income or debt.
Inventory generated 2026-09-27: RateAPI holds 43,724 live membership-eligibility rules across 2,967 inventory institutions; 2,942 of 3,721 active credit unions have at least one current rule (79%), each backed by a verbatim evidence quote and source URL; 1,481 name specific counties (2,438 counties across 51 states) and 1,345 name specific employers (8,035 resolved organizations). The newest rule confirmation is 2026-09-21; individual evidence dates vary. No rule references a credit score, income or debt. Row-level verdicts with the evidence behind each come from Enterprise Routes operations POST /v1/eligibility/search and POST /v1/eligibility/check; the aggregate at /v1/eligibility/coverage updates daily.
An eligibility API answers “can this person access this product at all?” from published access rules, with no credit data, and its answer is guidance the institution confirms at join time. A prequalification API answers “would this lender likely approve this person, on what terms?”, needs a soft credit pull and the lender’s underwriting rules, and its answer belongs to that lender. Eligibility comes first and is the wider net; prequalification is per-lender and downstream.
Every verdict should carry: the rule that produced it (an id you can look up later), the conditions of that rule the person’s facts matched, a deterministic reason sentence rendered from those conditions rather than model prose, the source URL and observation time of the evidence, a confidence carried from extraction, and — for anything short of a positive match — a machine-readable reason why (which fact is missing, or that no rules are held). In RateAPI’s response that is `paths[].rule_id`, `paths[].conditions_matched`, `reasons[]`, `coverage.last_confirmed_at`, `confidence` and `unknown_reason` — and, for anything undecided, `missing_facts[]` carrying the question to ask, the field an answer fills and the institution’s verbatim quote, rolled up across institutions as `next_questions[]`. A verdict without this is a number you cannot explain or correct.
In two separate systems. Access rules (who may hold the product: membership, geography, affinity) are evaluated deterministically from published criteria and self-reported facts. Underwriting (whether to approve, and on what terms) runs the lender’s credit policy against credit-bureau data. Conflating the two produces software that implies approvals it has no authority to make, or that hides products from people who could have accessed them.
Ask one high-value fact first (home ZIP), call an eligibility search, and render the eligible and conditionally-eligible institutions with their reasons and join cost. Then price the products those institutions actually offer, from their own published rate sheets. Do not show a product as “qualified” on eligibility alone — say “you can join, and this is their published rate”, and leave approval to the lender.
Return unknown with a reason and a question, never ineligible. RateAPI’s search does both: an undecided institution carries missing_facts[] — the question to ask, the field an answer populates, and the institution’s verbatim quote — and the response rolls those up into next_questions[], sorted so the question that opens the most institutions comes first. What no question can fix is stated in unanswerable[]: no rules held, an organization the page names that we could not resolve, or a boundary we can state but not decide. Treating unknown as a no silently removes real options from people; the failure is invisible in your metrics and very visible to the person.