macadress

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 12 Imported by: 0

README

macadress-go

Official Go client for the macadress.com MAC address and OUI vendor lookup API.

  • Vendor name, OUI, IEEE block, country, address type, EUI-64 / IPv6 link-local, randomization confidence, device guess
  • Keyless vendor-name lookup, plus keyed single / batch / directory-search calls
  • Typed results with a Raw escape hatch, typed errors with errors.Is / errors.As
  • context.Context on every call, no dependencies outside the standard library
import macadress "github.com/sapisos/macadress-go"

mac := macadress.New("mk_live_xxx")

name, _ := mac.Vendor(ctx, "00:03:93:AB:12:34")  // "Apple, Inc."   (no API key required)
res, _ := mac.Lookup(ctx, "00:03:93:AB:12:34")   // res.Country == "US"

Install

go get github.com/sapisos/macadress-go

Requires Go 1.23 or newer. The import path is github.com/sapisos/macadress-go; the package name is macadress.

Getting a key

Vendor needs no key. Everything else does. A free key (1,000 lookups a day) is instant at macadress.com/signup; see pricing for more.

Usage

Create a client
mac := macadress.New("mk_live_xxx")

// keyless: only Vendor will work
anon := macadress.New("")

// options
mac := macadress.New("mk_live_xxx",
    macadress.WithBaseURL("https://api.macadress.com"), // change only for a self-hosted deployment
    macadress.WithTimeout(10*time.Second),
    macadress.WithHTTPClient(myClient),
)

The *Client is safe for concurrent use.

Vendor - name only, no key

Returns an empty string (and a nil error) when the address is valid but has no vendor to report (unregistered, private, or locally administered / randomized).

mac.Vendor(ctx, "00:03:93:AB:12:34")   // "Apple, Inc.", nil
mac.Vendor(ctx, "02:1a:2b:3c:4d:5e")   // "", nil

:, -, . and space grouping are all accepted, as is a bare 12-hex string.

Lookup - full analysis
r, err := mac.Lookup(ctx, "3C:22:FB:12:34:56")

r.Organization            // string
r.VendorLookupReliable    // bool  (false for a private block / LAA)
r.OUI                     // "3C:22:FB"
r.MatchedPrefix           // full matched block at its real width
r.BlockType               // macadress.BlockMAL, etc.
r.Country                 // "US"
r.AdministrationType      // macadress.UniversallyAdministered | LocallyAdministered
r.PotentiallyRandomized   // bool
r.RandomizationConfidence // macadress.RandomizationNone | Possible | Likely
r.EUI64                   // "3E:22:FB:FF:FE:12:34:56"
r.IPv6LinkLocal           // "fe80::3e22:fbff:fe12:3456"
r.Device.Category         // macadress.DeviceUnknown (usually)
r.Explanation             // plain-English summary
r.Meta.DatabaseVersion    // "2026-08-30"

Any field the struct does not cover is still reachable:

r.Get("vendor_location.city")   // (any, bool)
r.Raw                           // map[string]any, the decoded payload
Batch - up to 100 at once

Results come back in input order; check each item.

items, err := mac.Batch(ctx, []string{"00:03:93:00:00:00", "3C:22:FB:00:00:00", "bad"})
for _, it := range items {
    if it.Failed() {
        fmt.Printf("%s -> ERROR %s\n", it.Input, it.Err)
    } else {
        fmt.Printf("%s -> %s\n", it.Input, it.Organization)
    }
}

Batch returns an error without making a request if the slice is empty or longer than macadress.MaxBatchSize (100).

SearchVendors - the directory
res, err := mac.SearchVendors(ctx, "Cisco", macadress.WithCountry("US"), macadress.WithLimit(20))

res.Total   // total matches, ignoring the limit
for _, b := range res.Blocks {
    fmt.Printf("%s %s (%s)\n", b.BlockType, b.Organization, b.Country)
}
Health
mac.Health(ctx)   // (bool, error); a transport failure is (false, nil)

Errors

res, err := mac.Lookup(ctx, input)

