batchdata

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 13 Imported by: 0

README

🏠  go-batchdata

A small, typed Go SDK for the BatchData V3 API: synchronous property skip tracing.


Release Go Version License


CI / CD    Build Last Commit      Quality    Coverage
Security    Scorecard Security      Community    Contributors Bitcoin


Project Navigation
🚀 Installation 🏁 Quick Start 💡 Usage ⚙️ Behavior 📚 Documentation
🧪 Examples & Tests ⚡ Benchmarks 🛠️ Code Standards 🤖 AI Usage 🤝 Contributing

go-batchdata calls BatchData's V3 property skip-trace endpoint: you send up to 100 properties, by address or by assessor's parcel number (APN), and get back the persons BatchData links to each property, with their phones, emails and addresses. Use it when a Go service needs owner contact data and has to stay in control of every billable call.

  • One attempt per call. No retries, redirects, replays, chunking or polling: the SDK never repeats a billable request on its own.
  • Typed results that keep BatchData's meaning. An absent flag is nil, not false; an absent count is nil, not 0; unknown enum values are kept as sent.
  • No guessing. A returned record is linked to your request item only by evidence, never by position, name or address.
  • Nothing implicit. No environment reads for the token, no injected option defaults, no globals.
  • Private by construction. Errors and printed clients never contain the token, URLs, bodies or personal data.
  • Zero runtime dependencies and bounded memory, time and response size.

Version 1 covers synchronous forward skip tracing (POST /api/v3/property/skip-trace) only; see what is not covered.


📦 Installation

go-batchdata requires a supported release of Go (Go 1.26 or newer).

go get github.com/mrz1836/go-batchdata

The library has zero third-party runtime dependencies: the root package and skiptrace use only the standard library. testify is used only by the tests and their internal helper package.


Credentials

You need a BatchData API token, sent as a bearer token; see BatchData's guide to authentication and tokens. go-batchdata never reads an environment variable or a configuration file for it: your code passes the token to batchdata.NewClient, from wherever it keeps secrets.

Token Use
Production token Real data, billed per matched record.
Sandbox token Same host, free mock data that varies and may lack attributes. The cheapest way to try the SDK against the real API.

🏁 Quick Start

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/mrz1836/go-batchdata"
	"github.com/mrz1836/go-batchdata/skiptrace"
)

func main() {
	// The application chooses where the token comes from; the SDK reads no environment.
	client, err := batchdata.NewClient(os.Getenv("BATCHDATA_API_TOKEN"))
	if err != nil {
		log.Fatal(err) // ErrInvalidConfig: no call was made
	}
	tracer := skiptrace.New(client)

	res, err := tracer.Trace(context.Background(), skiptrace.Request{
		Items: []skiptrace.Item{{
			PropertyAddress: skiptrace.Address{Street: "100 Example Ave", City: "Testville", State: "ZZ", Zip: "00000"},
		}},
	})
	if err != nil {
		log.Fatal(err) // the call may already have been processed and billed: do not resend blindly
	}
	for _, rec := range res.RecordsFor(0) {
		fmt.Println(rec.Outcome, len(rec.Persons))
	}
}
Next steps

💡 Usage

Set up once

Create one Client per token and one skiptrace.Service, and share them: both are immutable and safe for concurrent use.

client, err := batchdata.NewClient(apiKey,
	batchdata.WithTimeout(45*time.Second),      // the whole call, including the body read
	batchdata.WithUserAgent("example-app/1.0"), // replaces "go-batchdata"
)
if err != nil {
	return err // errors.Is(err, batchdata.ErrInvalidConfig); no call was made
}
tracer := skiptrace.New(client)
Option Default Accepted values
WithTimeout(d) 60s (DefaultTimeout) d > 0. An earlier deadline on the call's context wins.
WithMaxResponseBytes(n) 8 MiB (DefaultMaxResponseBytes) 1 byte to 64 MiB. A larger response fails as KindResponseTooLarge.
WithUserAgent(ua) go-batchdata 1 to 256 bytes of visible ASCII or spaces, starting and ending with a visible character.
WithHTTPClient(hc) a transport the SDK builds A non-nil *http.Client; see the transport.
WithTestEndpoint(url) https://api.batchdata.com Tests only: https on 127.0.0.0/8 or [::1], an optional port, and no path, query, fragment or userinfo.

Nil options are skipped, options apply in order, and the last option of each kind wins. NewClient validates the token (1 to 4096 bytes of visible ASCII) and every option before returning, and performs no I/O. Its errors name the argument or option, never the rejected value.

Holding many tokens? Without WithHTTPClient, each Client owns a connection pool that cannot be closed (idle connections close on their own after 90 seconds). Pass the same *http.Client to every Client so that they share one pool.

