← Back to Blog

BIN Lookup API for Python: Complete Integration Guide

Published: February 5, 2026
Tags: python, tutorial, integration, developer

This guide walks you through integrating BINLookupAPI into your Python application, from creating your account to writing production-ready code with proper error handling.

We’ll teach you how to:
  • 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
  • requests library (for synchronous code)
  • httpx library (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.

Account Setup
  1. 1 Go to app.binlookupapi.com/sign-in
  2. 2 Sign in with your Google account
  3. 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. 1 Navigate to API Keys in the dashboard
  2. 2 Click Create Key
  3. 3 Give it a name (e.g., "Python Development")
  4. 4 Copy the key immediately — it's only shown once

Your API key will look something like: blapi_live_xxxxxxxxxxxxxxxxxxxx

! Security

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}")
* Error Handling Pattern

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
! Availability and Quota

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}")
i Fail Open Strategy

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

Production Checklist
  • 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

Ready to get started? Create your free account and start building today.