BIN Lookup API for Java: Complete Integration Guide
This guide walks you through integrating BINLookupAPI into your Java 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 synchronous and asynchronous code
- ✓ Validate cards in a real payment flow
Prerequisites
- Java 11 or higher (Java 16+ recommended for record classes)
- Maven or Gradle for dependency management
- Jackson or Gson for JSON parsing
Add the following dependencies to your project:
Maven (pom.xml):
<dependencies>
<!-- Jackson for JSON parsing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
Gradle (build.gradle):
dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
}
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., "Java 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 java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class SimpleBinLookup {
private static final String API_KEY = "your_api_key_here";
private static final String API_URL = "https://api.binlookupapi.com/v1/bin";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String requestBody = "{\"number\": 42467101}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.body());
}
}
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 com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Optional;
// ============================================================================
// Exception Hierarchy
// ============================================================================
/**
* Base exception for all BIN lookup errors.
*/
public class BINLookupException extends Exception {
public BINLookupException(String message) {
super(message);
}
public BINLookupException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Raised when the BIN format is invalid.
*/
public class InvalidBINException extends BINLookupException {
public InvalidBINException(String message) {
super(message);
}
}
/**
* Raised when the API key is invalid or missing.
*/
public class AuthenticationException extends BINLookupException {
public AuthenticationException(String message) {
super(message);
}
}
/**
* Raised when the daily quota has been exceeded.
*/
public class QuotaExceededException extends BINLookupException {
public QuotaExceededException(String message) {
super(message);
}
}
/**
* Raised when the BIN is not in the database.
*/
public class BINNotFoundException extends BINLookupException {
public BINNotFoundException(String message) {
super(message);
}
}
/**
* Raised when the API service encounters an error.
*/
public class ServiceException extends BINLookupException {
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
}
// ============================================================================
// Data Models (Java 16+ Records)
// ============================================================================
/**
* Card issuer information.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record Issuer(
String name,
String website,
String phone
) {}
/**
* Country information.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record Country(
String code,
String name
) {}
/**
* Complete BIN information.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record BINInfo(
String bin,
String scheme,
String funding,
String brand,
String category,
Country country,
Issuer issuer,
String currency,
boolean prepaid,
boolean commercial
) {}
/**
* API response wrapper.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
record ApiResponse(BINInfo data, String message) {}
// ============================================================================
// BIN Lookup Client
// ============================================================================
/**
* Client for the BINLookupAPI service.
*/
public class BINLookupClient {
private static final String BASE_URL = "https://api.binlookupapi.com/v1/bin";
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(10);
private final String apiKey;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
/**
* Creates a client using the BINLOOKUP_API_KEY environment variable.
*
* @throws IllegalArgumentException if API key is not found
*/
public BINLookupClient() {
this(System.getenv("BINLOOKUP_API_KEY"));
}
/**
* Creates a client with the specified API key.
*
* @param apiKey Your BINLookupAPI key
* @throws IllegalArgumentException if API key is null or empty
*/
public BINLookupClient(String apiKey) {
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalArgumentException(
"API key required. Pass it directly or set " +
"BINLOOKUP_API_KEY environment variable."
);
}
this.apiKey = apiKey;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(DEFAULT_TIMEOUT)
.build();
this.objectMapper = new ObjectMapper();
}
/**
* Look up information for a BIN.
*
* @param binNumber The BIN to look up (4-8 digits)
* @return 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 BINInfo lookup(long binNumber) throws BINLookupException {
// Validate input
if (binNumber < 1000 || binNumber > 99999999L) {
throw new InvalidBINException(
String.format("BIN must be between 1000 and 99999999, got: %d", binNumber)
);
}
String requestBody = String.format("{\"number\": %d}", binNumber);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.timeout(DEFAULT_TIMEOUT)
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException e) {
throw new ServiceException("Could not connect to API. Check your network.", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("Request was interrupted.", e);
}
// Handle error responses
int statusCode = response.statusCode();
switch (statusCode) {
case 400 -> {
String message = extractErrorMessage(response.body(), "Invalid BIN");
throw new InvalidBINException(message);
}
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(
String.format("BIN %d not found in database.", binNumber)
);
case 429 -> throw new QuotaExceededException(
"Daily quota exceeded. Resets at midnight UTC."
);
case 502 -> throw new ServiceException(
"API gateway error. Please try again later."
);
}
if (statusCode >= 500) {
throw new ServiceException(
String.format("API service error (HTTP %d). Please try again later.", statusCode)
);
}
if (statusCode < 200 || statusCode >= 300) {
throw new BINLookupException(
String.format("Unexpected error: HTTP %d", statusCode)
);
}
// Parse successful response
try {
ApiResponse apiResponse = objectMapper.readValue(response.body(), ApiResponse.class);
return apiResponse.data();
} catch (Exception e) {
throw new ServiceException("Failed to parse API response.", e);
}
}
/**
* Get quota information from response headers.
*
* @return QuotaInfo with current usage details
*/
public Optional<QuotaInfo> getQuotaInfo() {
try {
String requestBody = "{\"number\": 42467101}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.timeout(DEFAULT_TIMEOUT)
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(
request,
HttpResponse.BodyHandlers.ofString()
);
int limit = Integer.parseInt(
response.headers().firstValue("X-Quota-Limit").orElse("0")
);
int remaining = Integer.parseInt(
response.headers().firstValue("X-Quota-Remaining").orElse("0")
);
long reset = Long.parseLong(
response.headers().firstValue("X-Quota-Reset").orElse("0")
);
return Optional.of(new QuotaInfo(limit, remaining, reset));
} catch (Exception e) {
return Optional.empty();
}
}
private String extractErrorMessage(String responseBody, String defaultMessage) {
try {
ApiResponse response = objectMapper.readValue(responseBody, ApiResponse.class);
return response.message() != null ? response.message() : defaultMessage;
} catch (Exception e) {
return defaultMessage;
}
}
/**
* Quota information.
*/
public record QuotaInfo(int limit, int remaining, long resetTimestamp) {}
}
The code above uses Java 16+ record classes. If you’re using Java 11-15, replace records with regular classes using getters and a constructor. See the alternative implementation in the expanded section below.
Step 5: Using the Client
Here’s how to use the production-ready client:
public class BinLookupExample {
public static void main(String[] args) {
// Initialize with API key from environment
BINLookupClient client = new BINLookupClient();
// Or pass the key directly
// BINLookupClient client = new BINLookupClient("your_api_key_here");
try {
BINInfo info = client.lookup(42467101);
System.out.println("Card Network: " + info.scheme());
System.out.println("Card Type: " + info.funding());
System.out.println("Issuer: " + info.issuer().name());
System.out.println("Country: " + info.country().name());
System.out.println("Is Prepaid: " + info.prepaid());
} catch (InvalidBINException e) {
System.err.println("Invalid BIN: " + e.getMessage());
} catch (AuthenticationException e) {
System.err.println("Auth error: " + e.getMessage());
// Check your API key
} catch (QuotaExceededException e) {
System.err.println("Quota exceeded: " + e.getMessage());
// Wait until midnight UTC or upgrade your plan
} catch (BINNotFoundException e) {
System.err.println("BIN not found: " + e.getMessage());
// This BIN isn't in the database
} catch (ServiceException e) {
System.err.println("Service error: " + e.getMessage());
// Retry with exponential backoff
} catch (BINLookupException e) {
System.err.println("Unexpected error: " + e.getMessage());
}
}
}
The exception hierarchy lets you catch specific errors or use the base BINLookupException to handle all API-related errors at once.
Step 6: Async Support with CompletableFuture
For high-throughput applications, here’s an async version using Java’s CompletableFuture:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
/**
* Async client for BINLookupAPI.
*/
public class AsyncBINLookupClient implements AutoCloseable {
private static final String BASE_URL = "https://api.binlookupapi.com/v1/bin";
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(10);
private final String apiKey;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private final ExecutorService executor;
public AsyncBINLookupClient(String apiKey) {
this.apiKey = apiKey;
this.executor = Executors.newVirtualThreadPerTaskExecutor(); // Java 21+
// For Java 11-20, use: Executors.newCachedThreadPool();
this.httpClient = HttpClient.newBuilder()
.connectTimeout(DEFAULT_TIMEOUT)
.executor(executor)
.build();
this.objectMapper = new ObjectMapper();
}
/**
* Async BIN lookup.
*/
public CompletableFuture<BINInfo> lookupAsync(long binNumber) {
String requestBody = String.format("{\"number\": %d}", binNumber);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.timeout(DEFAULT_TIMEOUT)
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> {
int statusCode = response.statusCode();
if (statusCode == 401) {
throw new RuntimeException(
new AuthenticationException("Invalid API key")
);
}
if (statusCode == 404) {
throw new RuntimeException(
new BINNotFoundException("BIN " + binNumber + " not found")
);
}
if (statusCode < 200 || statusCode >= 300) {
throw new RuntimeException(
new ServiceException("HTTP " + statusCode)
);
}
try {
ApiResponse apiResponse = objectMapper.readValue(
response.body(),
ApiResponse.class
);
return apiResponse.data();
} catch (Exception e) {
throw new RuntimeException(
new ServiceException("Failed to parse response", e)
);
}
});
}
/**
* Look up multiple BINs concurrently.
*/
public CompletableFuture<List<LookupResult>> lookupManyAsync(List<Long> binNumbers) {
List<CompletableFuture<LookupResult>> futures = binNumbers.stream()
.map(bin -> lookupAsync(bin)
.thenApply(info -> new LookupResult(bin, info, null))
.exceptionally(e -> new LookupResult(bin, null, e.getCause()))
)
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList())
);
}
@Override
public void close() {
executor.shutdown();
}
/**
* Result wrapper for batch lookups.
*/
public record LookupResult(long bin, BINInfo info, Throwable error) {
public boolean isSuccess() {
return info != null && error == null;
}
}
}
// Usage
public class AsyncExample {
public static void main(String[] args) throws Exception {
try (AsyncBINLookupClient client = new AsyncBINLookupClient("your_api_key")) {
// Single async lookup
client.lookupAsync(42467101)
.thenAccept(info -> System.out.println("Scheme: " + info.scheme()))
.exceptionally(e -> {
System.err.println("Error: " + e.getMessage());
return null;
})
.join();
// Multiple lookups in parallel
List<Long> bins = List.of(42467101L, 51234567L, 37123456L);
client.lookupManyAsync(bins)
.thenAccept(results -> {
for (var result : results) {
if (result.isSuccess()) {
System.out.printf("%d: %s%n",
result.bin(), result.info().scheme());
} else {
System.out.printf("%d: Error - %s%n",
result.bin(), result.error().getMessage());
}
}
})
.join();
}
}
}
Step 7: Real-World Example — Payment Form Validation
Here’s a practical example of using BIN lookup in a payment flow:
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Result of card validation.
*/
public record CardValidationResult(
boolean valid,
String cardType,
String issuer,
String country,
boolean isPrepaid,
List<String> warnings,
String error
) {
public static CardValidationResult success(
String cardType,
String issuer,
String country,
boolean isPrepaid,
List<String> warnings) {
return new CardValidationResult(
true, cardType, issuer, country, isPrepaid, warnings, null
);
}
public static CardValidationResult failure(String error) {
return new CardValidationResult(
false, null, null, null, false, null, error
);
}
public static CardValidationResult failure(
String error,
String cardType,
String issuer,
String country,
boolean isPrepaid) {
return new CardValidationResult(
false, cardType, issuer, country, isPrepaid, null, error
);
}
}
/**
* Card validator using BIN lookup.
*/
public class CardValidator {
private static final Pattern DIGITS_ONLY = Pattern.compile("\\D");
private final BINLookupClient client;
public CardValidator(BINLookupClient client) {
this.client = client;
}
/**
* Validate a card BIN for payment processing.
*
* @param cardNumber The card number (only first 6-8 digits are used)
* @param blockPrepaid If true, reject prepaid cards
* @param allowedCountries Set of allowed country codes (e.g., ["US", "GB"])
* @return CardValidationResult with validation details
*/
public CardValidationResult validateCardBin(
String cardNumber,
boolean blockPrepaid,
Set<String> allowedCountries) {
List<String> warnings = new ArrayList<>();
// Extract digits only
String digitsOnly = DIGITS_ONLY.matcher(cardNumber).replaceAll("");
if (digitsOnly.length() < 6) {
return CardValidationResult.failure(
"Card number must be at least 6 digits"
);
}
// Use first 8 digits as BIN
String binStr = digitsOnly.substring(0, Math.min(8, digitsOnly.length()));
long binNumber = Long.parseLong(binStr);
BINInfo info;
try {
info = client.lookup(binNumber);
} catch (BINNotFoundException e) {
return CardValidationResult.failure(
"Unable to identify card. Please check the number."
);
} catch (QuotaExceededException e) {
// Fail open - allow the transaction but log the issue
return CardValidationResult.success(
null, null, null, false,
List.of("BIN validation skipped due to quota limits")
);
} catch (BINLookupException e) {
// Fail open for service errors
return CardValidationResult.success(
null, null, null, false,
List.of("BIN validation unavailable: " + e.getMessage())
);
}
String cardType = info.scheme();
String issuerName = info.issuer() != null ? info.issuer().name() : null;
String countryCode = info.country() != null ? info.country().code() : null;
String countryName = info.country() != null ? info.country().name() : null;
// Check prepaid status
if (blockPrepaid && info.prepaid()) {
return CardValidationResult.failure(
"Prepaid cards are not accepted",
cardType, issuerName, countryCode, true
);
}
// Check country restrictions
if (allowedCountries != null &&
!allowedCountries.isEmpty() &&
countryCode != null &&
!allowedCountries.contains(countryCode)) {
return CardValidationResult.failure(
"Cards from " + countryName + " are not accepted",
cardType, issuerName, countryCode, info.prepaid()
);
}
// Add warnings for high-risk indicators
if (info.prepaid()) {
warnings.add("This is a prepaid card");
}
if (info.commercial()) {
warnings.add("This is a corporate/business card");
}
return CardValidationResult.success(
cardType,
issuerName,
countryCode,
info.prepaid(),
warnings.isEmpty() ? null : warnings
);
}
}
// Usage example
public class PaymentValidationExample {
public static void main(String[] args) {
BINLookupClient client = new BINLookupClient();
CardValidator validator = new CardValidator(client);
CardValidationResult result = validator.validateCardBin(
"4246710012345678",
true, // block prepaid cards
Set.of("US", "GB", "CA", "AU") // allowed countries
);
if (result.valid()) {
System.out.printf("Card accepted: %s from %s%n",
result.cardType(), result.issuer());
if (result.warnings() != null) {
for (String warning : result.warnings()) {
System.out.println("Warning: " + warning);
}
}
} else {
System.out.println("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
- ✓ Consider fail-open strategy for non-critical validation
- ✓ Monitor your quota usage via response headers
- ✓ Use try-with-resources for async client to ensure proper cleanup
- ✓ Consider connection pooling for high-throughput applications
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.