switch {
case errors.Is(err, macadress.ErrRateLimited):
    var apiErr *macadress.APIError
    errors.As(err, &apiErr)
    time.Sleep(apiErr.RetryAfter)
case errors.Is(err, macadress.ErrInvalidMAC):
    // caller mistake, never billed
case err != nil:
    var te *macadress.TransportError
    if errors.As(err, &te) {
        // never reached the API
    }
}
Sentinel / type When
ErrInvalidMAC HTTP 400, the input did not parse
ErrAuth HTTP 401, missing or invalid API key
ErrRateLimited HTTP 429, per-minute rate exceeded
ErrQuota HTTP 429, billing-cycle quota spent (also matches ErrRateLimited)
*APIError any non-2xx: carries StatusCode, Message, RequestID, RetryAfter, Body
*TransportError never reached the API: DNS, connection, TLS, timeout, canceled context

errors.Is(err, context.DeadlineExceeded) and context.Canceled work through *TransportError.

Development

go test -race ./...
go vet ./...
gofmt -l .

License

MIT, see LICENSE. A product of ApisOS FZE.

Documentation

Overview

Package macadress is the official Go client for the macadress.com MAC address and OUI vendor lookup API.

The zero-configuration client talks to the hosted API. A key is only needed for everything beyond the plain-text vendor lookup:

mac := macadress.New("mk_live_xxx") // or macadress.New("") for keyless

name, err := mac.Vendor(ctx, "00:03:93:AB:12:34") // "Apple, Inc.", nil
res, err := mac.Lookup(ctx, "3C:22:FB:12:34:56")  // *MacResult
items, err := mac.Batch(ctx, addrs)               // []BatchItem
hits, err := mac.SearchVendors(ctx, "Cisco", macadress.WithCountry("US"))

Every call takes a context.Context. Failures are errors: transport problems come back as *TransportError, API error responses as *APIError, and the common cases (400, 401, 429) also match the sentinels ErrInvalidMAC, ErrAuth, ErrRateLimited and ErrQuota via errors.Is.

The client has no dependencies outside the standard library.

Index

Examples

Constants

View Source
const MaxBatchSize = 100

MaxBatchSize is the most addresses Batch will send in one request.

View Source
const Version = "1.0.0"

Version is the client version, sent in the User-Agent header. Keep it in step with the CHANGELOG heading and the release tag.

Variables

View Source
var (
	// ErrInvalidMAC is an HTTP 400: the address or batch body did not parse.
	ErrInvalidMAC = errors.New("macadress: invalid MAC address")
	// ErrAuth is an HTTP 401: the API key is missing or invalid.
	ErrAuth = errors.New("macadress: invalid or missing API key")
	// ErrRateLimited is an HTTP 429 from the per-minute rate limiter. A
	// quota error also matches this, the same way it does in the other
	// clients.
	ErrRateLimited = errors.New("macadress: rate limit exceeded")
	// ErrQuota is an HTTP 429 where the billing-cycle quota is spent.
	ErrQuota = errors.New("macadress: quota exceeded")
)

Sentinel errors for the well-known API failure modes. Match them with errors.Is; reach for the *APIError with errors.As when you need the status code, request id or Retry-After.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	StatusCode int
	Message    string
	RequestID  string
	// RetryAfter is the Retry-After header as a duration, or 0 if absent.
	RetryAfter time.Duration
	// Body is the decoded JSON error body, when the API sent one.
	Body map[string]any
	// contains filtered or unexported fields
}

APIError is returned when the API responds with a non-2xx status, or with a body the client cannot read.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

func (e *APIError) Is(target error) bool

Is reports whether the error matches one of the package sentinels. A quota error reports true for both ErrQuota and ErrRateLimited.

type AdministrationType

type AdministrationType string

AdministrationType is whether the address is universally (IEEE-assigned) or locally administered.

const (
	UniversallyAdministered AdministrationType = "universally_administered"
	LocallyAdministered     AdministrationType = "locally_administered"
)

type BatchItem

type BatchItem struct {
	MacResult
	Input string `json:"input"`
	Err   string `json:"error"`
}