Inputs and limits

A request carries 1 to 100 items (skiptrace.MaxItems). Each item must satisfy at least one of the combinations BatchData documents:

Lookup Fields
Address PropertyAddress with Street, City, State and Zip
Address PropertyAddress with Street and Zip
Address PropertyAddress with Street, City and State
Parcel APN, County and State
Parcel APN and CountyFIPSCode (exactly 5 ASCII digits)

Name and MailingAddress are optional extras, never lookup keys, and extra fields alongside a satisfied combination are allowed. Request.Validate, which Trace runs before any request, also rejects a string that is invalid UTF-8 or only whitespace, an address set without a Street, and a DateFormat other than the two constants. Values are sent as given: the SDK never trims, normalizes or rewrites them. The encoded request must fit in 1 MiB (batchdata.MaxRequestBytes).

A request that fails any check returns a *batchdata.ValidationError matching batchdata.ErrInvalidRequest, naming the first problem by field path, such as Items[3].PropertyAddress.Street. No call is made.

One property, or many

A single property is a one-item request; a batch is up to 100 items in one call. BatchData notes that large synchronous batches respond more slowly.

many := skiptrace.Request{
	Items: []skiptrace.Item{
		{RequestID: "row-1", PropertyAddress: skiptrace.Address{Street: "100 Example Ave", City: "Testville", State: "ZZ"}},
		{RequestID: "row-2", APN: "000-000-0000", CountyFIPSCode: "00000"},
	},
	Options: skiptrace.Options{IncludeTCPABlacklistedPhones: new(true), DateFormat: skiptrace.DateFormatISODate},
}
res, err := tracer.Trace(ctx, many)
Options

An option you leave unset is omitted from the request, so BatchData's own default applies. The SDK never injects a default.

