← Back to Blog

BIN Lookup API for PHP: Complete Integration Guide

Published: February 5, 2026
Tags: php, tutorial, integration, developer, e-commerce

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

What You'll Build
  • Look up BIN information for any payment card
  • Handle all error cases gracefully
  • Work with both cURL and Guzzle HTTP client
  • Validate cards in a real payment flow

Prerequisites

  • PHP 8.0 or higher
  • cURL extension (built-in for most PHP installations)
  • Guzzle HTTP client (optional, for more advanced usage)

Install Guzzle via Composer (optional):

composer require guzzlehttp/guzzle

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 Development plan which includes 15,000 requests per month with mock responses — perfect for building and testing your integration.

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., "PHP 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 using cURL to verify everything works:

<?php

$apiKey = 'your_api_key_here';
$apiUrl = 'https://api.binlookupapi.com/v1/bin';

function lookupBin(int $binNumber, string $apiKey, string $apiUrl): array
{
    $ch = curl_init($apiUrl);

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $apiKey,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode(['number' => $binNumber]),
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        throw new Exception("API request failed with status $httpCode");
    }

    return json_decode($response, true);
}

// Test it out
$result = lookupBin(42467101, $apiKey, $apiUrl);
print_r($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 with PSR-4 compatible class structure:

<?php

declare(strict_types=1);

namespace App\BINLookup;

/**
 * Base exception for BIN lookup errors.
 */
class BINLookupException extends \Exception
{
}

/**
 * Raised when the BIN format is invalid.
 */
class InvalidBINException extends BINLookupException
{
}

/**
 * Raised when the API key is invalid or missing.
 */
class AuthenticationException extends BINLookupException
{
}

/**
 * Raised when the daily quota has been exceeded.
 */
class QuotaExceededException extends BINLookupException
{
}

/**
 * Raised when the BIN is not in the database.
 */
class BINNotFoundException extends BINLookupException
{
}

/**
 * Raised when the API service encounters an error.
 */
class ServiceException extends BINLookupException
{
}


/**
 * Data class for issuer information.
 */
class Issuer
{
    public function __construct(
        public readonly ?string $name,
        public readonly ?string $website,
        public readonly ?string $phone
    ) {
    }
}

/**
 * Data class for country information.
 */
class Country
{
    public function __construct(
        public readonly string $code,
        public readonly string $name
    ) {
    }
}

/**
 * Data class for BIN information.
 */
class BINInfo
{
    public function __construct(
        public readonly string $bin,
        public readonly string $scheme,
        public readonly string $funding,
        public readonly ?string $brand,
        public readonly ?string $category,
        public readonly Country $country,
        public readonly Issuer $issuer,
        public readonly ?string $currency,
        public readonly bool $prepaid,
        public readonly bool $commercial
    ) {
    }
}


/**
 * Client for the BINLookupAPI service.
 */
class BINLookupClient
{
    private const BASE_URL = 'https://api.binlookupapi.com/v1/bin';
    private const TIMEOUT = 10;

    private string $apiKey;

    /**
     * Initialize the client.
     *
     * @param string|null $apiKey Your BINLookupAPI key. If not provided,
     *                            reads from BINLOOKUP_API_KEY environment variable.
     * @throws \InvalidArgumentException If no API key is provided or found.
     */
    public function __construct(?string $apiKey = null)
    {
        $this->apiKey = $apiKey ?? getenv('BINLOOKUP_API_KEY') ?: ($_ENV['BINLOOKUP_API_KEY'] ?? '');

        if (empty($this->apiKey)) {
            throw new \InvalidArgumentException(
                'API key required. Pass it directly or set BINLOOKUP_API_KEY environment variable.'
            );
        }
    }

    /**
     * Look up information for a BIN.
     *
     * @param int $binNumber The BIN to look up (4-8 digits).
     * @return BINInfo BINInfo object with card details.
     * @throws InvalidBINException If the BIN format is invalid.
     * @throws AuthenticationException If the API key is invalid.
     * @throws QuotaExceededException If the daily quota is exceeded.
     * @throws BINNotFoundException If the BIN is not found.
     * @throws ServiceException If the API service has an error.
     */
    public function lookup(int $binNumber): BINInfo
    {
        // Validate input
        if ($binNumber < 1000 || $binNumber > 99999999) {
            throw new InvalidBINException(
                "BIN must be an integer between 1000 and 99999999, got: $binNumber"
            );
        }

        $ch = curl_init(self::BASE_URL);

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_TIMEOUT => self::TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => self::TIMEOUT,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $this->apiKey,
                'Content-Type: application/json',
            ],
            CURLOPT_POSTFIELDS => json_encode(['number' => $binNumber]),
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        $curlErrno = curl_errno($ch);
        curl_close($ch);

        // Handle cURL errors
        if ($curlErrno !== 0) {
            if ($curlErrno === CURLE_OPERATION_TIMEDOUT) {
                throw new ServiceException('Request timed out. Please try again.');
            }
            throw new ServiceException("Could not connect to API: $curlError");
        }

        // Handle error responses
        $responseData = json_decode($response, true);

        switch ($httpCode) {
            case 200:
                break; // Success, continue processing

            case 400:
                throw new InvalidBINException(
                    $responseData['message'] ?? 'Invalid BIN'
                );

            case 401:
                throw new AuthenticationException(
                    'Invalid API key. Check your credentials.'
                );

            case 402:
                throw new AuthenticationException(
                    'No active subscription. Please subscribe to a plan.'
                );

            case 403:
                throw new AuthenticationException(
                    'API key lacks required permissions.'
                );

            case 404:
                throw new BINNotFoundException(
                    "BIN $binNumber not found in database."
                );

            case 429:
                throw new QuotaExceededException(
                    'Daily quota exceeded. Resets at midnight UTC.'
                );

            case 502:
                throw new ServiceException(
                    'API gateway error. Please try again later.'
                );

            default:
                if ($httpCode >= 500) {
                    throw new ServiceException(
                        "API service error (HTTP $httpCode). Please try again later."
                    );
                }
                throw new BINLookupException(
                    "Unexpected error: HTTP $httpCode"
                );
        }

        // Parse successful response
        $data = $responseData['data'];

        return new BINInfo(
            bin: $data['bin'],
            scheme: $data['scheme'],
            funding: $data['funding'],
            brand: $data['brand'] ?? null,
            category: $data['category'] ?? null,
            country: new Country(
                code: $data['country']['code'],
                name: $data['country']['name']
            ),
            issuer: new Issuer(
                name: $data['issuer']['name'] ?? null,
                website: $data['issuer']['website'] ?? null,
                phone: $data['issuer']['phone'] ?? null
            ),
            currency: $data['currency'] ?? null,
            prepaid: $data['prepaid'],
            commercial: $data['commercial']
        );
    }
}

