BIN Lookup API for Python: Complete Integration Guide
This guide walks you through integrating BINLookupAPI into your Python application, from creating your account to writing production-ready code with proper error handling.
- ✓ Look up BIN information for any payment card
- ✓ Handle all error cases gracefully
- ✓ Look up thousands of BINs at once with the batch endpoint
- ✓ Work with both synchronous and asynchronous code
- ✓ Validate cards in a real payment flow
Prerequisites
- Python 3.8 or higher
requestslibrary (for synchronous code)httpxlibrary (optional, for async code)
Install the required packages:
pip install requests
Step 1: Create Your Account
First, you’ll need a BINLookupAPI account to get your API key.
- 1 Go to app.binlookupapi.com/sign-in
- 2 Sign in with your Google account
- 3 An organisation is created automatically for you
You’ll start on the free tier which includes 10 real lookups per day — no credit card required, so you can try the API before subscribing.
Step 2: Create an API Key
Once logged in:
- 1 Navigate to API Keys in the dashboard
- 2 Click Create Key
- 3 Give it a name (e.g., "Python Development")
- 4 Copy the key immediately — it's only shown once
Your API key will look something like: blapi_live_xxxxxxxxxxxxxxxxxxxx
Store your API key securely. Never commit API keys to version control.
Step 3: Your First BIN Lookup
Let’s start with a simple example to verify everything works:
import requests
API_KEY = "your_api_key_here"
API_URL = "https://api.binlookupapi.com/v1/bin"
def lookup_bin(bin_number: int) -> dict:
"""Look up a BIN and return card information."""
response = requests.post(
API_URL,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
},
json={"number": bin_number}
)
response.raise_for_status()
return response.json()
# Test it out
result = lookup_bin(42467101)
print(result)
Expected response:
{
"data": {
"bin": "42467101",
"scheme": "visa",
"funding": "debit",
"brand": "VISA",
"category": "CLASSIC",
"country": {
"code": "PL",
"name": "POLAND"
},
"issuer": {
"name": "ING BANK SLASKI SA",
"website": null,
"phone": null
},
"currency": "PLN",
"prepaid": false,
"commercial": false
}
}
Step 4: Production-Ready Code with Error Handling
The basic example above doesn’t handle errors properly. Here’s a production-ready implementation:
import requests
from typing import Optional
from dataclasses import dataclass
from enum import Enum
import os
class BINLookupError(Exception):
"""Base exception for BIN lookup errors."""
pass
class InvalidBINError(BINLookupError):
"""Raised when the BIN format is invalid."""
pass
class AuthenticationError(BINLookupError):
"""Raised when the API key is invalid or missing."""
pass
class QuotaExceededError(BINLookupError):
"""Raised when the daily quota has been exceeded."""
pass
class BINNotFoundError(BINLookupError):
"""Raised when the BIN is not in the database."""
pass
class ServiceError(BINLookupError):
"""Raised when the API service encounters an error."""
pass
@dataclass
class Issuer:
name: Optional[str]
website: Optional[str]
phone: Optional[str]
@dataclass
class Country:
code: str
name: str
@dataclass
class BINInfo:
bin: str
scheme: str
funding: str
brand: Optional[str]
category: Optional[str]
country: Country
issuer: Issuer
currency: Optional[str]
prepaid: bool
commercial: bool
class BINLookupClient:
"""Client for the BINLookupAPI service."""
BASE_URL = "https://api.binlookupapi.com/v1/bin"
def __init__(self, api_key: Optional[str] = None):
"""
Initialize the client.
Args:
api_key: Your BINLookupAPI key. If not provided,
reads from BINLOOKUP_API_KEY environment variable.
"""
self.api_key = api_key or os.environ.get("BINLOOKUP_API_KEY")
if not self.api_key:
raise ValueError(
"API key required. Pass it directly or set "
"BINLOOKUP_API_KEY environment variable."
)
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
})
def lookup(self, bin_number: int) -> BINInfo:
"""
Look up information for a BIN.
Args:
bin_number: The BIN to look up (6-11 digits).
Returns:
BINInfo object with card details.
Raises:
InvalidBINError: If the BIN format is invalid.
AuthenticationError: If the API key is invalid.
QuotaExceededError: If the daily quota is exceeded.
BINNotFoundError: If the BIN is not found.
ServiceError: If the API service has an error.
"""
# Validate input
if not isinstance(bin_number, int) or not (100000 <= bin_number <= 99999999999):
raise InvalidBINError(
f"BIN must be an integer between 100000 and 99999999999, "
f"got: {bin_number}"
)
try:
response = self.session.post(
self.BASE_URL,
json={"number": bin_number},
timeout=10
)
except requests.exceptions.Timeout:
raise ServiceError("Request timed out. Please try again.")
except requests.exceptions.ConnectionError:
raise ServiceError("Could not connect to API. Check your network.")
# Handle error responses
if response.status_code == 400:
raise InvalidBINError(response.json().get("message", "Invalid BIN"))
elif response.status_code == 401:
raise AuthenticationError(
"Invalid API key. Check your credentials."
)
elif response.status_code == 403:
raise AuthenticationError(
"API key lacks required permissions."
)
elif response.status_code == 429:
raise QuotaExceededError(
"Daily quota exceeded. Resets at midnight UTC."
)
elif response.status_code == 404:
raise BINNotFoundError(f"BIN {bin_number} not found in database.")
elif response.status_code >= 500:
raise ServiceError(
f"API service error (HTTP {response.status_code}). "
"Please try again later."
)
elif not response.ok:
raise BINLookupError(
f"Unexpected error: HTTP {response.status_code}"
)
# Parse successful response
data = response.json()["data"]
return BINInfo(
bin=data["bin"],
scheme=data["scheme"],
funding=data["funding"],
brand=data.get("brand"),
category=data.get("category"),
country=Country(
code=data["country"]["code"],
name=data["country"]["name"]
),
issuer=Issuer(
name=data["issuer"].get("name"),
website=data["issuer"].get("website"),
phone=data["issuer"].get("phone")
),
currency=data.get("currency"),
prepaid=data["prepaid"],
commercial=data["commercial"]
)
def get_quota_info(self) -> dict:
"""
Get current quota information from the last request.
Returns:
Dict with quota_limit, quota_remaining, and quota_reset.
"""
# Make a test request to get headers
try:
response = self.session.post(
self.BASE_URL,
json={"number": 42467101},
timeout=10
)
return {
"quota_limit": int(response.headers.get("X-Quota-Limit", 0)),
"quota_remaining": int(response.headers.get("X-Quota-Remaining", 0)),
"quota_reset": int(response.headers.get("X-Quota-Reset", 0))
}
except Exception:
return {}
Step 5: Using the Client
Here’s how to use the production-ready client:
# Initialize with API key from environment
client = BINLookupClient()
# Or pass the key directly
client = BINLookupClient(api_key="your_api_key_here")
# Look up a BIN
try:
info = client.lookup(42467101)
print(f"Card Network: {info.scheme}")
print(f"Card Type: {info.funding}")
print(f"Issuer: {info.issuer.name}")
print(f"Country: {info.country.name}")
print(f"Is Prepaid: {info.prepaid}")
except InvalidBINError as e:
print(f"Invalid BIN: {e}")
except AuthenticationError as e:
print(f"Auth error: {e}")
# Check your API key
except QuotaExceededError as e:
print(f"Quota exceeded: {e}")
# Wait until midnight UTC or upgrade your plan
except BINNotFoundError as e:
print(f"BIN not found: {e}")
# This BIN isn't in the database
except ServiceError as e:
print(f"Service error: {e}")
# Retry with exponential backoff
except BINLookupError as e:
print(f"Unexpected error: {e}")
The exception hierarchy lets you catch specific errors or use the base BINLookupError to handle all API-related errors at once.
Step 6: Batch Lookups
If you need to enrich a list of BINs — a transaction export, a fraud review queue, a data migration — don’t loop over the single-lookup endpoint. The API has a native batch endpoint: POST https://api.binlookupapi.com/v1/bins accepts 1 to 2,000 BINs per request. Each entry follows the same rules as a single lookup (6–11 digits, integer or digit string):
{"numbers": [400062, "400597", 99999999]}
The response contains one result per submitted number, in the same order. Per-item failures don’t affect the rest of the batch — an unknown or invalid BIN comes back as an error object while every other entry resolves normally:
{
"mode": "live",
"results": [
{
"number": 400062,
"data": {
"bin": "400062",
"scheme": "visa",
"funding": "debit",
"country": { "code": "AE", "name": "UNITED ARAB EMIRATES" },
"issuer": { "name": "EMIRATES NBD BANK (P.J.S.C.)" }
},
"meta": { "longer_bin_available": true }
},
{
"number": "400597",
"data": {
"bin": "400597",
"scheme": "visa",
"funding": "credit",
"category": "BUSINESS",
"country": { "code": "US", "name": "UNITED STATES" },
"issuer": { "name": "FIRST ELECTRONIC BANK" },
"commercial": true
},
"meta": { "longer_bin_available": true }
},
{
"number": 99999999,
"error": "NOT_FOUND",
"message": "BIN not found"
}
]
}
The whole request only fails for request-level problems: malformed JSON, a missing or oversized numbers array, authentication, or quota. One safety detail worth knowing: any value longer than 11 digits (a possible full card number) comes back as "[REDACTED]" with a per-item BAD_REQUEST and is never stored in request logs.
Let’s extend BINLookupClient from Step 4 with a lookup_batch method that chunks oversized lists and returns one entry per input — either a BINInfo or a BatchItemFailure:
@dataclass
class BatchItemFailure:
"""A single failed item in a batch lookup."""
number: object # as submitted ("[REDACTED]" if it looked like a card number)
error: str # "NOT_FOUND" or "BAD_REQUEST"
message: str
class BINLookupClient:
# ... everything from Step 4, plus:
BATCH_URL = "https://api.binlookupapi.com/v1/bins"
MAX_BATCH_SIZE = 2000
@staticmethod
def _chunked(items: list, size: int):
"""Split a list into chunks of at most `size` items."""
for i in range(0, len(items), size):
yield items[i:i + size]
def lookup_batch(
self, numbers: list[int | str]
) -> list["BINInfo | BatchItemFailure"]:
"""
Look up multiple BINs in batches of up to 2,000.
Args:
numbers: BINs to look up (6-11 digits each, int or digit string).
Returns:
One entry per input, in the same order: BINInfo on success,
BatchItemFailure for items that were invalid or not found.
Raises:
The same exceptions as lookup(), but only for request-level
problems (bad request body, auth, quota, service errors).
"""
results: list[BINInfo | BatchItemFailure] = []
for chunk in self._chunked(numbers, self.MAX_BATCH_SIZE):
try:
response = self.session.post(
self.BATCH_URL,
json={"numbers": chunk},
timeout=30
)
except requests.exceptions.Timeout:
raise ServiceError("Request timed out. Please try again.")
except requests.exceptions.ConnectionError:
raise ServiceError("Could not connect to API. Check your network.")
if response.status_code == 400:
raise InvalidBINError(
response.json().get("message", "Invalid batch request")
)
elif response.status_code in (401, 403):
raise AuthenticationError(
"Invalid API key or plan without batch access."
)
elif response.status_code == 429:
remaining = response.headers.get("X-Quota-Remaining")
raise QuotaExceededError(
f"Batch exceeds remaining daily quota "
f"({remaining} lookups left). Nothing was consumed."
)
elif response.status_code >= 500:
raise ServiceError(
f"API service error (HTTP {response.status_code}). "
"Please try again later."
)
elif not response.ok:
raise BINLookupError(
f"Unexpected error: HTTP {response.status_code}"
)
for item in response.json()["results"]:
if "error" in item:
results.append(BatchItemFailure(
number=item["number"],
error=item["error"],
message=item["message"]
))
else:
data = item["data"]
results.append(BINInfo(
bin=data["bin"],
scheme=data["scheme"],
funding=data["funding"],
brand=data.get("brand"),
category=data.get("category"),
country=Country(
code=data["country"]["code"],
name=data["country"]["name"]
),
issuer=Issuer(
name=data["issuer"].get("name"),
website=data["issuer"].get("website"),
phone=data["issuer"].get("phone")
),
currency=data.get("currency"),
prepaid=data["prepaid"],
commercial=data["commercial"]
))
return results
Using it:
client = BINLookupClient()
results = client.lookup_batch([400062, "400597", 99999999])
for result in results:
if isinstance(result, BatchItemFailure):
print(f"{result.number}: {result.error} — {result.message}")
else:
print(f"{result.bin}: {result.scheme} {result.funding}, "
f"{result.issuer.name} ({result.country.code})")
# 400062: visa debit, EMIRATES NBD BANK (P.J.S.C.) (AE)
# 400597: visa credit, FIRST ELECTRONIC BANK (US)
# 99999999: NOT_FOUND — BIN not found
Batch lookups are available on the BLAPI-500KPM plan ($100/mo). Test-mode organisations always have access, so you can build against the endpoint for free with mock data (500 lookups/day). Every submitted number counts as one lookup against your quota — including invalid and unknown ones. If a batch is larger than your remaining daily quota, the request returns 429, consumes nothing, and the X-Quota-Remaining header reports your actual remaining quota.
See the batch lookups documentation for the full endpoint reference.
Step 7: Async Support (Optional)
If your plan doesn’t include batch lookups, or you need non-blocking single lookups inside an async application, here’s an async version using httpx:
pip install httpx
import httpx
from typing import Optional
import asyncio
class AsyncBINLookupClient:
"""Async client for BINLookupAPI."""
BASE_URL = "https://api.binlookupapi.com/v1/bin"
def __init__(self, api_key: str):
self.api_key = api_key
self.client = httpx.AsyncClient(
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
},
timeout=10.0
)
async def lookup(self, bin_number: int) -> BINInfo:
"""Async BIN lookup."""
response = await self.client.post(
self.BASE_URL,
json={"number": bin_number}
)
if response.status_code == 401:
raise AuthenticationError("Invalid API key")
elif response.status_code == 404:
raise BINNotFoundError(f"BIN {bin_number} not found")
response.raise_for_status()
data = response.json()["data"]
return BINInfo(
bin=data["bin"],
scheme=data["scheme"],
funding=data["funding"],
brand=data.get("brand"),
category=data.get("category"),
country=Country(
code=data["country"]["code"],
name=data["country"]["name"]
),
issuer=Issuer(
name=data["issuer"].get("name"),
website=data["issuer"].get("website"),
phone=data["issuer"].get("phone")
),
currency=data.get("currency"),
prepaid=data["prepaid"],
commercial=data["commercial"]
)
async def lookup_many(self, bin_numbers: list[int]) -> list[BINInfo]:
"""
Look up multiple BINs concurrently.
Fallback for plans without the batch endpoint — prefer
lookup_batch from Step 6 if your plan includes it.
"""
tasks = [self.lookup(bin) for bin in bin_numbers]
return await asyncio.gather(*tasks, return_exceptions=True)
async def close(self):
"""Close the client connection."""
await self.client.aclose()
# Usage
async def main():
client = AsyncBINLookupClient("your_api_key")
try:
# Single lookup
info = await client.lookup(42467101)
print(f"Scheme: {info.scheme}")
# Multiple lookups in parallel (batch-endpoint fallback)
bins = [42467101, 51234567, 37123456]
results = await client.lookup_many(bins)
for result in results:
if isinstance(result, BINInfo):
print(f"{result.bin}: {result.scheme}")
else:
print(f"Error: {result}")
finally:
await client.close()
asyncio.run(main())
Step 8: Real-World Example — Payment Form Validation
Here’s a practical example of using BIN lookup in a payment flow:
from dataclasses import dataclass
from typing import Optional
@dataclass
class CardValidationResult:
valid: bool
card_type: Optional[str] = None
issuer: Optional[str] = None
country: Optional[str] = None
is_prepaid: bool = False
warnings: list[str] = None
error: Optional[str] = None
def validate_card_bin(
card_number: str,
client: BINLookupClient,
block_prepaid: bool = False,
allowed_countries: Optional[list[str]] = None
) -> CardValidationResult:
"""
Validate a card BIN for payment processing.
Args:
card_number: The card number (only first 6-8 digits are used)
client: BINLookupClient instance
block_prepaid: If True, reject prepaid cards
allowed_countries: List of allowed country codes (e.g., ['US', 'GB'])
Returns:
CardValidationResult with validation details
"""
warnings = []
# Extract BIN (first 8 digits)
digits_only = ''.join(filter(str.isdigit, card_number))
if len(digits_only) < 6:
return CardValidationResult(
valid=False,
error="Card number must be at least 6 digits"
)
bin_number = int(digits_only[:8])
try:
info = client.lookup(bin_number)
except BINNotFoundError:
return CardValidationResult(
valid=False,
error="Unable to identify card. Please check the number."
)
except QuotaExceededError:
# Fail open - allow the transaction but log the issue
return CardValidationResult(
valid=True,
warnings=["BIN validation skipped due to quota limits"]
)
except BINLookupError as e:
# Fail open for service errors
return CardValidationResult(
valid=True,
warnings=[f"BIN validation unavailable: {e}"]
)
# Check prepaid status
if block_prepaid and info.prepaid:
return CardValidationResult(
valid=False,
card_type=info.scheme,
issuer=info.issuer.name,
country=info.country.code,
is_prepaid=True,
error="Prepaid cards are not accepted"
)
# Check country restrictions
if allowed_countries and info.country.code not in allowed_countries:
return CardValidationResult(
valid=False,
card_type=info.scheme,
issuer=info.issuer.name,
country=info.country.code,
error=f"Cards from {info.country.name} are not accepted"
)
# Add warnings for high-risk indicators
if info.prepaid:
warnings.append("This is a prepaid card")
if info.commercial:
warnings.append("This is a corporate/business card")
return CardValidationResult(
valid=True,
card_type=info.scheme,
issuer=info.issuer.name,
country=info.country.code,
is_prepaid=info.prepaid,
warnings=warnings if warnings else None
)
# Usage example
client = BINLookupClient()
result = validate_card_bin(
card_number="4246710012345678",
client=client,
block_prepaid=True,
allowed_countries=["US", "GB", "CA", "AU"]
)
if result.valid:
print(f"Card accepted: {result.card_type} from {result.issuer}")
if result.warnings:
for warning in result.warnings:
print(f"Warning: {warning}")
else:
print(f"Card rejected: {result.error}")
For payment validation, consider “failing open” when the API is unavailable. It’s usually better to allow a transaction and verify later than to block all payments during an outage.
Best Practices Summary
- ✓ Store API key in BINLOOKUP_API_KEY environment variable
- ✓ Handle all error types: network, auth, quota, not-found
- ✓ Set request timeouts (10 seconds recommended)
- ✓ Implement retry logic with exponential backoff for transient errors
- ✓ Use the batch endpoint for bulk lookups instead of looping single requests
- ✓ Consider fail-open strategy for non-critical validation
- ✓ Monitor your quota usage via response headers
Next Steps
- View the full API Reference for all available fields and error codes
- Explore pricing plans to find the right quota for your needs
- Contact support if you need help with your integration
Ready to get started? Create your free account and start building today.