Getting Credit Card BIN/IIN Information with Python

There are free (& paid) services that allow you to submit API calls with partial credit card numbers to return information about the credit card, like type (Visa, MC, Amex), the issuing bank, the country & currency where the card was issued, etc. The problem is that the free services either limit what data is returned (only gives the card type in most cases) or throttles the number of requests allowed (no more than 10 per minute).

So with a paid service (at least the one I’m using), the API call requires sending you API key that they provide you with to get past the free limitations. To make matters a little more difficult, the service I am using didn’t have any python-specific documentation & it took a while for me to reverse-engineer their javascript code into something more python friendly.

However, before I even got to the part of using the API key, I thought “Surely there is a python library that would allow me to do this!” And the answer is yes, I found & used card_identifier…but it comes with one giant caveat: there’s no way to pass my API key.

Using card_identifier did make verifying that I was passing at least 6 digits & returning simple JSON-turned-dictionary pretty trivial for initial testing. And while that library was helpful, it really doesn’t (need to) do that much.

Replacing the card_identifier call with requests was again, easy to do. It was getting the API key formatted as a proper HTTP header that proved to be the only challenge.

The Code

It’s worth pointing out that my API key is being stored as an Airflow Variable so that I don’t have to store–and risk exposing–the API key in git and/or allow for the API key to be changed independently of the DAG code.

from airflow.models import Variable
from base64 import b64encode
import requests

binlist_url = "https://lookup.binlist.net/"
binlist_api_key = Variable.get("binlist_api_key")
binlist_auth = b64encode(str.encode(binlist_api_key + ":")).decode("ascii")
binlist_headers = {"Accept-Version": "3", "Authorization": f"Basic {binlist_auth}"}

def to_card_details(first_6_digits):
    """Submit API call to BINlist for card issuer information.
    """
    raw_html = requests.get(f"{binlist_url}{first_6_digits}", headers=binlist_headers)
    return raw_html.json()

What is completely not-obvious (at least not before figuring this out) is that the API key is passed as a username with a null password. First, the API key followed by a colon–to separate the username & passwords–are encoded as BASE64 then decoded at ASCII (since HTTP headers can only contain ASCII). The HTTP headers will always be the same, just the 6+ digits submitted in the URL will be different for each API call. Finally, the .json() method at the end means I get back a dictionary from the returned JSON to pull out the fields I want to use.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.