Step 5: Using the Client

Here’s how to use the production-ready client:

<?php

use App\BINLookup\BINLookupClient;
use App\BINLookup\InvalidBINException;
use App\BINLookup\AuthenticationException;
use App\BINLookup\QuotaExceededException;
use App\BINLookup\BINNotFoundException;
use App\BINLookup\ServiceException;
use App\BINLookup\BINLookupException;

// Initialize with API key from environment
$client = new BINLookupClient();

// Or pass the key directly
$client = new BINLookupClient('your_api_key_here');

// Look up a BIN
try {
    $info = $client->lookup(42467101);

    echo "Card Network: {$info->scheme}\n";
    echo "Card Type: {$info->funding}\n";
    echo "Issuer: {$info->issuer->name}\n";
    echo "Country: {$info->country->name}\n";
    echo "Is Prepaid: " . ($info->prepaid ? 'Yes' : 'No') . "\n";

} catch (InvalidBINException $e) {
    echo "Invalid BIN: {$e->getMessage()}\n";

} catch (AuthenticationException $e) {
    echo "Auth error: {$e->getMessage()}\n";
    // Check your API key

} catch (QuotaExceededException $e) {
    echo "Quota exceeded: {$e->getMessage()}\n";
    // Wait until midnight UTC or upgrade your plan

} catch (BINNotFoundException $e) {
    echo "BIN not found: {$e->getMessage()}\n";
    // This BIN isn't in the database

} catch (ServiceException $e) {
    echo "Service error: {$e->getMessage()}\n";
    // Retry with exponential backoff

} catch (BINLookupException $e) {
    echo "Unexpected error: {$e->getMessage()}\n";
}
* Error Handling Pattern

The exception hierarchy lets you catch specific errors or use the base BINLookupException to handle all API-related errors at once.

Step 6: Guzzle HTTP Client (Optional)

For applications already using Guzzle, here’s an alternative implementation:

composer require guzzlehttp/guzzle
<?php

declare(strict_types=1);

namespace App\BINLookup;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\RequestOptions;

/**
 * Guzzle-based client for BINLookupAPI.
 */
class GuzzleBINLookupClient
{
    private const BASE_URL = 'https://api.binlookupapi.com/v1/bin';

    private Client $client;

