TL;DR
What changed?
- Card networks (Visa, Mastercard, and others) expanded the Bank Identification Number (BIN) from 6 digits to 8 digits, per ISO 7812-1:2017.
- The first 8 digits of a card number now define the issuer, not just the first 6.
- Any system that truncates, stores, indexes, or regex-matches against only 6 BIN digits is now potentially misidentifying cards.
What should you do?
- Audit every database column, regex pattern, cache key, and analytics pipeline that assumes a fixed 6-digit BIN.
- Expand BIN storage fields from
CHAR(6)toVARCHAR(8)and update all related indexes. - Replace homegrown BIN lookups with an API that natively supports 6-to-8 digit ranges, such as BinSearchLookup.
- Re-train or re-feature any fraud or risk models that use raw BIN as a feature input.
- Test against both 6- and 8-digit BINs before deploying any change to production.
Why Did Networks Move to 8-Digit BINs?
The short answer is that the card industry ran out of BIN space.
The longer answer involves decades of growth. When ISO 7812 was first standardized, 6 digits allowed for roughly 810,000 unique BINs (the first digit is the Major Industry Identifier and is fixed per network, reducing the available combinations). That sounded like plenty in 1989.
By the 2010s, it was not plenty. The explosion of fintech issuers, virtual card programs, co-branded portfolios, and multi-currency prepaid products created a shortage. Networks began assigning BINs in blocks smaller than traditional ranges, and eventually those blocks ran thin.
The formal timeline:
| Year | Event |
|---|---|
| 2017 | ISO 7812-1:2017 published, formally extending IIN/BIN to 8 digits |
| 2019 | Mastercard mandates acquirer and processor readiness for 8-digit BINs |
| 2022 | Visa begins issuing 8-digit BINs commercially at scale |
| 2023 | Industry-wide adoption; most major processors now receive 8-digit BINs in authorization flows |
| 2024+ | Legacy 6-digit systems are actively misrouting or misidentifying a growing share of transactions |
The practical consequence: a card starting with 12345678xxxx... might have been identified by BIN 123456 under the old system. Under the new system, the correct BIN is 12345678. These can belong to completely different issuers in different countries. A 6-digit lookup on an 8-digit BIN card returns stale or wrong data.
What Breaks When You Still Assume 6 Digits?
More things break than most teams expect. Here is a category-by-category breakdown.
Database Schema
The most common problem. BIN columns defined as CHAR(6) or VARCHAR(6) silently truncate 8-digit values on insert.
-- Old schema (breaks silently)
CREATE TABLE bin_cache (
bin CHAR(6) NOT NULL PRIMARY KEY,
issuer VARCHAR(128),
country CHAR(2),
brand VARCHAR(32)
);
-- Insert an 8-digit BIN
INSERT INTO bin_cache (bin, issuer, country, brand)
VALUES ('12345678', 'Example Bank', 'US', 'VISA');
-- Stored value becomes '123456' in PostgreSQL, or raises a truncation error in strict mode
In PostgreSQL with standard_conforming_strings on, this may silently truncate. In MySQL with STRICT_TRANS_TABLES disabled, same result. You end up with a corrupted cache that maps 8-digit cards to the wrong issuer.
Fix:
ALTER TABLE bin_cache ALTER COLUMN bin TYPE VARCHAR(8);
DROP INDEX IF EXISTS idx_bin_cache_bin;
CREATE UNIQUE INDEX idx_bin_cache_bin ON bin_cache (bin);
Regex Patterns
Regex is the second most common landmine. Patterns like ^\d{6}$ appear in input validation, log parsers, ETL pipelines, and API middleware.
# Breaks on 8-digit BINs
BIN_PATTERN = re.compile(r"^\d{6}$")
# Correct pattern that accepts 6 or 8 digits
BIN_PATTERN = re.compile(r"^\d{6}(\d{2})?$")
# Or more explicitly if you want to accept both lengths but nothing else
BIN_PATTERN = re.compile(r"^\d{6}$|^\d{8}$")
In JavaScript, the same problem shows up in form validation and server-side middleware:
// Old
const isValidBin = (bin) => /^\d{6}$/.test(bin);
// Fixed
const isValidBin = (bin) => /^\d{6}(\d{2})?$/.test(bin);
Truncation at the Application Layer
It is common to see code that extracts a BIN by slicing the first 6 characters of a PAN (Primary Account Number).
# Breaks for 8-digit BIN issuers
def extract_bin(pan: str) -> str:
return pan[:6]
# Correct approach: use the issuer's indicated BIN length, or default to 8
def extract_bin(pan: str, bin_length: int = 8) -> str:
return pan[:bin_length]
The challenge here is that you often do not know the BIN length without looking it up first. This creates a bootstrapping problem: you need an 8-digit BIN to look up the correct BIN length, but your code is only giving you 6 digits.
The practical resolution is to default to 8 digits for all new cards while retaining 6-digit support for legacy BIN ranges that have not been migrated.
Analytics and BI Pipelines
Many analytics stacks join transaction data against a BIN reference table using a 6-character key. When 8-digit BINs arrive in transaction data, the join fails silently and those transactions get tagged as "unknown issuer" or dropped entirely.
-- Typical broken join
SELECT
t.amount,
b.country,
b.brand
FROM transactions t
LEFT JOIN bin_reference b
ON LEFT(t.card_number_masked, 6) = b.bin
WHERE t.created_at >= '2024-01-01';
-- Fixed join: try 8-digit match first, fall back to 6
SELECT
t.amount,
COALESCE(b8.country, b6.country) AS country,
COALESCE(b8.brand, b6.brand) AS brand
FROM transactions t
LEFT JOIN bin_reference b8
ON LEFT(t.card_number_masked, 8) = b8.bin
LEFT JOIN bin_reference b6
ON LEFT(t.card_number_masked, 6) = b6.bin
WHERE t.created_at >= '2024-01-01';
Fraud and Risk Models
This is the highest-stakes area. Risk models trained on 6-digit BINs have a feature that is now unreliable.
If your model uses bin_country, bin_issuer_type, or bin_brand as features derived from a 6-digit lookup, and the underlying BIN has been migrated to an 8-digit assignment, your features are now wrong. A Visa debit card issued in Germany might look like a Mastercard credit card issued in the US if the 6-digit prefix belongs to a legacy range that no longer maps correctly.
Mitigation steps:
- Add a
bin_lengthfeature to your model inputs (value: 6 or 8). - Re-extract BIN features from historical transactions using the correct BIN length.
- Monitor feature drift on BIN-derived inputs after the migration.
- If retraining is not immediately possible, add a correction layer that re-maps 8-digit BINs before feature extraction.
6-Digit vs 8-Digit BIN: Side-by-Side Comparison
| Dimension | 6-Digit BIN | 8-Digit BIN |
|---|---|---|
| ISO standard | ISO 7812-1 (pre-2017) | ISO 7812-1:2017 |
| BIN space | ~810,000 combinations | ~81,000,000 combinations |
| Storage type (SQL) | CHAR(6) |
VARCHAR(8) |
| Regex match | ^\d{6}$ |
^\d{6}(\d{2})?$ |
| PAN slice for BIN | pan[:6] |
pan[:8] |
| Lookup API compatibility | Most older APIs | Requires explicit 8-digit support |
| Risk model feature accuracy | Degrading since 2022 | Accurate when API returns correct data |
| Affected card brands | All major networks | All major networks |
| Backward compatibility | Must coexist with 8-digit | Must coexist with 6-digit |
How Do You Migrate Safely?
Safe migration is not a one-day task. It is a phased rollout with parallel processing and validation at each step.
Phase 1: Audit
Before touching any code, find every place in your system that handles BINs.
# Search for hardcoded 6-digit assumptions in a codebase
grep -rn "[:6\]" --include="*.py" .
grep -rn "\[0:6\]" --include="*.py" .
grep -rn "pan\[:6\]" --include="*.py" .
grep -rn "CHAR(6)" --include="*.sql" .
grep -rn "VARCHAR(6)" --include="*.sql" .
grep -rn "\\\\d{6}" --include="*.py" .
Run similar searches for your language of choice. Build a complete inventory before making any changes.
Phase 2: Schema Migration
Expand BIN columns first, before changing application logic. This avoids truncation errors during the transition period.
-- Step 1: Expand column (non-destructive, backward-compatible)
ALTER TABLE bin_cache ALTER COLUMN bin TYPE VARCHAR(8);
ALTER TABLE transactions ALTER COLUMN bin TYPE VARCHAR(8);
-- Step 2: Update indexes after column expansion
REINDEX TABLE bin_cache;
-- Step 3: Add a bin_length column for analytics and debugging
ALTER TABLE bin_cache ADD COLUMN IF NOT EXISTS bin_length SMALLINT;
UPDATE bin_cache SET bin_length = LENGTH(bin);
Do not drop the old 6-digit records yet. You need them as a fallback during the transition.
Phase 3: Application Logic
Update extraction and validation logic to default to 8 digits for new lookups, while retaining 6-digit compatibility for existing records.
class BinExtractor:
DEFAULT_LENGTH = 8
LEGACY_LENGTH = 6
def extract(self, pan: str, legacy: bool = False) -> str:
length = self.LEGACY_LENGTH if legacy else self.DEFAULT_LENGTH
return pan[:length]
def from_transaction(self, pan: str) -> str:
"""
Extract BIN with fallback logic.
Try 8-digit lookup first; fall back to 6-digit if no result.
"""
bin_8 = pan[:8]
bin_6 = pan[:6]
return bin_8 # Resolver handles fallback at lookup time
Phase 4: Use a BIN Lookup API That Supports Both Lengths
The cleanest solution for lookup is to delegate to a service that already handles the 6-to-8 digit transition internally. Hand-maintained BIN tables go stale within weeks as new BINs are issued constantly.
BinSearchLookup is one example of an API that accepts 6 to 8 digit BINs natively. The endpoint accepts a bin parameter of either length and returns issuer, brand, card type, and country without requiring you to know in advance whether you have a 6-digit or 8-digit BIN.
import httpx
def lookup_bin(bin_value: str, api_key: str, user_id: str) -> dict:
"""
Accepts 6 or 8 digit BINs. API resolves the correct issuer data.
"""
response = httpx.get(
"https://www.binsearchlookup.com/lookup",
params={"bin": bin_value},
headers={
"X-API-Key": api_key,
"X-User-ID": user_id
},
timeout=5.0
)
response.raise_for_status()
return response.json()
# Works for both:
result_6 = lookup_bin("412345", api_key, user_id)
result_8 = lookup_bin("41234567", api_key, user_id)
The response includes BIN, Brand, Type, Category, Issuer, CountryName, isoCode2, and isoCode3 fields, which maps cleanly to the feature set most fraud models expect.
For batch processing (up to 50 BINs per request), the API also provides a POST /lookup/batch endpoint, which is useful when re-enriching historical transaction data:
def batch_lookup_bins(bin_list: list[str], api_key: str, user_id: str) -> list[dict]:
response = httpx.post(
"https://www.binsearchlookup.com/lookup/batch",
json={"bins": bin_list},
headers={
"X-API-Key": api_key,
"X-User-ID": user_id,
"Content-Type": "application/json"
},
timeout=10.0
)
response.raise_for_status()
return response.json()
Phase 5: Backfill Historical Data
Once the lookup layer is working, re-enrich historical transaction records with correct 8-digit BIN metadata.
import time
def backfill_bin_metadata(db_cursor, api_key: str, user_id: str, batch_size: int = 50):
"""
Backfill BIN metadata for historical records using 8-digit BINs.
"""
db_cursor.execute(
"SELECT DISTINCT LEFT(card_number_masked, 8) FROM transactions "
"WHERE bin_country IS NULL OR bin_length = 6"
)
bins = [row[0] for row in db_cursor.fetchall()]
for i in range(0, len(bins), batch_size):
batch = bins[i:i + batch_size]
results = batch_lookup_bins(batch, api_key, user_id)
for result in results:
db_cursor.execute(
"""
UPDATE transactions
SET bin_country = %s,
bin_brand = %s,
bin_type = %s,
bin_length = %s
WHERE LEFT(card_number_masked, 8) = %s
""",
(
result["isoCode2"],
result["Brand"],
result["Type"],
len(result["BIN"]),
result["BIN"]
)
)
time.sleep(0.5) # Respect rate limits
Phase 6: Monitor After Deploy
After deploying changes, watch for these signals:
- Increase in
bin_country = NULLrows (indicates unmatched BINs) - Spike in JOIN nulls on BIN reference tables
- Shift in fraud model score distributions on newly issued cards
- API error rate on BIN lookups (especially
400 INVALID_BINresponses)
Migration Checklist
Use this before marking the migration complete.
Schema
- [ ] All BIN columns expanded to
VARCHAR(8)or wider - [ ] Indexes rebuilt after column expansion
- [ ]
bin_lengthcolumn added where useful for analytics - [ ] No
CHAR(6)orVARCHAR(6)BIN columns remain in production schema
Application Logic
- [ ] All
pan[:6]slices replaced withpan[:8]or parameterized length - [ ] All regex patterns updated to accept 6 or 8 digits
- [ ] Input validation updated on all API endpoints that accept BIN as a parameter
- [ ] Logging and monitoring updated to flag unexpected BIN lengths
BIN Lookup
- [ ] BIN lookup service tested with both 6-digit and 8-digit inputs
- [ ] Fallback logic implemented (try 8-digit, fall back to 6-digit for legacy ranges)
- [ ] BIN cache TTL set to 30 days or less (BIN assignments change)
- [ ] Batch backfill job written and tested in staging
Analytics and Risk
- [ ] BIN join logic in analytics queries updated to attempt 8-digit match first
- [ ] Risk model BIN feature extraction updated
- [ ] Historical transaction data backfilled with correct 8-digit BIN metadata
- [ ] Feature drift monitoring added for BIN-derived inputs
Testing
- [ ] Unit tests cover 6-digit BIN inputs
- [ ] Unit tests cover 8-digit BIN inputs
- [ ] Integration tests run against staging with real 8-digit BIN samples
- [ ] Rollback plan documented
FAQ
Q: Do all cards now use 8-digit BINs?
No. The transition is gradual. Many legacy card programs still operate on 6-digit BINs. You must support both formats simultaneously, likely for the next decade or more. The safe default is to attempt an 8-digit lookup and fall back to 6 digits if no result is found.
Q: How do I know if a BIN in my database is a real 8-digit BIN vs a truncated 6-digit BIN stored with zero-padding?
Check the bin_length column if you added one during migration. If you did not, look at the original transaction data. A BIN that was stored as 12345600 is almost certainly a truncated 6-digit BIN. A BIN stored as 12345678 may be either, and requires a live lookup to verify.
Q: Will 6-digit lookups return wrong data for 8-digit BIN cards?
Yes, potentially. If 12345678 is an 8-digit BIN assigned to a German bank, but 123456 is a 6-digit BIN assigned to a US bank, a 6-digit lookup will return US/bank data for a card that is actually German. This directly impacts fraud decisions, routing, and compliance checks.
Q: Is the first 8 digits always the BIN on a 16-digit card?
For networks that have adopted the ISO 7812-1:2017 standard, yes. For networks or issuers that have not yet migrated, the BIN may still be defined by only the first 6 digits. This is why fallback logic matters.
Q: Do American Express cards follow the same 8-digit BIN standard?
Amex uses a 15-digit PAN format. The BIN expansion affects them as well, though Amex has historically used tighter control over their BIN ranges. Verify with your Amex integration documentation for specifics.
Q: How often do BIN assignments change?
Frequently. New issuers, new programs, and BIN re-assignments happen continuously. A static BIN table that was accurate in 2022 is meaningfully out of date today. This is the strongest argument for using a maintained API rather than a self-hosted BIN database.
Q: What is the risk of not migrating?
Significant. Misidentified issuers affect currency routing, fraud scoring, and 3DS authentication flows. Regulatory audits that require accurate issuer identification (for example, interchange fee compliance) will surface incorrect data. The longer you wait, the larger the backfill problem grows as more 8-digit BINs are issued.
Last updated: April 2026. BIN assignment information is subject to ongoing change by card networks.
Top comments (0)