BatchItem is one entry of a Batch response: a full MacResult plus the original input string, and an Err message when that one address could not be resolved.

func (BatchItem) Failed

func (b BatchItem) Failed() bool

Failed reports whether this entry could not be resolved. When it does, the MacResult fields are unset and Err holds the reason.

type BlockType

type BlockType string

BlockType is the IEEE registry an assignment comes from. MA-L is a /24 (the classic OUI), MA-M a /28, MA-S a /36; IAB and CID are legacy blocks. An unrecognised value from a newer API version is kept as-is rather than rejected.

const (
	BlockMAL BlockType = "MA-L"
	BlockMAM BlockType = "MA-M"
	BlockMAS BlockType = "MA-S"
	BlockIAB BlockType = "IAB"
	BlockCID BlockType = "CID"
)

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a macadress.com API client. It is safe for concurrent use. Create one with New.

func New

func New(apiKey string, opts ...Option) *Client

New returns a Client. Pass an empty apiKey for keyless use, where only Vendor works.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL reports the API root the client is configured with.

func (*Client) Batch

func (c *Client) Batch(ctx context.Context, macs []string) ([]BatchItem, error)

Batch looks up up to MaxBatchSize addresses in one request. It needs an API key. Results come back in input order; check Failed on each item.

It returns an error without making a request if macs is empty or longer than MaxBatchSize.

func (*Client) Do

func (c *Client) Do(ctx context.Context, method, path string, query url.Values, body any) (*Response, error)

Do performs a raw request against path (for example "/v1/healthz") and returns the response as-is. Nothing is treated as an error for a non-2xx status; only a transport failure returns a non-nil error. If body is non-nil it is sent as JSON.

func (*Client) Health

func (c *Client) Health(ctx context.Context) (bool, error)

Health reports whether the API and its database are reachable. It is keyless and not counted against any quota.

A transport failure (unreachable, timeout) returns (false, nil): not reachable means not healthy. A non-nil error is only returned when the request itself could not be built.

func (*Client) Lookup

func (c *Client) Lookup(ctx context.Context, mac string) (*MacResult, error)

Lookup returns the full analysis of one address. It needs an API key.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	macadress "github.com/sapisos/macadress-go"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = io.WriteString(w, `{"organization":"Apple, Inc.","country":"US","block_type":"MA-L"}`)
	}))
	defer srv.Close()

	mac := macadress.New("mk_live_xxx", macadress.WithBaseURL(srv.URL))

	res, err := mac.Lookup(context.Background(), "3C:22:FB:12:34:56")
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s / %s / %s\n", res.Organization, res.Country, res.BlockType)
}
Output:
Apple, Inc. / US / MA-L
Example (RateLimited)
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	macadress "github.com/sapisos/macadress-go"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Retry-After", "5")
		w.WriteHeader(http.StatusTooManyRequests)
		_, _ = io.WriteString(w, `{"error":"rate limit exceeded"}`)
	}))
	defer srv.Close()

	mac := macadress.New("mk_live_xxx", macadress.WithBaseURL(srv.URL))

	_, err := mac.Lookup(context.Background(), "3C:22:FB:12:34:56")
	if errors.Is(err, macadress.ErrRateLimited) {
		var apiErr *macadress.APIError
		errors.As(err, &apiErr)
		fmt.Println("back off for", apiErr.RetryAfter)
	}
}
Output:
back off for 5s

func (*Client) SearchVendors

func (c *Client) SearchVendors(ctx context.Context, query string, opts ...SearchOption) (*VendorSearchResult, error)

SearchVendors searches the registered vendor/block directory by organization name and/or country. It needs an API key and counts as one call against the plan quota.

func (*Client) Vendor

func (c *Client) Vendor(ctx context.Context, mac string) (string, error)

Vendor returns the registered organization name for a MAC address as plain text. It needs no API key.

It returns an empty string (and a nil error) when the address is valid but has no vendor to report: an unregistered prefix, a private block, or a locally administered (usually privacy-randomized) address. Use Lookup to tell those cases apart.

