ABA Routing Number Validation in Python — US Banking
ABA routing numbers are 9-digit codes that identify US financial institutions for ACH transfers, wire payments, and check processing. Here's how the check-digit algorithm works and how to validate them in production.
In this guide
1. What is an ABA routing number?
An ABA routing number (also called a routing transit number or RTN) is a 9-digit code assigned by the American Bankers Association to identify US financial institutions. Introduced in 1910, it is one of the oldest bank identification systems still in active use.
Routing numbers appear on the bottom-left of paper checks (in MICR encoding) and are required for virtually every type of US domestic payment:
- ACH transfers — direct deposit, payroll, bill payments
- Fedwire transfers — same-day domestic wire payments
- Direct deposit — salary, tax refunds, government benefits
- Check processing — paper check clearing through the Federal Reserve
If your application accepts US bank account details — for payouts, payroll, or account linking — you need to validate the routing number before initiating any transaction.
2. ABA anatomy — prefix, identifier, check digit
Every ABA routing number has three components:
Federal Reserve prefix (2 digits)
The first two digits identify the Federal Reserve district where the institution is located. Values range from 01 (Boston) to 12 (San Francisco), with special prefixes for thrift institutions (21–32) and government accounts (00).
ABA institution identifier (4 digits)
Digits 3 through 8 uniquely identify the financial institution within its Federal Reserve district. Combined with the prefix, these six digits form the institution's unique identifier in the Federal Reserve system.
Check digit (1 digit)
The 9th digit is calculated using a weighted-sum algorithm. It detects most single-digit transcription errors and adjacent-digit transpositions.
| Prefix | Federal Reserve District |
|---|---|
| 01 | Boston |
| 02 | New York |
| 03 | Philadelphia |
| 04 | Cleveland |
| 05 | Richmond |
| 06 | Atlanta |
| 07 | Chicago |
| 08 | St. Louis |
| 09 | Minneapolis |
| 10 | Kansas City |
| 11 | Dallas |
| 12 | San Francisco |
3. The check digit algorithm — step by step
The ABA check digit uses a weighted-sum algorithm. The weights 3, 7, 1, 3, 7, 1, 3, 7, 1 are applied to all 9 digits, and the resulting sum must be divisible by 10. Let's walk through it with 102101645:
Step 1 — Write out the digits and their weights
| Position | d1 | d2 | d3 | d4 | d5 | d6 | d7 | d8 | d9 |
|---|---|---|---|---|---|---|---|---|---|
| Digit | 1 | 0 | 2 | 1 | 0 | 1 | 6 | 4 | 5 |
| Weight | 3 | 7 | 1 | 3 | 7 | 1 | 3 | 7 | 1 |
Step 2 — Multiply each digit by its weight
(1×3) + (0×7) + (2×1) + (1×3) + (0×7) + (1×1) + (6×3) + (4×7) + (5×1)
= 3 + 0 + 2 + 3 + 0 + 1 + 18 + 28 + 5
Step 3 — Sum the products and check divisibility by 10
3 + 0 + 2 + 3 + 0 + 1 + 18 + 28 + 5 = 60 → 60 mod 10 = 0 ✓
The sum is divisible by 10, so the check digit is valid.
Here's the algorithm implemented in Python:
# aba_check_digit.py — ABA routing number checksum validation import re def is_valid_aba(routing_number: str) -> bool: """Validate an ABA routing number using the 3-7-1 weighted checksum.""" if not re.fullmatch(r"\d{9}", routing_number): return False digits = [int(c) for c in routing_number] weights = [3, 7, 1, 3, 7, 1, 3, 7, 1] weighted_sum = sum(d * w for d, w in zip(digits, weights)) return weighted_sum % 10 == 0 print(is_valid_aba("102101645")) # True print(is_valid_aba("102101646")) # False — bad check digit
4. Why manual validation isn't enough
Checksum only catches typos
A routing number can pass the 3-7-1 checksum and still be completely fictitious. There are roughly 28,000 active routing numbers in the US — a valid checksum does not guarantee the number is actually assigned to a financial institution.
Doesn't verify the bank exists
Banks merge, close, and change routing numbers over time. A routing number that was valid last year may no longer be active. The Federal Reserve publishes an updated routing number directory, but maintaining a local copy means tracking weekly updates.
ABA vs. ACH routing differences
Some banks use different routing numbers for paper checks (ABA) and electronic transfers (ACH). A routing number valid for check processing may not work for ACH direct deposits, and vice versa. Large banks like JPMorgan Chase and Bank of America have dozens of routing numbers across different states and use cases.
Federal Reserve prefix validation
A routing number with prefix "15" or "99" is invalid because no Federal Reserve district uses those prefixes. A regex or checksum alone won't catch this unless you also maintain a prefix table.
5. The right solution
The IsValid ABA API validates the checksum, verifies the Federal Reserve prefix, and parses the routing number into its components in a single GET request. The response includes the Federal Reserve district, institution identifier, and check digit — all the fields you need for downstream processing.
Get your free API key at isvalid.dev. The free tier includes 100 calls per day — enough for most development and low-volume production use.
Full parameter reference and response schema: ABA Routing Number Validation API docs →
6. Python code example
Using the isvalid-sdk Python SDK or the requests library. Install with pip install isvalid-sdk or pip install requests.
# aba_validator.py import os from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) # ── Example usage ──────────────────────────────────────────────────────────── result = iv.aba("102101645") if not result["valid"]: print("Invalid routing number") else: print(f"Valid ABA — Fed district: {result['federalReserveDistrict']}") print(f"Routing number: {result['routingNumber']}") print(f"Check digit: {result['checkDigit']}") print(f"Institution ID: {result['abaInstitutionIdentifier']}") # → Valid ABA — Fed district: Kansas City # → Routing number: 102101645 # → Check digit: 5 # → Institution ID: 210164
In a bank account verification flow, you might use it like this with Flask:
# app.py (Flask) from flask import Flask, request, jsonify app = Flask(__name__) @app.post("/bank-account") def bank_account(): data = request.get_json() try: aba_check = validate_aba(data["routing_number"]) except requests.RequestException: return jsonify(error="ABA validation service unavailable"), 502 if not aba_check["valid"]: return jsonify(error="Invalid routing number"), 400 # Log the Federal Reserve district for compliance district = aba_check["federalReserveDistrict"] app.logger.info(f"Verified routing number in {district} district") link_bank_account( routing_number=data["routing_number"], account_number=data["account_number"], ) return jsonify(success=True, district=district)
federalReserveDistrict field to display the bank's region to the user for confirmation. This helps catch cases where a user accidentally enters a routing number from a different branch.7. cURL example
Validate a routing number from the command line:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/aba?value=102101645"
Test with an invalid routing number:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/aba?value=000000000"
8. Understanding the response
Valid routing number:
{ "valid": true, "routingNumber": "102101645", "federalReservePrefix": "10", "federalReserveDistrict": "Kansas City", "abaInstitutionIdentifier": "210164", "checkDigit": 5 }
Invalid routing number:
{ "valid": false }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the routing number passes format, prefix, and checksum validation |
| routingNumber | string | The 9-digit routing number as submitted |
| federalReservePrefix | string | First 2 digits identifying the Federal Reserve district (01–12 for banks, 21–32 for thrifts) |
| federalReserveDistrict | string | Human-readable name of the Federal Reserve district (e.g. "Kansas City") |
| abaInstitutionIdentifier | string | Digits 3–8 identifying the specific financial institution within the district |
| checkDigit | number | The 9th digit used for checksum validation (weighted sum mod 10) |
9. Edge cases to handle
Electronic vs. paper routing numbers
Some banks use different routing numbers for electronic (ACH) and paper (check) transactions. When a user provides a routing number from a check, it may not work for ACH direct deposits. Always ask users to specify the type of transaction and confirm the routing number matches the intended use case.
# Prompt users for the correct routing number type def get_routing_number_help(transfer_type: str) -> str: if transfer_type == "ach": return "Enter the electronic/ACH routing number (contact your bank if unsure)" return "Enter the routing number from the bottom-left of your check"
Thrift institution prefixes (21–32)
Credit unions and savings institutions use Federal Reserve prefixes in the range 21–32. These map to the same 12 districts (21 = Boston, 22 = New York, etc.) but indicate a thrift institution rather than a commercial bank. A simple prefix check that only accepts 01–12 will incorrectly reject these valid routing numbers.
Government routing numbers (prefix 00)
The prefix 00 is reserved for the US government (e.g. the US Treasury). These routing numbers are valid but are not used by commercial banks. If your application only handles private-sector payments, you may want to flag these separately rather than rejecting them.
Multiple routing numbers per bank
Large banks often have different routing numbers for different states or regions. For example, JPMorgan Chase uses different routing numbers in California, New York, Texas, and other states. Bank of America has over 30 active routing numbers. Do not assume a single routing number per bank — always validate the specific number the user provides rather than looking up by bank name.
Summary
See also
Validate ABA routing numbers instantly
Free tier includes 100 API calls per day. No credit card required. Checksum validation, Federal Reserve prefix parsing, and institution identifier extraction in a single call.