    public function __construct(?string $apiKey = null)
    {
        $apiKey = $apiKey ?? getenv('BINLOOKUP_API_KEY') ?: ($_ENV['BINLOOKUP_API_KEY'] ?? '');

        if (empty($apiKey)) {
            throw new \InvalidArgumentException(
                'API key required. Pass it directly or set BINLOOKUP_API_KEY environment variable.'
            );
        }

        $this->client = new Client([
            'base_uri' => self::BASE_URL,
            'timeout' => 10.0,
            'headers' => [
                'Authorization' => 'Bearer ' . $apiKey,
                'Content-Type' => 'application/json',
            ],
        ]);
    }

    public function lookup(int $binNumber): BINInfo
    {
        if ($binNumber < 1000 || $binNumber > 99999999) {
            throw new InvalidBINException(
                "BIN must be an integer between 1000 and 99999999, got: $binNumber"
            );
        }

        try {
            $response = $this->client->post(self::BASE_URL, [
                RequestOptions::JSON => ['number' => $binNumber],
            ]);
        } catch (ConnectException $e) {
            throw new ServiceException('Could not connect to API. Check your network.');
        } catch (RequestException $e) {
            $this->handleRequestException($e, $binNumber);
        }

        $responseData = json_decode($response->getBody()->getContents(), true);
        $data = $responseData['data'];

        return new BINInfo(
            bin: $data['bin'],
            scheme: $data['scheme'],
            funding: $data['funding'],
            brand: $data['brand'] ?? null,
            category: $data['category'] ?? null,
            country: new Country(
                code: $data['country']['code'],
                name: $data['country']['name']
            ),
            issuer: new Issuer(
                name: $data['issuer']['name'] ?? null,
                website: $data['issuer']['website'] ?? null,
                phone: $data['issuer']['phone'] ?? null
            ),
            currency: $data['currency'] ?? null,
            prepaid: $data['prepaid'],
            commercial: $data['commercial']
        );
    }

    private function handleRequestException(RequestException $e, int $binNumber): never
    {
        $response = $e->getResponse();

        if ($response === null) {
            throw new ServiceException('Request failed: ' . $e->getMessage());
        }

        $httpCode = $response->getStatusCode();
        $body = json_decode($response->getBody()->getContents(), true);

        match ($httpCode) {
            400 => throw new InvalidBINException($body['message'] ?? 'Invalid BIN'),
            401 => throw new AuthenticationException('Invalid API key. Check your credentials.'),
            402 => throw new AuthenticationException('No active subscription. Please subscribe to a plan.'),
            403 => throw new AuthenticationException('API key lacks required permissions.'),
            404 => throw new BINNotFoundException("BIN $binNumber not found in database."),
            429 => throw new QuotaExceededException('Daily quota exceeded. Resets at midnight UTC.'),
            502 => throw new ServiceException('API gateway error. Please try again later.'),
            default => $httpCode >= 500
                ? throw new ServiceException("API service error (HTTP $httpCode). Please try again later.")
                : throw new BINLookupException("Unexpected error: HTTP $httpCode")
        };
    }

    /**
     * Look up multiple BINs with concurrent requests.
     *
     * @param int[] $binNumbers Array of BIN numbers to look up.
     * @return array<int, BINInfo|BINLookupException> Results indexed by BIN number.
     */
    public function lookupMany(array $binNumbers): array
    {
        $promises = [];

        foreach ($binNumbers as $binNumber) {
            $promises[$binNumber] = $this->client->postAsync(self::BASE_URL, [
                RequestOptions::JSON => ['number' => $binNumber],
            ]);
        }

        $results = [];

        foreach ($promises as $binNumber => $promise) {
            try {
                $response = $promise->wait();
                $responseData = json_decode($response->getBody()->getContents(), true);
                $data = $responseData['data'];

                $results[$binNumber] = new BINInfo(
                    bin: $data['bin'],
                    scheme: $data['scheme'],
                    funding: $data['funding'],
                    brand: $data['brand'] ?? null,
                    category: $data['category'] ?? null,
                    country: new Country(
                        code: $data['country']['code'],
                        name: $data['country']['name']
                    ),
                    issuer: new Issuer(
                        name: $data['issuer']['name'] ?? null,
                        website: $data['issuer']['website'] ?? null,
                        phone: $data['issuer']['phone'] ?? null
                    ),
                    currency: $data['currency'] ?? null,
                    prepaid: $data['prepaid'],
                    commercial: $data['commercial']
                );
            } catch (\Exception $e) {
                $results[$binNumber] = new BINLookupException($e->getMessage());
            }
        }

        return $results;
    }
}


