← Back to Blog

BIN Lookup API for Go: Complete Integration Guide

Published: February 5, 2026
Tags: go, golang, tutorial, integration, developer

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.

What You'll Build
  • 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.

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 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. 1 Navigate to API Keys in the dashboard
  2. 2 Click Create Key
  3. 3 Give it a name (e.g., "Go 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:

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, &quotaErr):
			fmt.Printf("Quota exceeded: %v\n", err)
			// Wait until midnight UTC or upgrade your plan

		case errors.As(err, &notFoundErr):
			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)
}
* Error Handling Pattern

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)
		}
	}
}
i Rate Limiting

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, &notFoundErr) {
			return CardValidationResult{
				Valid: false,
				Error: "Unable to identify card. Please check the number.",
			}
		}

		if errors.As(err, &quotaErr) {
			// 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)
	}
}
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
  • 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

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