Option Meaning
IncludeTCPABlacklistedPhones true includes phones with TCPA restrictions, each flagged by Phone.TCPA; false filters them out.
PrioritizeMobilePhones true (BatchData's default) lists mobile numbers first; false leaves the order alone.
DateFormat DateFormatISODateTime (BatchData's default) or DateFormatISODate, for Person.DOB.

Set IncludeTCPABlacklistedPhones explicitly. BatchData's documentation contradicts itself: the option is described as defaulting to true in V3, but the tcpa field is described as appearing only when the option is true, with TCPA-restricted phones excluded when it is not specified.

Reading results

Result.Records holds one entry per element of BatchData's data array, in BatchData's order: never reordered, deduplicated or truncated. Each record has an Outcome:

Outcome Meaning
OutcomeMatched BatchData set meta.matched to true: it found data for the property.
OutcomeNoMatch meta.matched is false. A valid answer, not an error; Persons is usually an empty slice.
OutcomeFailed BatchData set meta.error to true. This wins over meta.matched, and BatchData documents no detail for it.
OutcomeUnreported No meta.matched flag was sent.
OutcomeUndecodable The element was null, not an object, or held a value of the wrong type. Nothing of it is kept: its Association is AssociationNone and every other field is zero.

A failed or undecodable record never turns into an error: Trace returns the Result with every paid record in it.

for _, rec := range res.Records {
	switch rec.Outcome {
	case skiptrace.OutcomeMatched:
		for _, p := range rec.Persons {
			for _, ph := range p.Phones {
				if ph.TCPA == nil {
					// not reported: absent is not false
				}
				_ = ph.Rank // presentation order, not a probability
			}
		}
	case skiptrace.OutcomeNoMatch: // a valid answer
	case skiptrace.OutcomeFailed, skiptrace.OutcomeUnreported, skiptrace.OutcomeUndecodable:
	}
}
billed := res.Meta.Results.MatchCount // *int; nil means not reported
supportID := res.Meta.CallRequestID   // identifies the call, not an item

Good to know about the data:

  • A match is about the property, not the name you sent. Name is not a lookup key, and a matched record can describe whoever BatchData links to the address, such as a later resident. Compare each person's Name.Full and Name.AKAs with the name you hold, as BatchData's own field documentation advises.
  • Flags are pointers. nil means BatchData did not send the flag. PropertyOwner is a name match against the owner of record, not proof of ownership; nil means it could not be evaluated. Deceased and DOB are nil when no value is on file.
  • Ranks are presentation order, starting at 1, not measured confidence; 0 means not reported.
  • Documented maxima are documentation only. BatchData documents at most 3 persons, 5 phones, 3 emails, 3 addresses and 3 AKAs; extra entries are kept.
  • Unknown values are kept as sent: PhoneType, AddressValidity and batchdata.WarningCode.
  • DOB stays a string, in whichever format DateFormat selected; the SDK never parses it.
  • Flags are provider data, not consent or compliance decisions. DNC, TCPA and Litigator describe what BatchData knows; whether and how you contact anyone is your decision and your responsibility.
Linking records to items

Record.Association says what links a record to a request item, and Result.RecordsFor(i) returns the records linked to Items[i]. The rule is strict: never by position, name or address.

Association When
AssociationRequestID The record echoes an input.requestId equal to the RequestID of exactly one item. Every record echoing it links to that item.
AssociationSingleItem The request had one item and the response one decodable record, and either the item had no RequestID or the record echoed none (a provider-generated echo is covered too).
AssociationAmbiguous The echo matches an item's RequestID, but more than one item has it, or another item's differs only in case or surrounding whitespace.
AssociationNone Anything else: no echo in a multi-item request, an echo that matches no item, a single item whose ID differs from the echo, or an undecodable record.

Give every item of a batch a distinct RequestID, and expect AssociationNone until BatchData confirms the echo. The V3 request schema documents requestId for correlation, but the V3 response schema does not list it; only BatchData's request ID correlation article describes the echo, with examples in an older shape. BatchData does not document whether data[i] answers requests[i], so the SDK never assumes it. When RecordsFor returns nothing, each record's echoed Input is still there to inspect yourself.

Errors

Every error from NewClient and Trace falls into exactly one row of the error reference. Match the classes with errors.Is, and use errors.AsType[*batchdata.Error] for the details of a call that passed validation.

res, err := tracer.Trace(ctx, req)
switch {
case errors.Is(err, batchdata.ErrInvalidConfig), errors.Is(err, batchdata.ErrInvalidRequest):
	// local: no call was made
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
	// stopped: the call may already have been processed and billed
case errors.Is(err, batchdata.ErrUnauthorized), errors.Is(err, batchdata.ErrForbidden):
	// token, permissions or balance
case errors.Is(err, batchdata.ErrRateLimited):
	if e, ok := errors.AsType[*batchdata.Error](err); ok && e.HasRetryAfter {
		_ = e.RetryAfter // a hint, not permission to repeat the call
	}
case errors.Is(err, batchdata.ErrRejected), errors.Is(err, batchdata.ErrMalformedResponse):
	// refused or unusable: it may already have been billed
case err != nil:
	// transport failure: the outcome is unknown, so do not resend blindly
}

*batchdata.Error carries Kind, StatusCode (HTTP), ProviderCode (the body's status.code when it could be decoded), and RetryAfter/HasRetryAfter from a standard Retry-After header, clamped to 24 hours. A 403 means insufficient permissions or insufficient balance; the SDK does not guess which.

Error strings use a fixed vocabulary, such as batchdata: rejected (HTTP 429). BatchData's own text is available only on request, through ProviderMessage() and Warnings(), because it may echo your input.

Timeouts, billing and repeat calls
  • BatchData bills each matched property record with at least one person: Meta.Results.MatchCount is the billed count.
  • The SDK never retries, hedges, falls back, splits a request or polls, and never follows a redirect. It sends the request without a rewindable body, so Go's HTTP/1.1 and HTTP/2 clients cannot resend it either.
  • A canceled or timed-out call may still be processed and billed. Cancellation stops the SDK from waiting, not BatchData from working.
  • Retry-After is not proof that a repeat is safe or free, and a malformed response may already have been billed. BatchData documents no idempotency mechanism, so the SDK sends none.
  • BatchData allows 3,000 requests per minute per account, shared by all of its tokens (rate and request limits), and documents no rate-limit headers.
  • DefaultTimeout (60 seconds) covers dialing, sending and reading the whole body. For large batches, raise it or send fewer items per call.
In a net/http handler
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
	defer cancel()
	res, err := h.Tracer.Trace(ctx, buildRequest(r))
	if err != nil {
		// map err as above; never echo ProviderMessage to the browser
		http.Error(w, "skip trace failed", http.StatusBadGateway)
		return
	}
	render(w, res.Records)
}
Testing your application

Depend on a small interface of your own and fake it:

type SkipTracer interface {
	Trace(ctx context.Context, req skiptrace.Request) (*skiptrace.Result, error)
}

var _ SkipTracer = (*skiptrace.Service)(nil)

type fakeTracer struct {
	res *skiptrace.Result
	err error
}

func (f fakeTracer) Trace(context.Context, skiptrace.Request) (*skiptrace.Result, error) {
	return f.res, f.err
}

To exercise the real SDK offline, serve a synthetic response from httptest.NewTLSServer and pass batchdata.WithHTTPClient(srv.Client()) and batchdata.WithTestEndpoint(srv.URL), as the examples do.

Storing results

skiptrace.Result round-trips through encoding/json unchanged (reflect.DeepEqual), keeping nil and empty slices, and absent and false flags, apart. encoding/json decodes a JSON null and an absent field alike, which matches BatchData's meaning for every nullable field. The SDK keeps no raw body.


⚙️ Behavior

The transport

Without WithHTTPClient, each Client builds its own *http.Transport with the settings of http.DefaultTransport (HTTP/2, and the standard dial, TLS and idle timeouts), plus a 64 KiB limit on response headers. Later changes to http.DefaultTransport do not reach it. Like the default transport, it takes a proxy from the HTTPS_PROXY and NO_PROXY environment variables; every call is https, so HTTP_PROXY is never used. The SDK's own code reads no environment variable.

With WithHTTPClient(hc), the SDK uses a copy of hc taken at NewClient, with redirects disabled and no cookie jar. hc itself is never modified, later changes to it have no effect, its Timeout still applies, and its Transport is shared. If hc.Transport is nil, Go uses http.DefaultTransport, and the 64 KiB header limit does not apply. An injected transport is trusted code: one that retries, follows redirects or replays requests on its own defeats the one-attempt guarantee.

Every request is a POST to https://api.batchdata.com/api/v3/property/skip-trace with Authorization: Bearer <token>, Content-Type: application/json, Accept: application/json and the User-Agent, and never an idempotency key. The SDK never sets Accept-Encoding: Go's transport may request gzip on its own and then decompresses it, and the size limit applies to the decompressed bytes. A response body is always closed and never read more than one byte past the limit.

Limits
Bound Value
Items per request 1 to 100
Encoded request 1 MiB
Response body 8 MiB by default, up to 64 MiB
Rejection body read 64 KiB, best-effort
Response headers (SDK-built transport) 64 KiB
JSON objects and arrays in a 200 body 100,000
JSON values in a 200 body 1,000,000
Call duration 60 seconds by default, including the whole body read

Decoding turns small JSON values into much larger Go values, so the byte limit alone would not bound memory. A full answer to 100 items holds a few thousand objects and arrays, and a response over either JSON bound fails as ErrMalformedResponse before it is decoded.

Error reference
Every error, and what it matches
What happened Kind errors.Is matches
Invalid token or option, or a nil or zero client (*ValidationError) ErrInvalidConfig
Nil context, or a request failing validation (*ValidationError) ErrInvalidRequest
Context already done before the call (no attempt) canceled / deadline_exceeded context.Canceled / context.DeadlineExceeded
No response: caller canceled canceled context.Canceled
No response: caller deadline deadline_exceeded context.DeadlineExceeded
No response: WithTimeout or the http.Client timeout timeout context.DeadlineExceeded
No response: any other failure transport none
HTTP 1xx, 3xx, 4xx or 5xx (redirects included) rejected ErrRejected, plus ErrUnauthorized (401), ErrForbidden (403) or ErrRateLimited (429) by HTTP status
HTTP 2xx other than 200 malformed_response ErrMalformedResponse
HTTP 200, body over the size limit response_too_large ErrMalformedResponse
HTTP 200, body read interrupted canceled / deadline_exceeded / timeout / transport as the rows above
HTTP 200 with a Content-Encoding the transport did not decode malformed_response ErrMalformedResponse
HTTP 200, body over the JSON bounds or not the documented envelope malformed_response ErrMalformedResponse
HTTP 200 whose status.code is present and not 200 rejected ErrRejected, plus the 401/403/429 sentinel by ProviderCode

The body of a rejection is read best-effort, up to 64 KiB, and dropped if it cannot be decoded: the status alone classifies it. Unwrap returns only context.Canceled or context.DeadlineExceeded; the underlying transport error is never kept.

Privacy
  • Error strings, GoString, every fmt verb on a Client or an error, and every error in an Unwrap chain never contain the token, URLs, request or response bodies, names, addresses, phones, emails, BatchData's messages, or network addresses. The token and URL live only inside a function the Client holds, which fmt and reflection-based dumpers cannot see into.
  • The SDK has no logger, telemetry, cache or persistence.
  • Results are personal data. Access control, logging, retention, and lawful and permissible use are your responsibility. A hashed field is not anonymous.
Contract uncertainties
What BatchData's V3 documentation leaves open, and what the SDK does
Question SDK behavior
Is each item's requestId echoed, and where? Read from input.requestId when present; otherwise no link by ID.
Does data[i] answer requests[i], and are the counts equal? Never assumed.
How does an item that "couldn't be processed" (errorCount) appear? meta.error: true becomes OutcomeFailed; a record that is missing is simply missing.
What does the 422 for more than 100 items look like? Requests over 100 items fail locally; any rejection body is read best-effort.
Are there rate-limit headers? A standard Retry-After is reported if present.
What is the TCPA default? Never injected; set the option explicitly.

The SDK is strict about the response envelope: a type change in status, result.options or result.meta fails the call with ErrMalformedResponse, even though BatchData may have billed it. encoding/json matches keys without regard to case; for a duplicate key, the last scalar or array wins, while duplicate objects merge field by field.

What is not covered

The asynchronous skip-trace endpoint, reverse skip tracing (by phone or email) and BatchData's other V3 APIs. New services can be added as sibling packages without changing the root package; see adding a service.


📚 Documentation

View the generated documentation and BatchData's API documentation.

Package Purpose
batchdata The Client, its options, and the errors every service shares
skiptrace Synchronous forward property skip tracing

Heads up! go-batchdata is intentionally light on dependencies. The only external package it uses is the excellent testify suite—and that's just for our tests. You can drop this library into your projects without dragging along extra baggage.


Development Setup (Getting Started)

Install MAGE-X build tool for development:

# Install MAGE-X for development and building
go install github.com/mrz1836/mage-x/cmd/magex@latest
magex update:install
Library Deployment

This project uses goreleaser for streamlined library releases to GitHub. To get started, install it via:

brew install goreleaser

The release process is defined in the .goreleaser.yml configuration file.

Then create and push a new Git tag using:

magex version:bump bump=patch push=true branch=master
Build Commands

View all build commands

magex help
GitHub Workflows

All workflows are driven by modular configuration in .github/env/ — no YAML editing required.

View all workflows and the control center →

Updating Dependencies

To update all dependencies (Go modules, linters, and related tools), run:

magex deps:update

🧪 Examples & Tests

All unit tests, fuzz tests and examples run via GitHub Actions on Go 1.26.x (the supported baseline) and the current Go release. View the configuration file.

Run all tests (fast):

magex test

Run all tests with race detector (slower):

magex test:race

Runnable examples live in the Example* functions of both packages and in examples/, which serves a synthetic response from a local TLS server:

go run ./examples # offline: no token, no network, no cost

Note: tests never touch the network. They use in-memory transports and local TLS servers over HTTP/1.1 and HTTP/2, with synthetic data only.


⚡ Benchmarks

Run the Go benchmarks:

magex bench

📊 Performance Results
Benchmark Iterations ns/op B/op allocs/op
Validate100 49,106 24,547 0 0
Encode100 13,681 87,719 41,172 3
Trace100 1,125 1,069,554 468,237 2,173
Do 763,564 1,570 2,720 27

Validate100 and Encode100 check and encode a 100-item request; Trace100 makes a full call against an in-memory transport and decodes a 100-record response; Do is one transport round trip in memory. They measure local CPU work only, executed on an Apple M1 Max (ARM64), and make no claim about network or BatchData latency, which dominates real calls.


🛠️ Code Standards

Read more about this Go project's code standards.


🤖 AI Usage & Assistant Guidelines

Read the AI Usage & Assistant Guidelines for details on how AI is used in this project and how to interact with the AI assistants.


👥 Maintainers

MrZ
MrZ

🤝 Contributing

View the contributing guidelines and please follow the code of conduct.

How can I help?

All kinds of contributions are welcome 🙌! The most basic way to show your support is to star 🌟 the project, or to raise issues 💬. You can also support this project by becoming a sponsor on GitHub 👏 or by making a bitcoin donation to ensure this journey continues indefinitely! 🚀

Stars


📝 License

License

Documentation

Overview

Package batchdata is the foundation of a small Go SDK for the BatchData V3 API.

It provides the Client, an authenticated and immutable connection, and the error surface every service shares. Typed access to each API lives in its own package, such as github.com/mrz1836/go-batchdata/skiptrace, so importing one service pulls in only that service, this package and the standard library.

Construct a client once and hand it to a service:

client, err := batchdata.NewClient(apiKey)
if err != nil {
	return err
}
tracer := skiptrace.New(client)
res, err := tracer.Trace(ctx, req)

The SDK makes at most one HTTP attempt per call. It never retries, follows redirects, replays a request or reads the environment for credentials. Every call may be billed, and a failed call may already have been processed.

Index

Examples

Constants

View Source
const (
	// DefaultTimeout bounds one call unless WithTimeout says otherwise: dial,
	// send and the full body read.
	DefaultTimeout = 60 * time.Second

	// DefaultMaxResponseBytes is the default response body limit.
	DefaultMaxResponseBytes = 8 << 20

	// MaxResponseBytesLimit is the largest limit WithMaxResponseBytes accepts.
	MaxResponseBytesLimit = 64 << 20

	// MaxRequestBytes bounds an encoded request body. A larger request fails
	// locally with ErrInvalidRequest.
	MaxRequestBytes = 1 << 20
)

Client defaults and limits.

Variables

View Source
var (
	// ErrInvalidConfig reports an invalid NewClient argument or option, or a
	// service built from a nil or zero Client.
	ErrInvalidConfig = errors.New("batchdata: invalid configuration")

	// ErrInvalidRequest reports a nil context or a request that fails local
	// validation, including one whose encoded body exceeds MaxRequestBytes.
	ErrInvalidRequest = errors.New("batchdata: invalid request")

	// ErrRejected reports a call the provider refused: an HTTP status other
	// than 2xx (a redirect included, which is never followed), or an HTTP 200
	// whose body reports a status code other than 200.
	ErrRejected = errors.New("batchdata: rejected by provider")

	// ErrUnauthorized reports a rejection with status 401: a missing, invalid
	// or revoked token. It also matches ErrRejected.
	ErrUnauthorized = errors.New("batchdata: unauthorized")

	// ErrForbidden reports a rejection with status 403. BatchData uses 403 for
	// both insufficient permissions and insufficient balance; the SDK does not
	// guess which. It also matches ErrRejected.
	ErrForbidden = errors.New("batchdata: forbidden")

	// ErrRateLimited reports a rejection with status 429. It also matches
	// ErrRejected.
	ErrRateLimited = errors.New("batchdata: rate limited")

	// ErrMalformedResponse reports a response the SDK cannot use: a 2xx status
	// other than 200, a body over the size limit, or an HTTP 200 body that is
	// not the documented envelope, carries a Content-Encoding nobody decoded,
	// or holds far more JSON objects, arrays or values than any real answer.
	// The call may already have been processed and billed.
	ErrMalformedResponse = errors.New("batchdata: malformed response")
)

Sentinel errors. Match them with errors.Is.

ErrInvalidConfig and ErrInvalidRequest are local: an error matching either means no call was made. Every other sentinel is matched only by an *Error.

Functions

This section is empty.

Types

type Client

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

Client is an authenticated connection to the BatchData API, shared by the service packages such as skiptrace. It is immutable after NewClient and safe for concurrent use.

Both methods have value receivers, so printing a Client or a *Client with fmt goes through String or GoString. The API key, the URL and the HTTP client exist only inside a function the Client holds: fmt prints a func as an address with every verb, even through unexported fields, and reflection-based dumpers cannot see what a function captures.

The zero Client is unusable: a service built from it, or from a nil *Client, reports ErrInvalidConfig on every call and makes none.

func NewClient

func NewClient(apiKey string, opts ...Option) (*Client, error)

NewClient returns a Client that authenticates with apiKey, sent as a bearer token. apiKey must be 1 to 4096 bytes of visible ASCII (no spaces or control characters); the SDK never reads it from the environment.

NewClient performs no I/O. An invalid argument or option returns a *ValidationError matching ErrInvalidConfig; its message never repeats the rejected value.

Create one Client per token and share it. Without WithHTTPClient, each Client owns a connection pool that cannot be closed; idle connections close on their own after 90 seconds. An application that holds many tokens should pass the same *http.Client to every Client with WithHTTPClient, so that they share one pool.

Example

The application decides where the key comes from; the SDK reads no environment. Printing a client never shows the key.

package main

import (
	"fmt"
	"time"

	"github.com/mrz1836/go-batchdata"
)

func main() {
	apiKey := "EXAMPLE-KEY-0000" // for example, from a secret manager
	client, err := batchdata.NewClient(apiKey,
		batchdata.WithTimeout(45*time.Second),
		batchdata.WithUserAgent("example-app/1.0"),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(client)
}
Output:
batchdata.Client{timeout: 45s, maxResponseBytes: 8388608}
Example (InvalidOption)

An invalid option fails before any call, and the error names the option without repeating its value.

package main

import (
	"errors"
	"fmt"

	"github.com/mrz1836/go-batchdata"
)

func main() {
	_, err := batchdata.NewClient("EXAMPLE-KEY-0000", batchdata.WithTimeout(0))
	fmt.Println(errors.Is(err, batchdata.ErrInvalidConfig))
	fmt.Println(err)
}
Output:
true
batchdata: invalid configuration: WithTimeout: must be positive

func (Client) GoString

func (c Client) GoString() string

GoString returns the same redacted form as String, so %#v cannot reveal the API key or the URL either.

func (Client) String

func (c Client) String() string

String returns a description of the Client's settings, such as `batchdata.Client{timeout: 1m0s, maxResponseBytes: 8388608}`. It never includes the API key, the URL or the User-Agent.

type Error

type Error struct {
	// Kind classifies the failure.
	Kind Kind

	// StatusCode is the HTTP status, or 0 when no response arrived.
	StatusCode int

	// ProviderCode is the status.code the response body reported, when the
	// body could be decoded; otherwise 0.
	ProviderCode int

	// RetryAfter is the Retry-After hint, clamped to [0, 24h].
	RetryAfter time.Duration

	// HasRetryAfter reports whether the response carried a usable
	// Retry-After header.
	HasRetryAfter bool
	// contains filtered or unexported fields
}

Error describes a call that passed local validation and then failed: the attempt failed, or it was never made because the context was already done. It is always returned as a *Error.

Its Error string is built from a fixed vocabulary: the kind and the status codes. It never contains the API key, URLs, request or response bodies, provider messages or network addresses, and neither do GoString, fmt verbs that print fields, or the chain Unwrap returns. Provider text is available only through ProviderMessage and Warnings.

RetryAfter is the provider's standard Retry-After hint, when it sent one. It is a hint, not proof that repeating the call is safe or free. HasRetryAfter distinguishes an explicit zero from an absent hint.

func (*Error) Error

func (e *Error) Error() string

Error returns "batchdata: <kind>", with the HTTP status and the provider status when known, such as "batchdata: rejected (HTTP 429)".

func (*Error) GoString

func (e *Error) GoString() string

GoString prints the exported fields only, so %#v cannot reveal the provider's text.

func (*Error) Is

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

Is reports whether target is a sentinel this error matches: ErrRejected for KindRejected, plus ErrUnauthorized, ErrForbidden or ErrRateLimited for status 401, 403 or 429, and ErrMalformedResponse for KindMalformedResponse and KindResponseTooLarge.

A rejection with an HTTP status other than 200 is matched by that HTTP status; an HTTP 200 whose body reports another status is matched by ProviderCode.

func (*Error) ProviderMessage

func (e *Error) ProviderMessage() string

ProviderMessage returns the status.message text of the provider's response body, or "" when there was none or it could not be decoded. It may echo request content, so treat it as sensitive: it is excluded from Error and GoString, and logging it is the caller's decision.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns context.Canceled for KindCanceled and context.DeadlineExceeded for KindDeadlineExceeded and KindTimeout, and nil otherwise. The underlying transport error is never kept.

func (*Error) Warnings

func (e *Error) Warnings() []Warning

Warnings returns a copy of the warnings in the provider's response body: status.warnings, then top-level warnings, in order. Like ProviderMessage, they may echo request content.

type Kind

type Kind string

Kind classifies a failed call. The list is closed.

const (
	// KindCanceled means the caller's context was canceled. It matches
	// context.Canceled.
	KindCanceled Kind = "canceled"

	// KindDeadlineExceeded means the caller's context deadline expired. It
	// matches context.DeadlineExceeded.
	KindDeadlineExceeded Kind = "deadline_exceeded"

	// KindTimeout means the call's own timeout expired: WithTimeout, or the
	// Timeout of an injected http.Client. It matches context.DeadlineExceeded.
	KindTimeout Kind = "timeout"

	// KindTransport means the call failed in transit (DNS, TLS, a reset
	// connection or an incomplete body). The provider may or may not have
	// processed it.
	KindTransport Kind = "transport"

	// KindRejected means the provider refused the call. It matches
	// ErrRejected, and ErrUnauthorized, ErrForbidden or ErrRateLimited by
	// status.
	KindRejected Kind = "rejected"

	// KindMalformedResponse means the response could not be used. It matches
	// ErrMalformedResponse.
	KindMalformedResponse Kind = "malformed_response"

	// KindResponseTooLarge means the response body exceeded the size limit.
	// It matches ErrMalformedResponse.
	KindResponseTooLarge Kind = "response_too_large"
)

Failure kinds.

type Option

type Option func(*config)

Option configures a Client in NewClient. Nil options are skipped, options apply in order, and the last option of each kind wins. Options only record their argument; NewClient validates the result and reports an invalid value as ErrInvalidConfig.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient sets the HTTP client that carries every call. NewClient copies it with redirects disabled and no cookie jar, and never modifies hc: its Transport is shared and never mutated, and its Timeout still applies. Changes to hc after NewClient have no effect. hc must not be nil.

The transport is trusted caller code. A transport that retries, follows redirects or replays requests on its own defeats the SDK's one-attempt guarantee. If hc.Transport is nil, Go uses http.DefaultTransport: changes to it then apply, and the SDK's 64 KiB response header limit does not.

Without this option the SDK builds its own transport with the settings of http.DefaultTransport, so later changes to http.DefaultTransport have no effect on it. Like http.DefaultTransport, it takes a proxy from the HTTPS_PROXY and NO_PROXY environment variables; every call is https, so HTTP_PROXY is never used.

func WithMaxResponseBytes

func WithMaxResponseBytes(n int64) Option

WithMaxResponseBytes limits the response body size. A larger body fails the call with KindResponseTooLarge, which matches ErrMalformedResponse, after the provider may already have billed it. The default is DefaultMaxResponseBytes; n must be from 1 to MaxResponseBytesLimit.

func WithTestEndpoint

func WithTestEndpoint(baseURL string) Option

WithTestEndpoint replaces https://api.batchdata.com with a loopback base URL, such as the URL of an httptest.NewTLSServer. It exists for tests: only https on 127.0.0.0/8 or [::1], with an optional port and no path, query, fragment or userinfo, is accepted. Host names, including "localhost", are rejected because they resolve through the system resolver. Requests go to <base>/api/v3/<endpoint path>.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout bounds each call, including sending the request and reading the whole response body. The caller's context deadline still applies when it is earlier. The default is DefaultTimeout; d must be positive.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent replaces the User-Agent header, "go-batchdata" by default. ua must be 1 to 256 bytes of visible ASCII or spaces, starting and ending with a visible character.

type ValidationError

type ValidationError struct {
	Err    error
	Field  string
	Reason string
}

ValidationError describes a local validation failure. Err is the matching sentinel, ErrInvalidConfig or ErrInvalidRequest. Field names what failed: the "apiKey" argument, an option such as "WithTimeout", or a Go field path such as "Items[3].PropertyAddress.Street"; it holds indexes, never values. Reason never contains caller data.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error returns the sentinel message followed by the field and the reason.

func (*ValidationError) Unwrap

func (e *ValidationError) Unwrap() error

Unwrap returns the sentinel.

type Warning

type Warning struct {
	Code    WarningCode `json:"code"`
	Message string      `json:"message,omitempty"`
}

Warning is a provider warning: a machine-readable code and a human-readable message. Unknown codes are kept as sent.

type WarningCode

type WarningCode string

WarningCode identifies the type of a Warning. Values other than the documented constants are kept as sent.

const (
	// WarningDatasetNotAllowed reports a requested dataset that does not exist
	// or is not available for the token.
	WarningDatasetNotAllowed WarningCode = "dataset_not_allowed"

	// WarningDeprecatedField reports the use of a deprecated field.
	WarningDeprecatedField WarningCode = "warning_deprecated_field"

	// WarningRateLimitApproaching reports that the account is close to its
	// rate limit.
	WarningRateLimitApproaching WarningCode = "warning_rate_limit_approaching"
)

Documented warning codes.

Directories

Path Synopsis
Command examples shows the go-batchdata SDK end to end, offline: it serves a synthetic skip-trace response from a local TLS server, so it needs no API key, makes no network call and costs nothing.
Command examples shows the go-batchdata SDK end to end, offline: it serves a synthetic skip-trace response from a local TLS server, so it needs no API key, makes no network call and costs nothing.
internal
call
Package call is the sealed bridge between the root batchdata package and the service packages, such as skiptrace.
Package call is the sealed bridge between the root batchdata package and the service packages, such as skiptrace.
httpx
Package httpx is the SDK's single HTTP transport: one bounded POST attempt per call, no redirects, no retries, no replays, and failures that carry no URLs, headers of the request, credentials, causes or bodies.
Package httpx is the SDK's single HTTP transport: one bounded POST attempt per call, no redirects, no retries, no replays, and failures that carry no URLs, headers of the request, credentials, causes or bodies.
jsonx
Package jsonx holds the byte-level JSON checks that the root batchdata package and the service packages share: the shape of a raw value, and a count of the containers and values in a document.
Package jsonx holds the byte-level JSON checks that the root batchdata package and the service packages share: the shape of a raw value, and a count of the containers and values in a document.
testutil
Package testutil holds the test plumbing go-batchdata's packages share: in-memory and recording HTTP fakes over HTTP/1.1 and HTTP/2, fmt leak assertions, and checks of error types and JSON round trips.
Package testutil holds the test plumbing go-batchdata's packages share: in-memory and recording HTTP fakes over HTTP/1.1 and HTTP/2, fmt leak assertions, and checks of error types and JSON round trips.
Package skiptrace provides synchronous forward property skip tracing on the BatchData V3 API: POST /api/v3/property/skip-trace.
Package skiptrace provides synchronous forward property skip tracing on the BatchData V3 API: POST /api/v3/property/skip-trace.

Jump to

Keyboard shortcuts

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