TL;DR
- A BIN (Bank Identification Number) is the first 6 to 8 digits of any payment card and tells you the issuing bank, card type, country of origin, and whether the card is prepaid or credit.
- You will build a reusable Express middleware function that intercepts incoming card data, calls a BIN lookup API, and attaches structured metadata to
req.binInfo. - The final middleware lets you block prepaid cards, filter by country, flag high-risk issuers, and add card branding in one clean layer before your payment logic runs.
- The API requires two credentials stored in
.env:BIN_API_KEYandX_USER_ID. Both are sent as request headers on every lookup call. - BIN values sent to the API must be between 6 and 8 digits. Fewer than 6 cannot identify an issuer. More than 8 starts encoding the cardholder account number, which you must never send to a third-party service.
- Stack: Node.js 18+, Express 4, Axios, dotenv.
- Total build time: under 60 minutes if you follow every step.
Why BIN Lookup Middleware Is Worth Your Time
If you process payments, subscriptions, or any kind of card-based transaction, you already know that not all cards are created equal.
A prepaid card from a high-risk country behaves very differently from a corporate Visa issued by a US bank. Your fraud rules should know the difference before a charge attempt ever reaches Stripe, Braintree, or your processor of choice.
The cleanest place to handle that logic is middleware. You run the BIN check once, attach the result to the request object, and every subsequent handler in your chain can read from req.binInfo without making a second API call. Your fraud logic, logging layer, and analytics all pull from the same enriched source.
This is a pattern used in production payment platforms. It is not complicated once you see it laid out step by step.
Let us build it.
Prerequisites
Before you write a single line of code, make sure your environment is ready.
Node.js version: 18 or higher. Run node -v to check. Node 18 ships with the native fetch API, but we will use Axios in this tutorial because it gives you cleaner error handling and automatic JSON parsing.
Package manager: npm or yarn. Either works.
Packages you will install:
-
express(version 4.x) for the HTTP server and routing -
axiosfor HTTP requests to the BIN API -
dotenvfor managing your API credentials as environment variables
A BIN lookup API key and User ID: This tutorial uses binsearchlookup.com for the lookup calls. Sign up, grab your API key and your User ID from the dashboard, and keep both nearby. You will need them in Step 2.
A tool for sending test requests: curl, Postman, or the VS Code REST Client extension all work.
Step 1: Set Up a Minimal Express Server
Create a new project folder and initialize it.
mkdir bin-middleware-demo
cd bin-middleware-demo
npm init -y
npm install express axios dotenv
Create your entry file and the middleware directory:
touch index.js
touch .env
mkdir middleware
touch middleware/binLookup.js
Open index.js and write the skeleton server:
// index.js
// Load environment variables from .env before anything else
require("dotenv").config();
const express = require("express");
const app = express();
// Parse incoming JSON bodies
app.use(express.json());
const binLookupMiddleware = require("./middleware/binLookup");
// Apply the BIN middleware only to the validate-card route
app.post("/validate-card", binLookupMiddleware, (req, res) => {
// At this point req.binInfo is already populated by the middleware
return res.json({
success: true,
binInfo: req.binInfo,
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Run node index.js and confirm the server starts. You should see the log line in your terminal. The /validate-card endpoint will return an error right now because the middleware file is still empty, but the structure is correct. Move on.
Step 2: Store Both Credentials Safely
The BIN lookup API requires two headers on every request: your API key and your User ID. Both live in your .env file so they never appear hardcoded in source code.
Open .env and add your credentials:
# .env
# Your BIN API key from binsearchlookup.com
BIN_API_KEY=bsl_7ffc440d90186fc5040d214bbed9e584686941e2e89d5168c137fe836e158a03
# Your User ID from your binsearchlookup.com account dashboard
X_USER_ID=e65dadd2-4797-4f77-9316-fe535f9b9268
PORT=3000
Never commit this file. Add it to .gitignore right now:
echo ".env" >> .gitignore
This one habit prevents credential leaks. Your credentials live in the environment, and your code reads them with process.env.BIN_API_KEY and process.env.X_USER_ID. No hardcoded strings, ever.
Step 3: Extract the BIN From the Request
The BIN is the first 6 to 8 digits of a card number. In real integrations you often receive the full card number on a server-to-server call, or a tokenized version that still carries the BIN prefix.
For this tutorial the client sends a JSON body like this:
{
"cardNumber": "5510291234567890"
}
Your middleware needs to strip everything except the first 8 digits, then validate that the result falls within the required 6-to-8 digit range. Fewer than 6 digits cannot identify a unique issuer. More than 8 digits starts encoding the individual account number, which is sensitive cardholder data you must never send to a third-party API.
Open middleware/binLookup.js and write the extraction logic:
// middleware/binLookup.js
const axios = require("axios");
// BIN must be between 6 and 8 digits -- no more, no less.
// Fewer than 6 cannot identify a unique issuer.
// More than 8 encodes the account number, which is sensitive cardholder data.
function extractBin(cardNumber) {
if (!cardNumber || typeof cardNumber !== "string") return null;
// Remove any spaces or dashes the client might have included
const cleaned = cardNumber.replace(/\D/g, "");
// Take the first 8 digits at most
const bin = cleaned.substring(0, 8);
// Reject if we cannot produce at least 6 digits
if (bin.length < 6) return null;
return bin;
}
module.exports = async function binLookupMiddleware(req, res, next) {
const { cardNumber } = req.body;
const bin = extractBin(cardNumber);
// Strict range check: the BIN must be 6, 7, or 8 digits
if (!bin || bin.length < 6 || bin.length > 8) {
return res.status(400).json({
success: false,
error: "BIN must be between 6 and 8 digits. Please provide a valid card number.",
});
}
// Attach the raw BIN to req for reference downstream
req.bin = bin;
// The API call happens in the next step -- for now just continue
next();
};
Notice that the middleware calls next() at the end. That is the Express convention. When you are done with your work, you hand control to the next handler in the chain. If you need to stop the request early, like the 400 response above, you return the response directly without calling next().
Step 4: Call the BIN Lookup API
Now replace the placeholder next() with the actual API request. You will call the BIN lookup service, receive a structured response, and attach it to req.binInfo.
The endpoint and required headers look like this when called directly:
curl -H "X-API-Key: bsl_7ffc440d90186fc5040d214bbed9e584686941e2e89d5168c137fe836e158a03" \
-H "X-User-ID: e65dadd2-4797-4f77-9316-fe535f9b9268" \
"https://api.binsearchlookup.com/lookup?bin=551029"
Two things to notice here. First, the BIN is passed as a query parameter (?bin=), not as a URL path segment. Second, both X-API-Key and X-User-ID are required headers. A request missing either one will be rejected by the API.
A successful response returns a JSON object similar to this:
{
"bin": "551029",
"brand": "MASTERCARD",
"type": "CREDIT",
"category": "CLASSIC",
"issuer": {
"name": "Citibank",
"country": "US",
"phone": "+18006274276",
"website": "www.citibank.com"
},
"country": {
"name": "United States",
"alpha2": "US",
"alpha3": "USA",
"numeric": "840",
"currency": "USD"
},
"prepaid": false
}
Now write the full middleware that makes this call. Replace the entire contents of middleware/binLookup.js with the following:
// middleware/binLookup.js -- full version with API call
const axios = require("axios");
// BIN must be exactly 6 to 8 digits.
// Anything shorter cannot identify an issuer.
// Anything longer encodes the account number -- never send that to a third party.
function extractBin(cardNumber) {
if (!cardNumber || typeof cardNumber !== "string") return null;
const cleaned = cardNumber.replace(/\D/g, "");
const bin = cleaned.substring(0, 8);
if (bin.length < 6) return null;
return bin;
}
module.exports = async function binLookupMiddleware(req, res, next) {
const { cardNumber } = req.body;
const bin = extractBin(cardNumber);
if (!bin || bin.length < 6 || bin.length > 8) {
return res.status(400).json({
success: false,
error: "BIN must be between 6 and 8 digits. Please provide a valid card number.",
});
}
req.bin = bin;
try {
const response = await axios.get(
"https://api.binsearchlookup.com/lookup",
{
params: {
// Pass the extracted BIN as a query parameter
// The API accepts 6, 7, or 8 digits
bin: bin,
},
headers: {
// Both headers are required -- both are read from environment variables
"X-API-Key": process.env.BIN_API_KEY,
"X-User-ID": process.env.X_USER_ID,
},
// Fail fast: if the BIN API takes longer than 3 seconds, soft-fail
timeout: 3000,
}
);
// Attach the full enriched BIN data to the request object.
// Every downstream handler can now read req.binInfo without a second API call.
req.binInfo = response.data;
next();
} catch (error) {
// Log the failure internally but never expose credentials or internals to the client
console.error("BIN lookup failed:", error.message);
// Soft-fail: set binInfo to null and continue.
// A BIN API outage should not take your entire checkout flow down.
// Your route handler decides what to do when binInfo is null.
req.binInfo = null;
next();
}
};
Your middleware now handles both credentials, enforces the BIN digit range, and attaches everything downstream handlers need.
Step 5: Use the Metadata for Real Business Rules
The middleware has done its job. req.binInfo is populated. Every handler downstream can read it without making another API call.
Open index.js and replace the skeleton route handler with real policy logic:
// index.js -- full version with business rules
require("dotenv").config();
const express = require("express");
const app = express();
app.use(express.json());
const binLookupMiddleware = require("./middleware/binLookup");
// Define which issuer countries your business accepts
const ALLOWED_COUNTRIES = ["US", "CA", "GB", "AU", "DE", "FR"];
app.post("/validate-card", binLookupMiddleware, (req, res) => {
const binInfo = req.binInfo;
// If the BIN lookup failed (API outage, unknown BIN), enforce your fallback policy.
// This example blocks the transaction. You could also allow it with a lower trust score.
if (!binInfo) {
return res.status(422).json({
success: false,
error: "Unable to verify card origin. Please try again.",
});
}
// Rule 1: Block prepaid cards.
// Prepaid cards are high-risk for chargebacks and subscription fraud.
if (binInfo.prepaid === true) {
return res.status(403).json({
success: false,
error: "Prepaid cards are not accepted for this service.",
code: "PREPAID_CARD_BLOCKED",
});
}
// Rule 2: Block cards issued outside your allowed countries.
// binInfo.country.alpha2 is the ISO 3166-1 two-letter country code.
const issuerCountry = binInfo.country?.alpha2;
if (issuerCountry && !ALLOWED_COUNTRIES.includes(issuerCountry)) {
return res.status(403).json({
success: false,
error: "Cards from this region are not supported.",
code: "COUNTRY_NOT_ALLOWED",
country: issuerCountry,
});
}
// All checks passed. Return enriched card metadata to the caller.
return res.json({
success: true,
bin: req.bin,
cardBrand: binInfo.brand,
cardType: binInfo.type,
issuer: binInfo.issuer?.name,
country: issuerCountry,
prepaid: binInfo.prepaid,
message: "Card validated successfully.",
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Each rule is a single conditional block. You can add, remove, or reorder rules without touching the middleware at all. The middleware fetches and attaches. The route handler enforces policy. Two different responsibilities, two different places.
Step 6: Test Every Scenario With Curl
Start your server:
node index.js
Test 1: A standard US credit card
curl -X POST http://localhost:3000/validate-card \
-H "Content-Type: application/json" \
-d '{"cardNumber": "5510291234567890"}'
Expected: 200 OK with card brand, type, issuer name, and country.
Test 2: A prepaid card
curl -X POST http://localhost:3000/validate-card \
-H "Content-Type: application/json" \
-d '{"cardNumber": "4023601234567890"}'
Expected: 403 Forbidden with PREPAID_CARD_BLOCKED code.
Test 3: Missing card number
curl -X POST http://localhost:3000/validate-card \
-H "Content-Type: application/json" \
-d '{}'
Expected: 400 Bad Request with a BIN digit range error.
Test 4: Card number too short to produce a valid BIN
curl -X POST http://localhost:3000/validate-card \
-H "Content-Type: application/json" \
-d '{"cardNumber": "411"}'
Expected: 400 Bad Request. The extracted BIN is only 3 digits, which is below the required minimum of 6.
Test 5: Card issued in a blocked country
Use a BIN known to belong to an issuer outside your ALLOWED_COUNTRIES list.
Expected: 403 Forbidden with COUNTRY_NOT_ALLOWED code and the country alpha2 value in the response body.
How your middleware maps to a raw API call: If you were hitting the BIN lookup API directly, the request would look exactly like this:
curl -H "X-API-Key: bsl_7ffc440d90186fc5040d214bbed9e584686941e2e89d5168c137fe836e158a03" \ -H "X-User-ID: e65dadd2-4797-4f77-9316-fe535f9b9268" \ "https://api.binsearchlookup.com/lookup?bin=551029"Your middleware handles both headers automatically from
.env. Your route handlers and your frontend never touch credentials directly. That is the entire point.
Run all five tests before moving on. If any response is unexpected, re-read the middleware and double-check your .env file.
Step 7: Add a Simple In-Memory Cache (Optional but Recommended)
You do not need to call the BIN API on every single request. A BIN is permanently tied to an issuer. It does not change. If the same BIN appears twice in a session or across requests within any window, serve it from cache.
Here is a lightweight in-memory cache using a plain JavaScript Map:
// middleware/binLookup.js -- final version with caching
const axios = require("axios");
// Simple in-memory cache: BIN string -> API response data.
// In production, replace this with Redis and set a TTL of 24 hours.
const binCache = new Map();
function extractBin(cardNumber) {
if (!cardNumber || typeof cardNumber !== "string") return null;
const cleaned = cardNumber.replace(/\D/g, "");
const bin = cleaned.substring(0, 8);
if (bin.length < 6) return null;
return bin;
}
module.exports = async function binLookupMiddleware(req, res, next) {
const { cardNumber } = req.body;
const bin = extractBin(cardNumber);
if (!bin || bin.length < 6 || bin.length > 8) {
return res.status(400).json({
success: false,
error: "BIN must be between 6 and 8 digits. Please provide a valid card number.",
});
}
req.bin = bin;
// Serve from cache if available -- avoids a network call entirely
if (binCache.has(bin)) {
req.binInfo = binCache.get(bin);
req.binCacheHit = true; // Useful for metrics and logging
return next();
}
try {
const response = await axios.get(
"https://api.binsearchlookup.com/lookup",
{
params: { bin: bin },
headers: {
"X-API-Key": process.env.BIN_API_KEY,
"X-User-ID": process.env.X_USER_ID,
},
timeout: 3000,
}
);
// Store result in cache before attaching to req
binCache.set(bin, response.data);
req.binInfo = response.data;
req.binCacheHit = false;
next();
} catch (error) {
console.error("BIN lookup failed:", error.message);
req.binInfo = null;
req.binCacheHit = false;
next();
}
};
For a production system, replace the Map with a Redis client and set a TTL of 24 hours. BIN data is stable, and Redis handles memory management, persistence, and expiry automatically.
Step 8: Structure Your Project for the Long Term
Here is the final folder structure you should have:
bin-middleware-demo/
middleware/
binLookup.js # Fetches BIN data and attaches it to req.binInfo
index.js # Express server, route definitions, and business rules
.env # API credentials and config (never commit this)
.gitignore
package.json
As your project grows, split the business rules into a separate validators/cardPolicy.js file and import it into the route handler. The middleware stays thin and focused on fetching. The policy layer stays focused on rules. Clean separation means easier unit testing and easier onboarding for every engineer who joins the project after you.
Full Code Reference
Here is every file in its final state so you can copy, paste, and run without scrolling back through the steps.
.env
BIN_API_KEY=bsl_7ffc440d90186fc5040d214bbed9e584686941e2e89d5168c137fe836e158a03
X_USER_ID=e65dadd2-4797-4f77-9316-fe535f9b9268
PORT=3000
middleware/binLookup.js
const axios = require("axios");
// In-memory cache to avoid redundant API calls for the same BIN.
// Replace with Redis in production and set a 24-hour TTL.
const binCache = new Map();
// BIN must be between 6 and 8 digits.
// 6 digits is the minimum to identify a unique issuer.
// 8 digits is the maximum before you start encoding the account number.
function extractBin(cardNumber) {
if (!cardNumber || typeof cardNumber !== "string") return null;
const cleaned = cardNumber.replace(/\D/g, "");
const bin = cleaned.substring(0, 8);
if (bin.length < 6) return null;
return bin;
}
module.exports = async function binLookupMiddleware(req, res, next) {
const { cardNumber } = req.body;
const bin = extractBin(cardNumber);
if (!bin || bin.length < 6 || bin.length > 8) {
return res.status(400).json({
success: false,
error: "BIN must be between 6 and 8 digits. Please provide a valid card number.",
});
}
// Attach the raw BIN for downstream reference
req.bin = bin;
// Return cached result if available
if (binCache.has(bin)) {
req.binInfo = binCache.get(bin);
req.binCacheHit = true;
return next();
}
try {
const response = await axios.get(
"https://api.binsearchlookup.com/lookup",
{
params: { bin: bin },
headers: {
// X-API-Key and X-User-ID are both required by the API
"X-API-Key": process.env.BIN_API_KEY,
"X-User-ID": process.env.X_USER_ID,
},
timeout: 3000,
}
);
binCache.set(bin, response.data);
req.binInfo = response.data;
req.binCacheHit = false;
next();
} catch (error) {
// Soft-fail: log internally, do not expose API details to the client
console.error("BIN lookup failed:", error.message);
req.binInfo = null;
req.binCacheHit = false;
next();
}
};
index.js
require("dotenv").config();
const express = require("express");
const app = express();
app.use(express.json());
const binLookupMiddleware = require("./middleware/binLookup");
// Cards issued outside this list will be blocked
const ALLOWED_COUNTRIES = ["US", "CA", "GB", "AU", "DE", "FR"];
app.post("/validate-card", binLookupMiddleware, (req, res) => {
const binInfo = req.binInfo;
// BIN lookup failed (API outage or unknown BIN) -- block and ask to retry
if (!binInfo) {
return res.status(422).json({
success: false,
error: "Unable to verify card origin. Please try again.",
});
}
// Rule 1: Block prepaid cards
if (binInfo.prepaid === true) {
return res.status(403).json({
success: false,
error: "Prepaid cards are not accepted for this service.",
code: "PREPAID_CARD_BLOCKED",
});
}
// Rule 2: Block cards from unsupported issuer countries
const issuerCountry = binInfo.country?.alpha2;
if (issuerCountry && !ALLOWED_COUNTRIES.includes(issuerCountry)) {
return res.status(403).json({
success: false,
error: "Cards from this region are not supported.",
code: "COUNTRY_NOT_ALLOWED",
country: issuerCountry,
});
}
// All checks passed
return res.json({
success: true,
bin: req.bin,
cardBrand: binInfo.brand,
cardType: binInfo.type,
issuer: binInfo.issuer?.name,
country: issuerCountry,
prepaid: binInfo.prepaid,
cacheHit: req.binCacheHit,
message: "Card validated successfully.",
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Summary
You built a production-quality BIN lookup middleware in eight steps. Here is what you now have:
- A reusable
binLookupMiddlewarefunction that extracts the BIN, enforces the strict 6-to-8 digit rule, calls the lookup API with both required credentials, and attaches structured data toreq.binInfo. - Both
BIN_API_KEYandX_USER_IDstored safely in.envand passed asX-API-KeyandX-User-IDheaders on every API request. - A
/validate-cardendpoint that enforces prepaid card and country-of-origin rules using that metadata. - Graceful soft-fail handling so an API outage does not break your checkout flow.
- An in-memory cache to eliminate redundant network calls for repeated BINs.
- Clean separation between the data-fetching middleware and the business rule layer in the route handler.
The pattern you learned here applies beyond BIN lookups. Any time you need to enrich an incoming request with external data before your main handler runs, middleware is the right tool. IP geolocation, device fingerprinting, user reputation scoring: all of them follow exactly this shape.
FAQ
What is a BIN number and why does it matter for payments?
BIN stands for Bank Identification Number. It is the first 6 to 8 digits of a payment card number and it identifies the issuing bank, the card network (Visa, Mastercard, Amex), the card type (credit, debit, prepaid), and the country where the card was issued. Payment platforms use BIN data to run fraud checks, apply geographic restrictions, and display the correct card brand logo at checkout.
Why does the BIN value sent to the API have to be between 6 and 8 digits?
The BIN lookup database is indexed on 6-digit and 8-digit prefixes. Fewer than 6 digits does not uniquely identify an issuer. More than 8 digits begins encoding the individual account number, which is sensitive cardholder data that you must never send to any third-party service. The extractBin function enforces this range before the network call is ever made.
What are X-API-Key and X-User-ID and why are both required?
X-API-Key authenticates your application to the BIN lookup API. X-User-ID identifies the specific account making the request, which allows the API provider to apply per-account rate limits, usage tracking, and billing. Both are assigned when you create an account at binsearchlookup.com. Storing them in .env and reading them with process.env keeps them out of your source code and out of version control.
Is it safe to log the full card number on the server?
No. You should never log a full card number. Extract only the BIN immediately and discard or tokenize the rest. The BIN alone is not sensitive because it does not identify the cardholder or authorize a transaction. Log the BIN, not the card number.
What happens if the BIN API is down?
The middleware uses a soft-fail pattern. If the API call throws an error or times out, req.binInfo is set to null and next() is called. Your route handler checks for null and decides the fallback policy. The example in this tutorial returns a 422 and asks the user to try again. You could also allow the transaction through with reduced confidence scoring. The decision belongs in the route handler, not in the middleware.
Can I use this with Stripe or another payment processor?
Yes. This middleware runs before you ever call Stripe. You enrich the request object first, apply your rules, and only then initiate the Stripe payment intent if everything passes. The BIN check adds a validation layer that sits entirely on your own server.
Should I use 6-digit or 8-digit BINs?
Use 8-digit BINs when the card number is long enough to produce them. The payments industry has been migrating toward 8-digit BINs since 2022, and modern BIN lookup APIs return more accurate results with 8 digits. The extractBin function in this tutorial uses substring(0, 8) to grab as many digits as possible up to the 8-digit maximum, then validates that at least 6 were available.
How do I extend the middleware to handle debit cards differently?
Read req.binInfo.type in your route handler. It will be "CREDIT", "DEBIT", or "PREPAID". Write conditional logic for each type without touching the middleware at all. That is the benefit of keeping data fetching and policy enforcement in separate layers.
What is the right timeout for a BIN lookup API call?
A 2 to 3 second timeout is reasonable. BIN lookups are fast because the data is static and well-indexed. If your upstream API consistently takes longer than 1 second, check whether you are hitting a rate limit or whether a geographically closer endpoint is available.
If this tutorial helped you, share what rules you are enforcing in your own BIN middleware in the comments. The more specific your use case, the more others in the community can learn from it.
Top comments (0)