// Usage
$client = new GuzzleBINLookupClient('your_api_key');

// Single lookup
$info = $client->lookup(42467101);
echo "Scheme: {$info->scheme}\n";

// Multiple lookups in parallel
$bins = [42467101, 51234567, 37123456];
$results = $client->lookupMany($bins);

foreach ($results as $bin => $result) {
    if ($result instanceof BINInfo) {
        echo "$bin: {$result->scheme}\n";
    } else {
        echo "$bin: Error - {$result->getMessage()}\n";
    }
}

Step 7: Real-World Example — Payment Form Validation

Here’s a practical example of using BIN lookup in a payment flow:

<?php

declare(strict_types=1);

namespace App\Payment;

use App\BINLookup\BINLookupClient;
use App\BINLookup\BINNotFoundException;
use App\BINLookup\QuotaExceededException;
use App\BINLookup\BINLookupException;

class CardValidationResult
{
    public function __construct(
        public readonly bool $valid,
        public readonly ?string $cardType = null,
        public readonly ?string $issuer = null,
        public readonly ?string $country = null,
        public readonly bool $isPrepaid = false,
        public readonly ?array $warnings = null,
        public readonly ?string $error = null
    ) {
    }
}


class PaymentValidator
{
    public function __construct(
        private BINLookupClient $client
    ) {
    }

    /**
     * Validate a card BIN for payment processing.
     *
     * @param string $cardNumber The card number (only first 6-8 digits are used).
     * @param bool $blockPrepaid If true, reject prepaid cards.
     * @param array|null $allowedCountries List of allowed country codes (e.g., ['US', 'GB']).
     * @return CardValidationResult Validation result with details.
     */
    public function validateCardBin(
        string $cardNumber,
        bool $blockPrepaid = false,
        ?array $allowedCountries = null
    ): CardValidationResult {
        $warnings = [];

        // Extract BIN (first 8 digits)
        $digitsOnly = preg_replace('/\D/', '', $cardNumber);

        if (strlen($digitsOnly) < 6) {
            return new CardValidationResult(
                valid: false,
                error: 'Card number must be at least 6 digits'
            );
        }

        $binNumber = (int) substr($digitsOnly, 0, 8);

        try {
            $info = $this->client->lookup($binNumber);
        } catch (BINNotFoundException) {
            return new CardValidationResult(
                valid: false,
                error: 'Unable to identify card. Please check the number.'
            );
        } catch (QuotaExceededException) {
            // Fail open - allow the transaction but log the issue
            return new CardValidationResult(
                valid: true,
                warnings: ['BIN validation skipped due to quota limits']
            );
        } catch (BINLookupException $e) {
            // Fail open for service errors
            return new CardValidationResult(
                valid: true,
                warnings: ["BIN validation unavailable: {$e->getMessage()}"]
            );
        }

        // Check prepaid status
        if ($blockPrepaid && $info->prepaid) {
            return new CardValidationResult(
                valid: false,
                cardType: $info->scheme,
                issuer: $info->issuer->name,
                country: $info->country->code,
                isPrepaid: true,
                error: 'Prepaid cards are not accepted'
            );
        }

        // Check country restrictions
        if ($allowedCountries !== null && !in_array($info->country->code, $allowedCountries, true)) {
            return new CardValidationResult(
                valid: false,
                cardType: $info->scheme,
                issuer: $info->issuer->name,
                country: $info->country->code,
                error: "Cards from {$info->country->name} are not accepted"
            );
        }

        // Add warnings for high-risk indicators
        if ($info->prepaid) {
            $warnings[] = 'This is a prepaid card';
        }

        if ($info->commercial) {
            $warnings[] = 'This is a corporate/business card';
        }

        return new CardValidationResult(
            valid: true,
            cardType: $info->scheme,
            issuer: $info->issuer->name,
            country: $info->country->code,
            isPrepaid: $info->prepaid,
            warnings: !empty($warnings) ? $warnings : null
        );
    }
}


// Usage example
$client = new BINLookupClient();
$validator = new PaymentValidator($client);

$result = $validator->validateCardBin(
    cardNumber: '4246710012345678',
    blockPrepaid: true,
    allowedCountries: ['US', 'GB', 'CA', 'AU']
);

if ($result->valid) {
    echo "Card accepted: {$result->cardType} from {$result->issuer}\n";
    if ($result->warnings) {
        foreach ($result->warnings as $warning) {
            echo "Warning: $warning\n";
        }
    }
} else {
    echo "Card rejected: {$result->error}\n";
}
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
  • 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.