The address may use ":", "-", "." or space grouping, or be a bare 12-hex-digit string.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	macadress "github.com/sapisos/macadress-go"
)

func main() {
	// A stand-in for api.macadress.com. In real code, drop WithBaseURL.
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = io.WriteString(w, "Apple, Inc.")
	}))
	defer srv.Close()

	mac := macadress.New("", macadress.WithBaseURL(srv.URL)) // keyless

	name, err := mac.Vendor(context.Background(), "00:03:93:AB:12:34")
	if err != nil {
		panic(err)
	}
	fmt.Println(name)
}
Output:
Apple, Inc.

type Device

type Device struct {
	Category           DeviceCategory   `json:"category"`
	PossibleCategories []DeviceCategory `json:"possible_categories"`
	Confidence         string           `json:"confidence"`
	InferenceSource    string           `json:"inference_source"`
	ExactModelKnown    bool             `json:"exact_model_known"`

	Raw map[string]any `json:"-"`
}

Device is the device block of a lookup result. A MAC address alone rarely determines a device type, so Category is "unknown" for most registrations and ExactModelKnown is effectively always false.

type DeviceCategory

type DeviceCategory string

DeviceCategory is the controlled device taxonomy from the device.category field. Unknown is by far the most common value.

const (
	DeviceComputer            DeviceCategory = "computer"
	DeviceSmartphone          DeviceCategory = "smartphone"
	DeviceTablet              DeviceCategory = "tablet"
	DeviceRouter              DeviceCategory = "router"
	DeviceSwitch              DeviceCategory = "switch"
	DeviceWirelessAccessPoint DeviceCategory = "wireless_access_point"
	DeviceFirewall            DeviceCategory = "firewall"
	DeviceServer              DeviceCategory = "server"
	DeviceStorage             DeviceCategory = "storage"
	DevicePrinter             DeviceCategory = "printer"
	DeviceCamera              DeviceCategory = "camera"
	DeviceSmartTV             DeviceCategory = "smart_tv"
	DeviceMediaDevice         DeviceCategory = "media_device"
	DeviceGamingConsole       DeviceCategory = "gaming_console"
	DeviceIoT                 DeviceCategory = "iot"
	DeviceEmbeddedDevice      DeviceCategory = "embedded_device"
	DeviceIndustrial          DeviceCategory = "industrial"
	DeviceMedical             DeviceCategory = "medical"
	DeviceAutomotive          DeviceCategory = "automotive"
	DeviceVirtualMachine      DeviceCategory = "virtual_machine"
	DeviceContainer           DeviceCategory = "container"
	DeviceNetworkInterface    DeviceCategory = "network_interface"
	DeviceConsumerElectronics DeviceCategory = "consumer_electronics"
	DeviceUnknown             DeviceCategory = "unknown"
)

type MacResult

type MacResult struct {
	MAC                     string                  `json:"mac"`
	Valid                   bool                    `json:"valid"`
	OUI                     string                  `json:"oui"`
	Registered              bool                    `json:"registered"`
	Organization            string                  `json:"organization"`
	VendorAddress           string                  `json:"vendor_address"`
	Country                 string                  `json:"country"`
	BlockType               BlockType               `json:"block_type"`
	MatchedPrefix           string                  `json:"matched_prefix"`
	PrefixLength            int                     `json:"prefix_length"`
	AddressCapacity         int64                   `json:"address_capacity"`
	RangeStart              string                  `json:"range_start"`
	RangeEnd                string                  `json:"range_end"`
	TransmissionType        TransmissionType        `json:"transmission_type"`
	AdministrationType      AdministrationType      `json:"administration_type"`
	LocallyAdministered     bool                    `json:"locally_administered"`
	SLAPQuadrant            string                  `json:"slap_quadrant"`
	EUI64                   string                  `json:"eui64"`
	IPv6LinkLocal           string                  `json:"ipv6_link_local"`
	PotentiallyRandomized   bool                    `json:"potentially_randomized"`
	RandomizationConfidence RandomizationConfidence `json:"randomization_confidence"`
	// VendorLookupReliable is false when the organization cannot be trusted
	// for this address specifically (a locally administered address, a
	// private block).
	VendorLookupReliable bool   `json:"vendor_lookup_reliable"`
	IsZero               bool   `json:"is_zero"`
	IsBroadcast          bool   `json:"is_broadcast"`
	Explanation          string `json:"explanation"`
	Device               Device `json:"device"`
	Meta                 Meta   `json:"meta"`

	// Raw is the decoded JSON object exactly as the API returned it.
	Raw map[string]any `json:"-"`
}

