How to Validate BIC / SWIFT Codes in Python
BIC validation is more than a regex check. Learn the structure, the edge cases, and how to enrich a code with institution name and country in a single API call.
In this guide
- 1. What is a BIC / SWIFT code?
- 2. BIC structure decoded
- 3. The naive approach: regex (and what it misses)
- 4. Why BIC validation is more than format
- 5. The right solution: one API call
- 6. Python example with requests
- 7. Python example — zero dependencies (urllib)
- 8. cURL example
- 9. Understanding the response
- 10. Edge cases to handle
1. What is a BIC / SWIFT code?
A BIC (Bank Identifier Code), also called a SWIFT code, is a unique identifier for financial institutions used in international wire transfers. It is defined by ISO 9362 and maintained by SWIFT (Society for Worldwide Interbank Financial Telecommunication).
BICs appear in SEPA transfers, IBAN structures, correspondent banking, and payment system integrations. When a user enters a BIC in your app — whether during onboarding, a transfer form, or KYC — you need to verify it's a real, correctly formatted code before passing it downstream.
2. BIC structure decoded
A BIC is either 8 characters (without branch) or 11 characters (with branch). Let's decode a real example:
Example: DEUTDEDBBER
| Part | Length | Description | Example |
|---|---|---|---|
| Institution code | 4 letters | Identifies the bank or institution | DEUT |
| Country code | 2 letters | ISO 3166-1 alpha-2 country code | DE |
| Location code | 2 chars | Alphanumeric; passive participants end in 1 | DB |
| Branch code | 3 chars (opt.) | Specific branch. XXX = primary office. Omitted in 8-char BICs. | BER |
DEUTDEDB (8 chars) and DEUTDEDBXXX (11 chars with XXX) both refer to the same primary office of Deutsche Bank in Germany. Both are equally valid.
3. The naive approach: regex (and what it misses)
The first impulse is to write a regex against the ISO 9362 format definition:
import re # ❌ Structurally correct — but misses real-world edge cases BIC_REGEX = re.compile( r'^[A-Z]{4}' # institution (4 letters) r'[A-Z]{2}' # country (2 letters) r'[A-Z0-9]{2}' # location (2 alphanumeric) r'([A-Z0-9]{3})?$' # optional branch (3 alphanumeric) ) def validate_bic_naive(bic: str) -> bool: return bool(BIC_REGEX.match(bic.upper().strip())) print(validate_bic_naive("DEUTDEDB")) # True ✓ print(validate_bic_naive("DEUTDEDBXXX")) # True ✓ print(validate_bic_naive("DEUTDEDBBER")) # True ✓ print(validate_bic_naive("AAAABB11")) # True ✗ — structurally valid but not a real institution print(validate_bic_naive("DEUTDEDE")) # True ✗ — technically valid format, may not exist print(validate_bic_naive("DEUTDE")) # False ✓ — too short
4. Why BIC validation is more than format
Institution lookup
The regex above accepts AAAABBCC— four random letters followed by a valid country and location. To confirm the code maps to a real institution, you need access to the SWIFT BIC directory (or a database derived from it). This is the most common source of user errors: a digit transposed, two letters swapped, or an old BIC that was retired.
8-char vs 11-char handling
Payment systems treat 8-char BICs and their XXX-suffixed equivalents as identical. But some systems expect exactly 11 characters and will reject an 8-char BIC. You often need to normalise the input — either stripping XXX or appending it — depending on your downstream system.
Passive SWIFT participants
If the second character of the location code is 1 (e.g. location code U1), the institution is a passive participant — it receives SWIFT messages but does not initiate them. Some payment workflows must not route outbound transfers to passive participants.
Country enrichment
The two-letter country segment lets you derive the country of the institution automatically — useful for displaying a flag, applying geo-specific fee rules, or SEPA zone checks. But EG could be Egypt, and you need a lookup table, not just a slice of the string.
5. The right solution: one API call
The IsValid BIC API validates format, resolves the institution name, city, and branch, and returns the full country name — all from a single GET request.
Get your free API key at isvalid.dev — 100 calls per day, no credit card required.
Full parameter reference and response schema: BIC Validation API docs →
6. Python example with requests
Using the isvalid-sdk Python SDK or the popular requests library. Install either with pip install isvalid-sdk or pip install requests.
# bic_validator.py import os from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) result = iv.bic("DEUTDEDBBER") if not result["valid"]: print("Invalid BIC") else: bank = result.get("bankName") or result["bankCode"] country = result.get("countryName") or result["countryCode"] city = result.get("city") or "—" branch = result.get("branch") or "primary office" print(f"{bank} · {country} · {city} · {branch}") # → Deutsche Bank · Germany · Berlin · Berlin
In a Django or Flask view you might use it like this:
# views.py (Django / Flask pattern) from django.http import JsonResponse import requests def validate_bank_code(request): bic = request.GET.get("bic", "").strip() if not bic: return JsonResponse({"error": "bic parameter required"}, status=400) try: result = validate_bic(bic) except requests.Timeout: return JsonResponse({"error": "validation service timeout"}, status=502) except requests.HTTPError as exc: return JsonResponse({"error": str(exc)}, status=502) if not result["valid"]: return JsonResponse({"error": "Invalid BIC code"}, status=422) return JsonResponse({ "bic": bic.upper(), "institution": result.get("bankName"), "country": result.get("countryName"), "city": result.get("city"), })
7. Python example — zero dependencies (urllib)
If you can't add third-party packages — Lambda layers, minimal Docker images, or just preference — here's the same logic using only the standard library:
# bic_validator_stdlib.py import json import os import urllib.error import urllib.parse import urllib.request API_KEY = os.environ["ISVALID_API_KEY"] BASE_URL = "https://api.isvalid.dev" def validate_bic(bic: str) -> dict: params = urllib.parse.urlencode({"value": bic}) url = f"{BASE_URL}/v0/bic?{params}" req = urllib.request.Request( url, headers={"Authorization": f"Bearer {API_KEY}"}, ) try: with urllib.request.urlopen(req, timeout=5) as resp: return json.loads(resp.read().decode()) except urllib.error.HTTPError as exc: body = exc.read().decode() raise RuntimeError(f"API error {exc.code}: {body}") from exc # ── Batch validation ────────────────────────────────────────────────────────── def validate_bics(bic_list: list[str]) -> list[dict]: """Validate multiple BICs, returning results in order.""" results = [] for bic in bic_list: try: results.append({"bic": bic, **validate_bic(bic)}) except RuntimeError as exc: results.append({"bic": bic, "valid": False, "error": str(exc)}) return results if __name__ == "__main__": codes = ["DEUTDEDBBER", "BNPAFRPPXXX", "CHASUS33", "INVALID123"] for r in validate_bics(codes): status = "✓" if r.get("valid") else "✗" name = r.get("bankName") or "unknown" print(f" {status} {r['bic']:<15} {name}")
8. cURL example
Deutsche Bank, Berlin branch (11-char BIC):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/bic?value=DEUTDEDBBER"
Same institution, primary office shorthand (8-char BIC):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/bic?value=DEUTDEDB"
Invalid BIC (wrong format):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/bic?value=TOOSHORT"
9. Understanding the response
Response for XASXAU2SRTG (ASX Operations, Sydney):
{ "valid": true, "bankCode": "XASX", "countryCode": "AU", "countryName": "Australia", "locationCode": "2S", "branchCode": "RTG", "bankName": "ASX Operations Pty Limited", "city": "Sydney", "branch": "RTGS Settlement" }
Response for an 8-char BIC with no branch code:
{ "valid": true, "bankCode": "BNPA", "countryCode": "FR", "countryName": "France", "locationCode": "PP", "branchCode": null, "bankName": "BNP Paribas", "city": "Paris", "branch": null }
Response for an invalid code:
{ "valid": false, "bankCode": "TOOS", "countryCode": "HO", "countryName": null, "locationCode": "RT", "branchCode": null, "bankName": null, "city": null, "branch": null }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Format is correct and the code is recognised |
| bankCode | string | 4-letter institution identifier |
| countryCode | string | ISO 3166-1 alpha-2 code (chars 5–6 of BIC) |
| countryName | string | null | Full English country name, or null if unrecognised |
| locationCode | string | 2-char location code; second char 1 = passive participant |
| branchCode | string | null | XXX = primary office; null for 8-char BICs |
| bankName | string | null | Institution name if in directory, else null |
| city | string | null | City of the branch or head office |
| branch | string | null | Branch or department name if available |
10. Edge cases to handle
Normalise 8- and 11-char BICs
Some downstream systems require exactly 11 characters. If your system needs this, append XXX to 8-char BICs after validation. Never pad before validation — let the API accept both forms.
def normalise_bic(bic: str, *, to_11: bool = False) -> str: """Normalise BIC after successful validation.""" bic = bic.upper().strip() if to_11 and len(bic) == 8: return bic + "XXX" if not to_11 and bic.endswith("XXX"): return bic[:-3] return bic
Check for passive participants
If you route outbound transfers, reject passive participants before sending.
def is_passive_participant(result: dict) -> bool: """Passive SWIFT participants have '1' as the second char of locationCode.""" location = result.get("locationCode", "") return len(location) == 2 and location[1] == "1" result = validate_bic("DEUTDEDB") if is_passive_participant(result): raise ValueError("Cannot send outbound transfer to a passive SWIFT participant")
Cache results to reduce API calls
BIC directory data is stable. Cache validated results for 24 hours to avoid redundant calls during high-throughput batch processing.
import functools import time # Simple in-process TTL cache (use Redis for multi-process deployments) _cache: dict[str, tuple[dict, float]] = {} TTL = 86_400 # 24 hours def validate_bic_cached(bic: str) -> dict: key = bic.upper().strip() if key in _cache: result, ts = _cache[key] if time.monotonic() - ts < TTL: return result result = validate_bic(key) _cache[key] = (result, time.monotonic()) return result
Handle unknown institutions gracefully
A BIC can be structurally valid (passes format check) but not found in the institution directory — the API returns valid: true but bankName: null. This can happen with newly issued BICs not yet in the built-in directory. Always fall back to displaying the bankCode if bankName is None.
Summary
Python integration notes
Pydantic V2 makes it straightforward to validate BIC/SWIFT Code as part of a data model. Use @field_validator or the Annotated pattern with AfterValidator to call the IsValid API inside the validator and raise aValueError on failure. The validator runs automatically whenever the model is instantiated — in a FastAPI request body, a SQLModel ORM model, or a standalone Pydantic parse. Initialise the API client as a module-level singleton so it is not re-created on every request.
FastAPI and Django integration
In FastAPI, inject the IsValid client viaDepends() so the samehttpx.AsyncClient connection pool is shared across all concurrent requests. Pair it with async route handlers to keep the event loop non-blocking during BIC/SWIFT Code validation. In Django, implement aclean() method on your model or form field to call the synchronous SDK client; wrap it insync_to_async() if you are using Django Channels or async views.
When processing a batch of BIC/SWIFT Code values — during a CSV import or a nightly reconciliation job — useasyncio.gather() with a shared httpx.AsyncClientand an asyncio.Semaphoreto cap concurrency at the API rate limit. This brings total validation time from O(n) sequential to O(1) bounded by the pool size.
Handle httpx.HTTPStatusError andhttpx.RequestErrorseparately. A 422 from IsValid means the BIC/SWIFT Code is structurally invalid — surface this as a validation error to the user. A 503 or network error means the API is temporarily unavailable — retry with exponential backoff usingtenacity before falling back gracefully.
- Store
ISVALID_API_KEYin a.envfile and load it withpython-dotenvat startup - Use
pytest-asynciowithasyncio_mode = "auto"for testing async validation paths - Type-annotate validated fields with
NewType('BicCode', str)for clarity in function signatures - Apply
.strip()and.upper()before validation — BIC/SWIFT Code values often arrive with stray whitespace or mixed case from user input
See also
Try BIC validation instantly
Free tier includes 100 API calls per day. No credit card required. Institution name, city, and branch lookup included in every response.