← Back to Blog

BIN Lookup API for Java: Complete Integration Guide

Published: February 5, 2026
Tags: java, tutorial, integration, developer, enterprise

Learn how to integrate BINLookupAPI into your Java application using this guide. We’ll break it down right from the beginning to writing production-ready code.

We’ll teach you how to:
  • Look up BIN information for any payment card
  • Handle all error cases gracefully
  • Process up to 2,000 BINs per request with the batch endpoint
  • 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.

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., "Java 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 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 (6-11 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 < 100000L || binNumber > 99999999999L) {
            throw new InvalidBINException(
                String.format("BIN must be between 100000 and 99999999999, 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 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) {}
}
i Java Version Compatibility

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());
        }
    }
}
* 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: Batch Lookups with the Native Batch Endpoint

If you need to enrich a list of BINs — a transaction export, a fraud review queue, a card portfolio — don’t loop over the single-lookup endpoint. The API has a native batch endpoint:

POST https://api.binlookupapi.com/v1/bins

It accepts 1 to 2,000 BINs per request. The body is a single numbers array, and each entry follows the same rules as a single lookup: 6–11 digits, as an integer or a digit string.

{"numbers": [400062, "400597", 99999999]}

The response contains one result per submitted number, in the same order:

{
  "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"
    }
  ]
}

Each item has either data (with the same fields as a single lookup) or a per-item error (NOT_FOUND or BAD_REQUEST). Per-item failures don’t affect the rest of the batch — the whole request only fails for request-level problems like malformed JSON, a missing or oversized array, authentication, or quota. As a safety measure, values longer than 11 digits (possible full card numbers) come back as "[REDACTED]" with a per-item BAD_REQUEST and are never stored in request logs.

! Availability & Quota

Batch lookups are available on the BLAPI-500KPM plan ($100/mo). Test-mode organisations always have access — with mock data and a 500-lookups/day limit — so you can build against the endpoint for free. Every submitted number counts as one lookup, including invalid and unknown ones. If a batch is larger than your remaining daily quota, the request returns 429 and consumes nothing; the X-Quota-Remaining header reports how many lookups you actually have left.

Let’s extend the BINLookupClient from Step 4 with batch support. Add these records alongside the other data models:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.List;

/**
 * Per-item metadata.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public record BatchItemMeta(
    @JsonProperty("longer_bin_available") boolean longerBinAvailable
) {}

/**
 * One result from a batch lookup. Exactly one of data or error is set.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public record BatchItem(
    String number,
    BINInfo data,
    BatchItemMeta meta,
    String error,
    String message
) {
    public boolean isSuccess() {
        return data != null;
    }
}

/**
 * Batch API response wrapper.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
record BatchResponse(String mode, List<BatchItem> results) {}

Then add a lookupBatch method to BINLookupClient. It partitions lists larger than 2,000 into multiple requests automatically:

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class BINLookupClient {

    private static final String BATCH_URL = "https://api.binlookupapi.com/v1/bins";
    private static final int MAX_BATCH_SIZE = 2000;

    // ... existing fields, constructors, and lookup() from Step 4 ...

    /**
     * Look up multiple BINs in a single API call (or several, for large lists).
     *
     * @param numbers The BINs to look up (each 6-11 digits)
     * @return One BatchItem per submitted number, in the same order
     * @throws InvalidBINException if the request itself is rejected
     * @throws AuthenticationException if the API key is invalid
     * @throws QuotaExceededException if the batch exceeds the remaining quota
     * @throws ServiceException if the API service has an error
     */
    public List<BatchItem> lookupBatch(List<String> numbers) throws BINLookupException {
        if (numbers == null || numbers.isEmpty()) {
            throw new InvalidBINException("At least one BIN is required.");
        }

        List<BatchItem> allResults = new ArrayList<>(numbers.size());

        // The endpoint accepts up to 2,000 BINs per request —
        // partition larger lists into multiple requests
        for (int start = 0; start < numbers.size(); start += MAX_BATCH_SIZE) {
            List<String> chunk = numbers.subList(
                start, Math.min(start + MAX_BATCH_SIZE, numbers.size())
            );
            allResults.addAll(sendBatch(chunk));
        }

        return allResults;
    }

    private List<BatchItem> sendBatch(List<String> chunk) throws BINLookupException {
        String requestBody;
        try {
            requestBody = objectMapper.writeValueAsString(Map.of("numbers", chunk));
        } catch (Exception e) {
            throw new ServiceException("Failed to serialise request body.", e);
        }

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(BATCH_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);
        }

        int statusCode = response.statusCode();

        switch (statusCode) {
            case 400 -> {
                String message = extractErrorMessage(response.body(), "Invalid batch request");
                throw new InvalidBINException(message);
            }
            case 401 -> throw new AuthenticationException(
                "Invalid API key. Check your credentials."
            );
            case 403 -> throw new AuthenticationException(
                "API key lacks required permissions, or your plan " +
                "does not include batch lookups."
            );
            case 429 -> throw new QuotaExceededException(
                "Batch exceeds remaining daily quota. Nothing was consumed. " +
                "Remaining: " +
                response.headers().firstValue("X-Quota-Remaining").orElse("unknown")
            );
        }

        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)
            );
        }

        try {
            BatchResponse batchResponse = objectMapper.readValue(
                response.body(),
                BatchResponse.class
            );
            return batchResponse.results();
        } catch (Exception e) {
            throw new ServiceException("Failed to parse API response.", e);
        }
    }
}

Using it is straightforward — iterate the results and branch on per-item success:

public class BatchLookupExample {
    public static void main(String[] args) {
        BINLookupClient client = new BINLookupClient();

        try {
            List<BatchItem> results = client.lookupBatch(
                List.of("400062", "400597", "99999999")
            );

            for (BatchItem item : results) {
                if (item.isSuccess()) {
                    BINInfo info = item.data();
                    System.out.printf("%s: %s %s — %s (%s)%n",
                        item.number(),
                        info.scheme(),
                        info.funding(),
                        info.issuer().name(),
                        info.country().code());
                } else {
                    System.out.printf("%s: %s — %s%n",
                        item.number(), item.error(), item.message());
                }
            }
        } catch (QuotaExceededException e) {
            System.err.println("Quota: " + e.getMessage());
        } catch (BINLookupException e) {
            System.err.println("Batch failed: " + e.getMessage());
        }
    }
}

Output:

400062: visa debit — EMIRATES NBD BANK (P.J.S.C.) (AE)
400597: visa credit — FIRST ELECTRONIC BANK (US)
99999999: NOT_FOUND — BIN not found

For the full request and response reference, see the batch lookups documentation.

Step 7: Async Support with CompletableFuture

The batch endpoint is the fastest way to process a large list, but it’s only available on plans that include it. If your plan doesn’t have batch lookups — or you need non-blocking single lookups inside an async application — Java’s CompletableFuture lets you run individual lookups concurrently:

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 concurrent 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 8: 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());
        }
    }
}
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
  • Use the batch endpoint for bulk lookups when your plan includes it — every submitted BIN counts against quota
  • Use try-with-resources for async client to ensure proper cleanup
  • Consider connection pooling for high-throughput applications

Next Steps

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