BIN Lookup API for Go: Complete Integration Guide
This guide walks you through integrating BINLookupAPI into your Go application, from creating your account to writing production-ready code with proper error handling and concurrency support.
- ✓ Look up BIN information for any payment card
- ✓ Handle all error cases gracefully
- ✓ Use goroutines for concurrent BIN lookups
- ✓ Validate cards in a real payment flow
Prerequisites
- Go 1.21 or higher
- Standard library only (no external dependencies)
Create a new module for your project:
mkdir bin-lookup && cd bin-lookup
go mod init bin-lookup
Your go.mod file should look like:
module bin-lookup
go 1.21
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., "Go 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:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const (
apiKey = "your_api_key_here"
apiURL = "https://api.binlookupapi.com/v1/bin"
)
type BINRequest struct {
Number int `json:"number"`
}
type BINResponse struct {
Data map[string]any `json:"data"`
}
func lookupBIN(binNumber int) (map[string]any, error) {
reqBody, _ := json.Marshal(BINRequest{Number: binNumber})
req, _ := http.NewRequest("POST", apiURL, bytes.NewReader(reqBody))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result BINResponse
json.Unmarshal(body, &result)
return result.Data, nil
}
func main() {
result, err := lookupBIN(42467101)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("%+v\n", 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:
package binlookup
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
)
// Error types for BIN lookup operations
var (
ErrInvalidBIN = errors.New("invalid BIN format")
ErrAuthentication = errors.New("authentication failed")
ErrQuotaExceeded = errors.New("quota exceeded")
ErrBINNotFound = errors.New("BIN not found")
ErrService = errors.New("service error")
)
// BINLookupError represents an error from the BIN lookup API
type BINLookupError struct {
Err error
Message string
Code int
}
func (e *BINLookupError) Error() string {
if e.Message != "" {
return fmt.Sprintf("%s: %s", e.Err.Error(), e.Message)
}
return e.Err.Error()
}
func (e *BINLookupError) Unwrap() error {
return e.Err
}
// InvalidBINError indicates the BIN format is invalid
type InvalidBINError struct {
BIN int
Message string
}
func (e *InvalidBINError) Error() string {
return fmt.Sprintf("invalid BIN %d: %s", e.BIN, e.Message)
}
func (e *InvalidBINError) Unwrap() error {
return ErrInvalidBIN
}
// AuthenticationError indicates an API key issue
type AuthenticationError struct {
Message string
}
func (e *AuthenticationError) Error() string {
return fmt.Sprintf("authentication error: %s", e.Message)
}
func (e *AuthenticationError) Unwrap() error {
return ErrAuthentication
}
// QuotaExceededError indicates the daily quota has been exceeded
type QuotaExceededError struct {
Message string
}
func (e *QuotaExceededError) Error() string {
return fmt.Sprintf("quota exceeded: %s", e.Message)
}
func (e *QuotaExceededError) Unwrap() error {
return ErrQuotaExceeded
}
// BINNotFoundError indicates the BIN is not in the database
type BINNotFoundError struct {
BIN int
}
func (e *BINNotFoundError) Error() string {
return fmt.Sprintf("BIN %d not found in database", e.BIN)
}
func (e *BINNotFoundError) Unwrap() error {
return ErrBINNotFound
}
// ServiceError indicates an API service error
type ServiceError struct {
StatusCode int
Message string
}
func (e *ServiceError) Error() string {
return fmt.Sprintf("service error (HTTP %d): %s", e.StatusCode, e.Message)
}
func (e *ServiceError) Unwrap() error {
return ErrService
}
// Country represents card issuing country information
type Country struct {
Code string `json:"code"`
Name string `json:"name"`
}
// Issuer represents card issuer information
type Issuer struct {
Name *string `json:"name"`
Website *string `json:"website"`
Phone *string `json:"phone"`
}
// BINInfo contains the BIN lookup result
type BINInfo struct {
BIN string `json:"bin"`
Scheme string `json:"scheme"`
Funding string `json:"funding"`
Brand *string `json:"brand"`
Category *string `json:"category"`
Country Country `json:"country"`
Issuer Issuer `json:"issuer"`
Currency *string `json:"currency"`
Prepaid bool `json:"prepaid"`
Commercial bool `json:"commercial"`
}
type binRequest struct {
Number int `json:"number"`
}
type binResponse struct {
Data BINInfo `json:"data"`
Message string `json:"message,omitempty"`
}
// Client is a client for the BINLookupAPI service
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
}
// ClientOption configures the Client
type ClientOption func(*Client)
// WithHTTPClient sets a custom HTTP client
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = httpClient
}
}
// WithBaseURL sets a custom base URL (useful for testing)
func WithBaseURL(baseURL string) ClientOption {
return func(c *Client) {
c.baseURL = baseURL
}
}
// NewClient creates a new BINLookupAPI client
//
// If apiKey is empty, it reads from BINLOOKUP_API_KEY environment variable.
func NewClient(apiKey string, opts ...ClientOption) (*Client, error) {
if apiKey == "" {
apiKey = os.Getenv("BINLOOKUP_API_KEY")
}
if apiKey == "" {
return nil, errors.New(
"API key required: pass it directly or set BINLOOKUP_API_KEY environment variable",
)
}
c := &Client{
apiKey: apiKey,
baseURL: "https://api.binlookupapi.com/v1/bin",
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
for _, opt := range opts {
opt(c)
}
return c, nil
}
// Lookup retrieves information for a BIN
//
// The binNumber should be 4-8 digits. Context can be used for
// timeouts and cancellation.
func (c *Client) Lookup(ctx context.Context, binNumber int) (*BINInfo, error) {
// Validate input
if binNumber < 1000 || binNumber > 99999999 {
return nil, &InvalidBINError{
BIN: binNumber,
Message: "BIN must be between 1000 and 99999999",
}
}
// Build request
reqBody, err := json.Marshal(binRequest{Number: binNumber})
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
// Execute request
resp, err := c.httpClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, &ServiceError{
StatusCode: 0,
Message: "request timed out",
}
}
if errors.Is(err, context.Canceled) {
return nil, fmt.Errorf("request canceled: %w", err)
}
return nil, &ServiceError{
StatusCode: 0,
Message: fmt.Sprintf("connection error: %v", err),
}
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// Handle error responses
switch resp.StatusCode {
case http.StatusOK:
// Success - continue to parse
case http.StatusBadRequest: // 400
var errResp struct {
Message string `json:"message"`
}
json.Unmarshal(body, &errResp)
return nil, &InvalidBINError{
BIN: binNumber,
Message: errResp.Message,
}
case http.StatusUnauthorized: // 401
return nil, &AuthenticationError{
Message: "invalid API key - check your credentials",
}
case http.StatusPaymentRequired: // 402
return nil, &AuthenticationError{
Message: "no active subscription - please subscribe to a plan",
}
case http.StatusForbidden: // 403
return nil, &AuthenticationError{
Message: "API key lacks required permissions",
}
case http.StatusNotFound: // 404
return nil, &BINNotFoundError{BIN: binNumber}
case http.StatusTooManyRequests: // 429
return nil, &QuotaExceededError{
Message: "daily quota exceeded - resets at midnight UTC",
}
case http.StatusBadGateway: // 502
return nil, &ServiceError{
StatusCode: resp.StatusCode,
Message: "upstream service unavailable - please try again",
}
default:
if resp.StatusCode >= 500 {
return nil, &ServiceError{
StatusCode: resp.StatusCode,
Message: "server error - please try again later",
}
}
return nil, &BINLookupError{
Err: errors.New("unexpected error"),
Message: fmt.Sprintf("HTTP %d", resp.StatusCode),
Code: resp.StatusCode,
}
}
// Parse successful response
var result binResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return &result.Data, nil
}
Step 5: Using the Client
Here’s how to use the production-ready client:
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"bin-lookup/binlookup"
)
func main() {
// Initialize with API key from environment
client, err := binlookup.NewClient("")
if err != nil {
log.Fatal(err)
}
// Or pass the key directly
// client, err := binlookup.NewClient("your_api_key_here")
// Create a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// Look up a BIN
info, err := client.Lookup(ctx, 42467101)
if err != nil {
// Handle specific error types
var invalidErr *binlookup.InvalidBINError
var authErr *binlookup.AuthenticationError
var quotaErr *binlookup.QuotaExceededError
var notFoundErr *binlookup.BINNotFoundError
var serviceErr *binlookup.ServiceError
switch {
case errors.As(err, &invalidErr):
fmt.Printf("Invalid BIN: %v\n", err)
case errors.As(err, &authErr):
fmt.Printf("Auth error: %v\n", err)
// Check your API key
case errors.As(err, "aErr):
fmt.Printf("Quota exceeded: %v\n", err)
// Wait until midnight UTC or upgrade your plan
case errors.As(err, ¬FoundErr):
fmt.Printf("BIN not found: %v\n", err)
// This BIN isn't in the database
case errors.As(err, &serviceErr):
fmt.Printf("Service error: %v\n", err)
// Retry with exponential backoff
default:
fmt.Printf("Unexpected error: %v\n", err)
}
return
}
fmt.Printf("Card Network: %s\n", info.Scheme)
fmt.Printf("Card Type: %s\n", info.Funding)
if info.Issuer.Name != nil {
fmt.Printf("Issuer: %s\n", *info.Issuer.Name)
}
fmt.Printf("Country: %s\n", info.Country.Name)
fmt.Printf("Is Prepaid: %t\n", info.Prepaid)
}
Go’s errors.As function lets you check for specific error types, while errors.Is checks against sentinel errors like ErrBINNotFound. Use the type assertions for detailed information.
Step 6: Concurrent BIN Lookups with Goroutines
For high-throughput applications, here’s how to perform concurrent lookups using goroutines and channels:
package main
import (
"context"
"fmt"
"sync"
"time"
"bin-lookup/binlookup"
)
// LookupResult holds the result of a BIN lookup
type LookupResult struct {
BIN int
Info *binlookup.BINInfo
Error error
}
// LookupMany performs concurrent BIN lookups
func LookupMany(ctx context.Context, client *binlookup.Client, bins []int) []LookupResult {
results := make([]LookupResult, len(bins))
var wg sync.WaitGroup
// Create a semaphore to limit concurrent requests
semaphore := make(chan struct{}, 10) // Max 10 concurrent requests
for i, bin := range bins {
wg.Add(1)
go func(index int, binNumber int) {
defer wg.Done()
// Acquire semaphore
semaphore <- struct{}{}
defer func() { <-semaphore }()
info, err := client.Lookup(ctx, binNumber)
results[index] = LookupResult{
BIN: binNumber,
Info: info,
Error: err,
}
}(i, bin)
}
wg.Wait()
return results
}
// LookupManyWithChannel uses channels for streaming results
func LookupManyWithChannel(
ctx context.Context,
client *binlookup.Client,
bins []int,
) <-chan LookupResult {
results := make(chan LookupResult, len(bins))
go func() {
defer close(results)
var wg sync.WaitGroup
semaphore := make(chan struct{}, 10)
for _, bin := range bins {
wg.Add(1)
go func(binNumber int) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
info, err := client.Lookup(ctx, binNumber)
select {
case results <- LookupResult{BIN: binNumber, Info: info, Error: err}:
case <-ctx.Done():
return
}
}(bin)
}
wg.Wait()
}()
return results
}
func main() {
client, err := binlookup.NewClient("")
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
bins := []int{42467101, 51234567, 37123456, 60110000, 35281234}
// Option 1: Wait for all results
fmt.Println("=== Batch Lookup ===")
results := LookupMany(ctx, client, bins)
for _, r := range results {
if r.Error != nil {
fmt.Printf("BIN %d: Error - %v\n", r.BIN, r.Error)
} else {
fmt.Printf("BIN %d: %s (%s)\n", r.BIN, r.Info.Scheme, r.Info.Country.Name)
}
}
// Option 2: Stream results as they arrive
fmt.Println("\n=== Streaming Lookup ===")
for result := range LookupManyWithChannel(ctx, client, bins) {
if result.Error != nil {
fmt.Printf("BIN %d: Error - %v\n", result.BIN, result.Error)
} else {
fmt.Printf("BIN %d: %s (%s)\n", result.BIN, result.Info.Scheme, result.Info.Country.Name)
}
}
}
The semaphore pattern limits concurrent requests to prevent overwhelming the API. Adjust the buffer size based on your plan’s rate limits.
Step 7: Real-World Example — Payment Form Validation
Here’s a practical example of using BIN lookup in a payment flow:
package main
import (
"context"
"errors"
"fmt"
"strings"
"time"
"unicode"
"bin-lookup/binlookup"
)
// CardValidationResult holds the result of card validation
type CardValidationResult struct {
Valid bool
CardType string
Issuer string
Country string
IsPrepaid bool
Warnings []string
Error string
}
// CardValidator validates payment cards using BIN lookup
type CardValidator struct {
client *binlookup.Client
blockPrepaid bool
allowedCountries map[string]bool
}
// NewCardValidator creates a new card validator
func NewCardValidator(client *binlookup.Client) *CardValidator {
return &CardValidator{
client: client,
}
}
// BlockPrepaid configures the validator to reject prepaid cards
func (v *CardValidator) BlockPrepaid() *CardValidator {
v.blockPrepaid = true
return v
}
// AllowCountries restricts cards to specific countries
func (v *CardValidator) AllowCountries(codes ...string) *CardValidator {
v.allowedCountries = make(map[string]bool)
for _, code := range codes {
v.allowedCountries[strings.ToUpper(code)] = true
}
return v
}
// Validate checks if a card number is acceptable for payment
func (v *CardValidator) Validate(ctx context.Context, cardNumber string) CardValidationResult {
var warnings []string
// Extract digits only
var digits strings.Builder
for _, r := range cardNumber {
if unicode.IsDigit(r) {
digits.WriteRune(r)
}
}
digitsStr := digits.String()
if len(digitsStr) < 6 {
return CardValidationResult{
Valid: false,
Error: "Card number must be at least 6 digits",
}
}
// Extract BIN (first 8 digits, or all if less)
binStr := digitsStr
if len(binStr) > 8 {
binStr = binStr[:8]
}
var binNumber int
fmt.Sscanf(binStr, "%d", &binNumber)
// Look up BIN
info, err := v.client.Lookup(ctx, binNumber)
if err != nil {
// Handle specific errors
var notFoundErr *binlookup.BINNotFoundError
var quotaErr *binlookup.QuotaExceededError
if errors.As(err, ¬FoundErr) {
return CardValidationResult{
Valid: false,
Error: "Unable to identify card. Please check the number.",
}
}
if errors.As(err, "aErr) {
// Fail open - allow the transaction but log the issue
return CardValidationResult{
Valid: true,
Warnings: []string{"BIN validation skipped due to quota limits"},
}
}
// Fail open for service errors
return CardValidationResult{
Valid: true,
Warnings: []string{fmt.Sprintf("BIN validation unavailable: %v", err)},
}
}
// Get issuer name safely
issuerName := ""
if info.Issuer.Name != nil {
issuerName = *info.Issuer.Name
}
// Check prepaid status
if v.blockPrepaid && info.Prepaid {
return CardValidationResult{
Valid: false,
CardType: info.Scheme,
Issuer: issuerName,
Country: info.Country.Code,
IsPrepaid: true,
Error: "Prepaid cards are not accepted",
}
}
// Check country restrictions
if v.allowedCountries != nil && !v.allowedCountries[info.Country.Code] {
return CardValidationResult{
Valid: false,
CardType: info.Scheme,
Issuer: issuerName,
Country: info.Country.Code,
Error: fmt.Sprintf("Cards from %s are not accepted", info.Country.Name),
}
}
// Add warnings for high-risk indicators
if info.Prepaid {
warnings = append(warnings, "This is a prepaid card")
}
if info.Commercial {
warnings = append(warnings, "This is a corporate/business card")
}
return CardValidationResult{
Valid: true,
CardType: info.Scheme,
Issuer: issuerName,
Country: info.Country.Code,
IsPrepaid: info.Prepaid,
Warnings: warnings,
}
}
func main() {
client, err := binlookup.NewClient("")
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// Create a validator that blocks prepaid cards
// and only allows cards from US, GB, CA, AU
validator := NewCardValidator(client).
BlockPrepaid().
AllowCountries("US", "GB", "CA", "AU")
// Validate a card
result := validator.Validate(ctx, "4246-7100-1234-5678")
if result.Valid {
fmt.Printf("Card accepted: %s from %s\n", result.CardType, result.Issuer)
for _, warning := range result.Warnings {
fmt.Printf("Warning: %s\n", warning)
}
} else {
fmt.Printf("Card rejected: %s\n", 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
- ✓ Use context.Context for timeouts and cancellation
- ✓ Implement retry logic with exponential backoff for transient errors
- ✓ Use goroutines with semaphores for concurrent lookups
- ✓ Consider fail-open strategy for non-critical validation
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.