MacResult is the full analysis of one address, from Lookup and from each item of Batch. A result is always returned, registered or not: check Registered rather than expecting an error.

Fields the typed struct does not cover stay reachable through Raw and Get; the API only ever adds fields, so this keeps working against newer response shapes.

func (*MacResult) Get

func (r *MacResult) Get(path string) (any, bool)

Get fetches a value from the raw payload by dotted path, e.g. "meta.database_version". The second return is false if the path is absent.

type Meta

type Meta struct {
	RequestID       string `json:"request_id"`
	DatabaseVersion string `json:"database_version"`
	Cached          bool   `json:"cached"`
}

Meta carries per-response bookkeeping.

type Option

type Option func(*Client)

Option configures a Client in New.

func WithBaseURL

func WithBaseURL(raw string) Option

WithBaseURL points the client at a different API root, for a self-hosted deployment. A trailing slash is trimmed.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies the *http.Client to use for requests. Combine it with WithTimeout by passing WithHTTPClient first.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the whole-request timeout on the client's HTTP client.

func WithUserAgent

func WithUserAgent(s string) Option

WithUserAgent overrides the User-Agent header.

type RandomizationConfidence

type RandomizationConfidence string

RandomizationConfidence is how strongly the address looks like an OS privacy-randomized MAC.

const (
	RandomizationNone     RandomizationConfidence = "none"
	RandomizationPossible RandomizationConfidence = "possible"
	RandomizationLikely   RandomizationConfidence = "likely"
)

type Response

type Response struct {
	StatusCode int
	Header     http.Header
	Body       []byte
}

Response is a raw HTTP response with its body already read. Returned by Do.

func (*Response) JSON

func (r *Response) JSON(v any) error

JSON decodes the response body into v.

type SearchOption

type SearchOption func(url.Values)

SearchOption narrows a SearchVendors query.

func WithCountry

func WithCountry(code string) SearchOption

WithCountry filters to an ISO 3166-1 alpha-2 country.

func WithLimit

func WithLimit(n int) SearchOption

WithLimit caps the number of blocks returned for the page.

type TransmissionType

type TransmissionType string

TransmissionType classifies the destination: unicast, multicast or broadcast.

const (
	Unicast   TransmissionType = "unicast"
	Multicast TransmissionType = "multicast"
	Broadcast TransmissionType = "broadcast"
)

type TransportError

type TransportError struct {
	Err error
}

TransportError wraps a failure to get an HTTP response at all: DNS, connection, TLS, timeout, or a canceled context. The cause is available through errors.Unwrap / errors.Is / errors.As.

func (*TransportError) Error

func (e *TransportError) Error() string

func (*TransportError) Unwrap

func (e *TransportError) Unwrap() error

type VendorBlock

type VendorBlock struct {
	PrefixInt     int64     `json:"prefix_int"`
	MaskBits      int       `json:"mask_bits"`
	BlockType     BlockType `json:"block_type"`
	Organization  string    `json:"organization"`
	Address       string    `json:"address"`
	Country       string    `json:"country"`
	IsPrivate     bool      `json:"is_private"`
	FirstSeenAt   string    `json:"first_seen_at"`
	LastChangedAt string    `json:"last_changed_at"`
}

VendorBlock is one IEEE-assigned MAC/OUI prefix block from SearchVendors.

type VendorSearchResult

type VendorSearchResult struct {
	Total  int           `json:"total"`
	Blocks []VendorBlock `json:"blocks"`

	Raw map[string]any `json:"-"`
}

VendorSearchResult is the result of SearchVendors: the matching blocks for this page plus the total match count, which ignores the limit.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL