BIN Lookup API for PHP: Complete Integration Guide
This guide walks you through integrating BINLookupAPI into your PHP 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
- ✓ 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.
- 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 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 Navigate to API Keys in the dashboard
- 2 Click Create Key
- 3 Give it a name (e.g., "PHP 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 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";
}
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";
}
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
- ✓ 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.