A lightweight, production-grade Go client library for the USPS Addresses 3.0 REST API and OAuth 2.0 API.
Enterprise-grade address validation and standardization for Go applications.
- 🎯 Complete Coverage - All USPS Addresses 3.0 and OAuth 2.0 endpoints
- 🔒 Automatic OAuth - Built-in token management with automatic refresh
- 💪 Strongly Typed - Full type safety based on OpenAPI specification
- 📦 Zero Dependencies - Only uses Go standard library
- 🏗️ Production Ready - Built with enterprise-grade patterns and best practices
- 🧪 Fully Tested - 97%+ test coverage with comprehensive test suite
- Why go-usps?
- Quick Start
- Core Concepts
- Usage Examples
- Address Parsing
- Advanced Usage
- Advanced Topics
- API Reference
- Additional Resources
- Development
- Requirements
- API Documentation
- License
- Contributing
Get started in 60 seconds:
go get github.com/my-eq/go-uspspackage main
import (
"context"
"fmt"
"log"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
func main() {
// Create client with automatic OAuth (recommended)
client := usps.NewClientWithOAuth("your-client-id", "your-client-secret")
// Standardize an address
req := &models.AddressRequest{
StreetAddress: "123 Main St",
City: "New York",
State: "NY",
}
resp, err := client.GetAddress(context.Background(), req)
if err != nil {
log.Fatalf("Error: %v", err)
}
fmt.Printf("Standardized: %s, %s, %s %s\n",
resp.Address.StreetAddress,
resp.Address.City,
resp.Address.State,
resp.Address.ZIPCode)
}Get your credentials: Register at USPS Developer Portal
The USPS Addresses API provides real-time validation and standardization of US domestic addresses. It ensures addresses are deliverable, corrects common errors, and enriches data with ZIP+4 codes and delivery point information.
Key Benefits:
- Reduce returns - Validate shipping addresses before fulfillment
- Improve deliverability - Standardize formats to USPS specifications
- Save costs - Catch errors before packages are shipped
- Enhance data quality - Fill in missing ZIP codes and abbreviations
Validates and standardizes complete addresses. Returns the official USPS format with ZIP+4 codes, delivery point validation, and carrier route information.
req := &models.AddressRequest{
StreetAddress: "123 Main St",
SecondaryAddress: "Apt 4B", // Optional
City: "New York",
State: "NY",
}
resp, err := client.GetAddress(ctx, req)
// Returns: standardized address + ZIP+4 + delivery infoUse when: You have a complete address and need to validate or standardize it.
Returns the official city and state names for a given ZIP code.
req := &models.CityStateRequest{
ZIPCode: "10001",
}
resp, err := client.GetCityState(ctx, req)
// Returns: "NEW YORK, NY"Use when: You have a ZIP code and need the corresponding city and state.
Returns the ZIP code and ZIP+4 for a given address.
req := &models.ZIPCodeRequest{
StreetAddress: "123 Main St",
City: "New York",
State: "NY",
}
resp, err := client.GetZIPCode(ctx, req)
// Returns: ZIP code + ZIP+4Use when: You have an address without a ZIP code or need to find the ZIP+4.
All USPS API requests require OAuth 2.0 authentication. This library handles it automatically.
Recommended approach (automatic token management):
client := usps.NewClientWithOAuth("client-id", "client-secret")
// Tokens are automatically acquired and refreshedAlternative (manual token provider):
tokenProvider := usps.NewStaticTokenProvider("your-access-token")
client := usps.NewClient(tokenProvider)Tokens expire after 8 hours but are automatically refreshed 5 minutes before expiration
when using NewClientWithOAuth or NewOAuthTokenProvider.
The library provides structured errors with detailed information:
resp, err := client.GetAddress(ctx, req)
if err != nil {
if apiErr, ok := err.(*usps.APIError); ok {
// API-specific error
fmt.Printf("API Error: %s\n", apiErr.ErrorMessage.Error.Message)
for _, detail := range apiErr.ErrorMessage.Error.Errors {
fmt.Printf(" - %s: %s\n", detail.Title, detail.Detail)
}
} else {
// Network or other error
fmt.Printf("Error: %v\n", err)
}
return
}The library supports both production and testing environments:
// Production (default)
client := usps.NewClientWithOAuth(clientID, clientSecret)
// Testing
client := usps.NewTestClientWithOAuth(clientID, clientSecret)Prevent shipping errors by validating customer addresses during checkout:
import (
"context"
"fmt"
"os"
"time"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
func ValidateShippingAddress(street, city, state, zip string) (*models.AddressResponse, error) {
client := usps.NewClientWithOAuth(os.Getenv("USPS_CLIENT_ID"),
os.Getenv("USPS_CLIENT_SECRET"))
req := &models.AddressRequest{
StreetAddress: street,
City: city,
State: state,
ZIPCode: zip,
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetAddress(ctx, req)
if err != nil {
if apiErr, ok := err.(*usps.APIError); ok {
return nil, fmt.Errorf("invalid address: %s", apiErr.ErrorMessage.Error.Message)
}
return nil, err
}
return resp, nil
}Process large batches of addresses efficiently with built-in rate limiting and retry logic:
import (
"context"
"fmt"
"log"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
func ProcessAddresses(addresses []*models.AddressRequest) {
client := usps.NewClientWithOAuth(clientID, clientSecret)
// Configure bulk processor
config := &usps.BulkConfig{
MaxConcurrency: 10, // Process 10 addresses concurrently
RequestsPerSecond: 10, // Rate limit to 10 requests/second
MaxRetries: 3, // Retry failed requests up to 3 times
ProgressCallback: func(completed, total int, err error) {
fmt.Printf("Progress: %d/%d\n", completed, total)
if err != nil {
log.Printf("Error processing item: %v", err)
}
},
}
processor := usps.NewBulkProcessor(client, config)
// Process all addresses with automatic rate limiting and retries
results := processor.ProcessAddresses(context.Background(), addresses)
// Handle results
for _, result := range results {
if result.Error != nil {
log.Printf("Address %d failed: %v", result.Index, result.Error)
continue
}
fmt.Printf("Standardized: %s, %s, %s %s\n",
result.Response.Address.StreetAddress,
result.Response.Address.City,
result.Response.Address.State,
result.Response.Address.ZIPCode)
}
}Key Features:
- Automatic rate limiting - Respects USPS API limits to prevent 429 errors
- Concurrent processing - Configurable worker pool for optimal throughput
- Smart retries - Exponential backoff for transient failures (500, 503, 429)
- Progress tracking - Optional callback for real-time progress monitoring
- Context support - Full cancellation and timeout support
The bulk processor also supports ProcessCityStates() and ProcessZIPCodes() for bulk
lookups of other endpoint types.
Help users by automatically filling in ZIP codes:
func AutoCompleteZIP(street, city, state string) (string, error) {
// Note: In production, create the client once and reuse it
client := usps.NewClientWithOAuth(clientID, clientSecret)
req := &models.ZIPCodeRequest{
StreetAddress: street,
City: city,
State: state,
}
resp, err := client.GetZIPCode(context.Background(), req)
if err != nil {
return "", err
}
// Return ZIP+4 format if available
if resp.ZIPCode.ZIPPlus4 != nil && *resp.ZIPCode.ZIPPlus4 != "" {
return fmt.Sprintf("%s-%s", resp.ZIPCode.ZIPCode, *resp.ZIPCode.ZIPPlus4), nil
}
return resp.ZIPCode.ZIPCode, nil
}Check if an address is a business location:
func IsBusinessAddress(address *models.AddressRequest) (bool, error) {
// Note: In production, create the client once and reuse it
client := usps.NewClientWithOAuth(clientID, clientSecret)
resp, err := client.GetAddress(context.Background(), address)
if err != nil {
return false, err
}
// Check additional info for business indicator
if resp.AdditionalInfo != nil {
return resp.AdditionalInfo.Business == "Y", nil
}
return false, nil
}Standardize addresses for mail merge or label printing:
import (
"context"
"fmt"
"strings"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
func FormatMailingLabel(address *models.AddressRequest) (string, error) {
// Note: In production, create the client once and reuse it
client := usps.NewClientWithOAuth(clientID, clientSecret)
resp, err := client.GetAddress(context.Background(), address)
if err != nil {
return "", err
}
// Build formatted address
var lines []string
if resp.Firm != "" {
lines = append(lines, resp.Firm)
}
lines = append(lines, resp.Address.StreetAddress)
if resp.Address.SecondaryAddress != "" {
lines = append(lines, resp.Address.SecondaryAddress)
}
cityLine := fmt.Sprintf("%s, %s %s",
resp.Address.City,
resp.Address.State,
resp.Address.ZIPCode)
if resp.Address.ZIPPlus4 != nil && *resp.Address.ZIPPlus4 != "" {
cityLine = fmt.Sprintf("%s, %s %s-%s",
resp.Address.City,
resp.Address.State,
resp.Address.ZIPCode,
*resp.Address.ZIPPlus4)
}
lines = append(lines, cityLine)
return strings.Join(lines, "\n"), nil
}The parser package provides intelligent parsing of free-form address strings into structured
AddressRequest objects. This is essential for handling user input that doesn't follow a strict format.
- Handle free-form input - Parse addresses from a single text field
- Smart tokenization - Automatically identifies address components
- USPS standardization - Applies official USPS abbreviations and formatting
- Validation feedback - Provides diagnostics for missing or incorrect components
- Zero dependencies - Pure Go implementation using only the standard library
import (
"context"
"fmt"
"log"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/parser"
)
func main() {
// Parse a free-form address string
input := "123 North Main Street Apartment 4B, New York, NY 10001-1234"
parsed, diagnostics := parser.Parse(input)
// Check for issues
for _, d := range diagnostics {
fmt.Printf("%s: %s\n", d.Severity, d.Message)
}
// Convert to AddressRequest
req := parsed.ToAddressRequest()
fmt.Printf("Street: %s\n", req.StreetAddress) // "123 N MAIN ST"
fmt.Printf("Secondary: %s\n", req.SecondaryAddress) // "APT 4B"
fmt.Printf("City: %s\n", req.City) // "NEW YORK"
fmt.Printf("State: %s\n", req.State) // "NY"
fmt.Printf("ZIP: %s\n", req.ZIPCode) // "10001"
fmt.Printf("ZIP+4: %s\n", req.ZIPPlus4) // "1234"
}Combine parsing with USPS validation for the complete workflow:
import (
"context"
"fmt"
"log"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/parser"
)
func ValidateFreeFormAddress(userInput string) error {
// Step 1: Parse the free-form input
parsed, diagnostics := parser.Parse(userInput)
// Step 2: Check for critical errors
for _, d := range diagnostics {
if d.Severity == parser.SeverityError {
return fmt.Errorf("parse error: %s - %s", d.Message, d.Remediation)
}
}
// Step 3: Convert to AddressRequest
req := parsed.ToAddressRequest()
// Step 4: Validate with USPS API
client := usps.NewClientWithOAuth("client-id", "client-secret")
resp, err := client.GetAddress(context.Background(), req)
if err != nil {
return fmt.Errorf("validation failed: %v", err)
}
fmt.Printf("Validated address: %s, %s, %s %s\n",
resp.Address.StreetAddress,
resp.Address.City,
resp.Address.State,
resp.Address.ZIPCode)
return nil
}Intelligent Component Recognition:
// Handles directionals
parser.Parse("123 North Main St, New York, NY 10001")
// → Street: "123 N MAIN ST"
// Handles secondary units
parser.Parse("456 Oak Ave Apt 4B, Boston, MA 02101")
// → Street: "456 OAK AVE", Secondary: "APT 4B"
// Handles ZIP+4
parser.Parse("789 Elm Blvd, Chicago, IL 60601-1234")
// → ZIP: "60601", ZIP+4: "1234"Automatic Standardization:
The parser applies USPS Publication 28 standards automatically:
| Input | Standardized |
|---|---|
| Street | ST |
| Avenue | AVE |
| Boulevard | BLVD |
| North | N |
| Apartment | APT |
| Suite | STE |
Diagnostics and Validation:
parsed, diagnostics := parser.Parse("123 Main St, New York")
for _, d := range diagnostics {
fmt.Printf("%s: %s\n", d.Severity, d.Message)
if d.Remediation != "" {
fmt.Printf(" Fix: %s\n", d.Remediation)
}
}
// Output:
// Error: Missing required state code
// Fix: Add a 2-letter state code (e.g., NY, CA, TX)
// Warning: Missing ZIP code
// Fix: Add a 5-digit ZIP code for better address validationSingle-field address input:
// User enters complete address in one field
userInput := "123 Main St Apt 4, New York, NY 10001"
parsed, _ := parser.Parse(userInput)
req := parsed.ToAddressRequest()
// Now ready for USPS validation
resp, err := client.GetAddress(ctx, req)Form auto-fill:
// Parse as user types, fill individual fields
parsed, _ := parser.Parse(userInput)
// Use ToAddressRequest() for proper formatting
req := parsed.ToAddressRequest()
streetField.SetText(req.StreetAddress)
secondaryField.SetText(req.SecondaryAddress)
cityField.SetText(req.City)
stateField.SetText(req.State)
zipField.SetText(req.ZIPCode)Import from CSV or external data:
// Parse addresses from external sources
for _, row := range csvData {
parsed, diagnostics := parser.Parse(row.AddressColumn)
// Check for errors
hasErrors := false
for _, d := range diagnostics {
if d.Severity == parser.SeverityError {
hasErrors = true
break
}
}
if hasErrors {
log.Printf("Skipping invalid address: %s", row.AddressColumn)
continue
}
req := parsed.ToAddressRequest()
// Validate with USPS...
}For complete parser documentation, see the parser package README.
Implement the TokenProvider interface for advanced authentication scenarios like
credential rotation, vault integration, or custom caching:
import (
"context"
"fmt"
vault "github.com/hashicorp/vault/api" // Example: HashiCorp Vault client
)
type TokenProvider interface {
GetToken(ctx context.Context) (string, error)
}
// Example: Vault-backed token provider
type VaultTokenProvider struct {
vaultClient *vault.Client
path string
}
func (p *VaultTokenProvider) GetToken(ctx context.Context) (string, error) {
secret, err := p.vaultClient.Logical().Read(p.path)
if err != nil {
return "", err
}
token, ok := secret.Data["usps_token"].(string)
if !ok {
return "", fmt.Errorf("token not found in vault")
}
return token, nil
}
// Use with client
client := usps.NewClient(&VaultTokenProvider{
vaultClient: vaultClient,
path: "secret/data/usps",
})Configure timeouts, retries, and transport settings:
import (
"net/http"
"time"
"github.com/my-eq/go-usps"
)
httpClient := &http.Client{
Timeout: 60 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
}
client := usps.NewClient(
tokenProvider,
usps.WithHTTPClient(httpClient),
)Handle transient failures with intelligent retries:
import (
"context"
"fmt"
"time"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
func GetAddressWithRetry(client *usps.Client, req *models.AddressRequest) (*models.AddressResponse, error) {
maxRetries := 3
baseDelay := 1 * time.Second
for attempt := 0; attempt <= maxRetries; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
resp, err := client.GetAddress(ctx, req)
cancel()
if err == nil {
return resp, nil
}
// Check if error is retryable
if apiErr, ok := err.(*usps.APIError); ok {
// Don't retry 4xx errors (except 429)
if apiErr.StatusCode >= 400 && apiErr.StatusCode < 500 && apiErr.StatusCode != 429 {
return nil, err
}
}
if attempt < maxRetries {
delay := baseDelay * time.Duration(1<<uint(attempt)) // Exponential backoff
time.Sleep(delay)
}
}
return nil, fmt.Errorf("max retries exceeded")
}Protect your application from cascading failures:
import (
"context"
"fmt"
"sync"
"time"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
type CircuitBreaker struct {
client *usps.Client
maxFailures int
resetTimeout time.Duration
mu sync.Mutex
failures int
lastFailTime time.Time
state string // "closed", "open", "half-open"
}
func (cb *CircuitBreaker) GetAddress(ctx context.Context, req *models.AddressRequest) (*models.AddressResponse, error) {
cb.mu.Lock()
// Check if circuit should be reset
if cb.state == "open" && time.Since(cb.lastFailTime) > cb.resetTimeout {
cb.state = "half-open"
cb.failures = 0
}
if cb.state == "open" {
cb.mu.Unlock()
return nil, fmt.Errorf("circuit breaker is open")
}
cb.mu.Unlock()
// Attempt the request
resp, err := cb.client.GetAddress(ctx, req)
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.failures++
cb.lastFailTime = time.Now()
if cb.failures >= cb.maxFailures {
cb.state = "open"
}
return nil, err
}
// Success - reset circuit
cb.failures = 0
cb.state = "closed"
return resp, nil
}Add logging, metrics, or tracing to all requests:
import (
"context"
"log"
"time"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
// Note: This example shows the pattern. MetricsCollector is a pseudo-code interface.
// In production, use a concrete metrics library like Prometheus (see Metrics Collection example).
type InstrumentedClient struct {
client *usps.Client
logger *log.Logger
metrics MetricsCollector // Your metrics implementation
}
// MetricsCollector interface (implement this with your metrics library)
type MetricsCollector interface {
RecordDuration(name string, duration time.Duration)
IncrementCounter(name string)
}
func (ic *InstrumentedClient) GetAddress(ctx context.Context, req *models.AddressRequest) (*models.AddressResponse, error) {
start := time.Now()
ic.logger.Printf("GetAddress request: %s, %s, %s", req.StreetAddress, req.City, req.State)
resp, err := ic.client.GetAddress(ctx, req)
duration := time.Since(start)
ic.metrics.RecordDuration("get_address", duration)
if err != nil {
ic.metrics.IncrementCounter("get_address_errors")
ic.logger.Printf("GetAddress error: %v (duration: %v)", err, duration)
return nil, err
}
ic.metrics.IncrementCounter("get_address_success")
ic.logger.Printf("GetAddress success (duration: %v)", duration)
return resp, nil
}For complex OAuth flows like authorization code with PKCE:
// Step 1: Obtain authorization code (user redirects to USPS)
authURL := "https://apis.usps.com/oauth2/v3/authorize?" +
"client_id=your-client-id&" +
"redirect_uri=https://yourapp.com/callback&" +
"response_type=code&" +
"scope=addresses"
// Step 2: Exchange code for tokens
oauthClient := usps.NewOAuthClient()
req := &models.AuthorizationCodeCredentials{
GrantType: "authorization_code",
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
Code: codeFromCallback,
RedirectURI: "https://yourapp.com/callback",
}
result, err := oauthClient.PostToken(context.Background(), req)
if err != nil {
return err
}
tokens := result.(*models.ProviderTokensResponse)
// Step 3: Use access token
tokenProvider := usps.NewStaticTokenProvider(tokens.AccessToken)
client := usps.NewClient(tokenProvider)
// Step 4: Refresh when needed
refreshReq := &models.RefreshTokenCredentials{
GrantType: "refresh_token",
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
RefreshToken: tokens.RefreshToken,
}
newTokens, err := oauthClient.PostToken(context.Background(), refreshReq)Create a custom token provider for testing:
type MockTokenProvider struct{}
func (m *MockTokenProvider) GetToken(ctx context.Context) (string, error) {
return "mock-token-for-testing", nil
}
// In tests
func TestAddressValidation(t *testing.T) {
// Use test environment
client := usps.NewTestClient(&MockTokenProvider{})
// Your test code here
}When running in a distributed environment, centralize OAuth token management:
// Token service that manages tokens for all microservices
type TokenService struct {
client *usps.OAuthClient
clientID string
secret string
mu sync.RWMutex
token string
expiry time.Time
}
func (ts *TokenService) GetToken(ctx context.Context) (string, error) {
ts.mu.RLock()
if time.Now().Before(ts.expiry.Add(-5 * time.Minute)) {
token := ts.token
ts.mu.RUnlock()
return token, nil
}
ts.mu.RUnlock()
// Need to refresh
ts.mu.Lock()
defer ts.mu.Unlock()
// Double-check after acquiring write lock
if time.Now().Before(ts.expiry.Add(-5 * time.Minute)) {
return ts.token, nil
}
// Fetch new token
req := &models.ClientCredentials{
GrantType: "client_credentials",
ClientID: ts.clientID,
ClientSecret: ts.secret,
}
result, err := ts.client.PostToken(ctx, req)
if err != nil {
return "", err
}
resp := result.(*models.ProviderAccessTokenResponse)
ts.token = resp.AccessToken
ts.expiry = time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second)
return ts.token, nil
}Distribute requests across multiple client instances:
import (
"context"
"sync/atomic"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
type LoadBalancedClient struct {
clients []*usps.Client
idx uint32
}
func (lbc *LoadBalancedClient) GetAddress(ctx context.Context, req *models.AddressRequest) (*models.AddressResponse, error) {
// Round-robin selection
idx := atomic.AddUint32(&lbc.idx, 1)
client := lbc.clients[idx%uint32(len(lbc.clients))]
return client.GetAddress(ctx, req)
}
// Initialize with multiple clients
func NewLoadBalancedClient(tokenProvider usps.TokenProvider, count int) *LoadBalancedClient {
clients := make([]*usps.Client, count)
for i := 0; i < count; i++ {
clients[i] = usps.NewClient(tokenProvider)
}
return &LoadBalancedClient{clients: clients}
}Cache validated addresses to reduce API calls:
type CachedClient struct {
client *usps.Client
cache *sync.Map
ttl time.Duration
}
type cacheEntry struct {
response *models.AddressResponse
timestamp time.Time
}
func (cc *CachedClient) GetAddress(ctx context.Context, req *models.AddressRequest) (*models.AddressResponse, error) {
// Create cache key
key := fmt.Sprintf("%s|%s|%s", req.StreetAddress, req.City, req.State)
// Check cache
if val, ok := cc.cache.Load(key); ok {
entry := val.(cacheEntry)
if time.Since(entry.timestamp) < cc.ttl {
return entry.response, nil
}
cc.cache.Delete(key) // Expired
}
// Fetch from API
resp, err := cc.client.GetAddress(ctx, req)
if err != nil {
return nil, err
}
// Store in cache
cc.cache.Store(key, cacheEntry{
response: resp,
timestamp: time.Now(),
})
return resp, nil
}For distributed caching across multiple instances:
import (
"context"
"encoding/json"
"time"
"github.com/go-redis/redis/v8" // Example: go-redis client
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
type RedisCachedClient struct {
client *usps.Client
redisClient *redis.Client
ttl time.Duration
}
func (rc *RedisCachedClient) GetAddress(ctx context.Context, req *models.AddressRequest) (*models.AddressResponse, error) {
key := fmt.Sprintf("usps:address:%s:%s:%s", req.StreetAddress, req.City, req.State)
// Try cache first
cached, err := rc.redisClient.Get(ctx, key).Result()
if err == nil {
var resp models.AddressResponse
if json.Unmarshal([]byte(cached), &resp) == nil {
return &resp, nil
}
}
// Fetch from API
resp, err := rc.client.GetAddress(ctx, req)
if err != nil {
return nil, err
}
// Store in Redis
if data, err := json.Marshal(resp); err == nil {
rc.redisClient.Set(ctx, key, data, rc.ttl)
}
return resp, nil
}For most use cases, use the built-in BulkProcessor which includes automatic rate limiting:
import (
"context"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
func ProcessWithRateLimit(addresses []*models.AddressRequest) {
client := usps.NewClientWithOAuth(clientID, clientSecret)
// Configure rate limiting
config := &usps.BulkConfig{
MaxConcurrency: 10, // Concurrent workers
RequestsPerSecond: 10, // Automatic rate limiting
MaxRetries: 3, // Retry on rate limit errors
}
processor := usps.NewBulkProcessor(client, config)
results := processor.ProcessAddresses(context.Background(), addresses)
// Handle results...
}The bulk processor uses a token bucket algorithm (stdlib only) to enforce rate limits and automatically handles 429 responses with exponential backoff.
For custom implementations, you can build your own rate limiter:
import (
"context"
"fmt"
"golang.org/x/time/rate"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
type RateLimitedClient struct {
client *usps.Client
limiter *rate.Limiter
}
func NewRateLimitedClient(client *usps.Client, requestsPerSecond int) *RateLimitedClient {
return &RateLimitedClient{
client: client,
limiter: rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond),
}
}
func (rlc *RateLimitedClient) GetAddress(ctx context.Context, req *models.AddressRequest) (*models.AddressResponse, error) {
// Wait for rate limiter
if err := rlc.limiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("rate limit: %w", err)
}
return rlc.client.GetAddress(ctx, req)
}Add comprehensive logging for production debugging:
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
type ObservableClient struct {
client *usps.Client
logger *slog.Logger
}
func (oc *ObservableClient) GetAddress(ctx context.Context, req *models.AddressRequest) (*models.AddressResponse, error) {
requestID := ctx.Value("request_id")
oc.logger.Info("address_validation_start",
slog.String("request_id", fmt.Sprint(requestID)),
slog.String("street", req.StreetAddress),
slog.String("city", req.City),
slog.String("state", req.State))
start := time.Now()
resp, err := oc.client.GetAddress(ctx, req)
duration := time.Since(start)
if err != nil {
oc.logger.Error("address_validation_failed",
slog.String("request_id", fmt.Sprint(requestID)),
slog.Duration("duration", duration),
slog.String("error", err.Error()))
return nil, err
}
oc.logger.Info("address_validation_success",
slog.String("request_id", fmt.Sprint(requestID)),
slog.Duration("duration", duration),
slog.String("zip", resp.Address.ZIPCode))
return resp, nil
}Track performance and errors with Prometheus:
import (
"context"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
var (
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "usps_request_duration_seconds",
Help: "Duration of USPS API requests",
},
[]string{"endpoint", "status"},
)
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "usps_requests_total",
Help: "Total number of USPS API requests",
},
[]string{"endpoint", "status"},
)
)
func init() {
prometheus.MustRegister(requestDuration)
prometheus.MustRegister(requestsTotal)
}
type MetricsClient struct {
client *usps.Client
}
func (mc *MetricsClient) GetAddress(ctx context.Context, req *models.AddressRequest) (*models.AddressResponse, error) {
start := time.Now()
resp, err := mc.client.GetAddress(ctx, req)
duration := time.Since(start).Seconds()
status := "success"
if err != nil {
status = "error"
}
requestDuration.WithLabelValues("get_address", status).Observe(duration)
requestsTotal.WithLabelValues("get_address", status).Inc()
return resp, err
}Implement health checks for Kubernetes or load balancers:
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/my-eq/go-usps"
"github.com/my-eq/go-usps/models"
)
func USPSHealthCheck(client *usps.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
// Use a known good address for health check
req := &models.AddressRequest{
StreetAddress: "475 L'Enfant Plaza SW",
City: "Washington",
State: "DC",
}
_, err := client.GetAddress(ctx, req)
if err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]string{
"status": "unhealthy",
"error": err.Error(),
})
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
})
}
}When deploying to production, ensure you have:
- ✓ Error handling - Graceful degradation for API failures
- ✓ Timeouts - Context timeouts on all requests (5-10 seconds recommended)
- ✓ Retries - Exponential backoff for transient failures
- ✓ Rate limiting - Respect USPS API limits to avoid 429 errors
- ✓ Caching - Cache validated addresses (TTL: 24 hours recommended)
- ✓ Monitoring - Track success rates, latencies, and error rates
- ✓ Circuit breaker - Prevent cascading failures
- ✓ Logging - Structured logs with request IDs
- ✓ Health checks - Endpoint for load balancer health checks
- ✓ Token rotation - Automatic OAuth token refresh
- ✓ Secrets management - Never hardcode credentials
type AddressResponse struct {
Firm string // Business name
Address *DomesticAddress // Standardized address
AdditionalInfo *AddressAdditionalInfo // Delivery point, carrier route, etc.
Corrections []AddressCorrection // Suggested improvements
Matches []AddressMatch // Match indicators
Warnings []string // Warnings
}type DomesticAddress struct {
StreetAddress string // Standardized street address
SecondaryAddress string // Apartment, suite, etc.
City string // City name
State string // 2-letter state code
ZIPCode string // 5-digit ZIP code
ZIPPlus4 *string // 4-digit ZIP+4 extension (pointer, may be nil)
Urbanization string // Urbanization code (Puerto Rico)
}Rich delivery metadata returned with validated addresses:
type AddressAdditionalInfo struct {
DeliveryPoint string // Unique delivery address identifier (2 digits)
CarrierRoute string // Carrier route code (4 characters)
DPVConfirmation string // Delivery Point Validation: Y, D, S, or N
DPVCMRA string // Commercial Mail Receiving Agency: Y or N
Business string // Business address indicator: Y or N
CentralDeliveryPoint string // Central delivery point: Y or N
Vacant string // Vacant address indicator: Y or N
}DPV Confirmation Codes:
Y- Address is deliverableD- Address is deliverable but missing secondary (apt, suite)S- Address is deliverable to building, but not to specific unitN- Address is not deliverable
The Corrections field provides visibility into all modifications the USPS API made to standardize your
submitted address according to USPS Publication 28 (Postal Addressing Standards). This is crucial for
understanding what changed and why.
type AddressCorrection struct {
Code string // Correction type code
Text string // Human-readable explanation
}Common Correction Types:
The USPS API applies various standardizations to ensure addresses are deliverable:
- Street Abbreviations - "Street" → "St", "Avenue" → "Ave", "Boulevard" → "Blvd"
- Directional Standardization - "North" → "N", "Southwest" → "SW"
- Unit Designators - "Apartment" → "Apt", "Suite" → "Ste", "Building" → "Bldg"
- City Names - Full city names may be standardized or abbreviated per USPS rules
- ZIP+4 Addition - Missing ZIP+4 codes are added when available
- Secondary Address - Apartment/suite numbers may be reformatted or corrected
- Spelling Corrections - Typos in street names or cities are automatically fixed
Example Corrections Array:
resp, err := client.GetAddress(ctx, req)
if err != nil {
return err
}
// Check what corrections were made
for _, correction := range resp.Corrections {
fmt.Printf("Correction: %s - %s\n", correction.Code, correction.Text)
}
// Example output:
// Correction: st - Street was abbreviated to St
// Correction: zip4 - ZIP+4 code was addedWhy Corrections Matter:
- Audit Trail - Track exactly how user input was modified
- User Feedback - Inform users about changes to improve future submissions
- Data Quality - Identify patterns in address entry issues
- Compliance - Ensure addresses meet USPS standards for optimal delivery
The corrections array helps you understand the transformation from submitted address to the final standardized USPS format, which is essential for deliverability and reducing returns.
// Custom timeout
client := usps.NewClient(tokenProvider, usps.WithTimeout(60 * time.Second))
// Custom HTTP client
client := usps.NewClient(tokenProvider, usps.WithHTTPClient(httpClient))
// Custom base URL (usually for testing)
client := usps.NewClient(tokenProvider, usps.WithBaseURL("https://custom.url"))// Custom scopes
provider := usps.NewOAuthTokenProvider(
clientID,
clientSecret,
usps.WithOAuthScopes("addresses tracking labels"),
)
// Custom refresh buffer (default: 5 minutes)
provider := usps.NewOAuthTokenProvider(
clientID,
clientSecret,
usps.WithTokenRefreshBuffer(10 * time.Minute),
)
// Enable refresh tokens
provider := usps.NewOAuthTokenProvider(
clientID,
clientSecret,
usps.WithRefreshTokens(true),
)
// Testing environment
provider := usps.NewOAuthTokenProvider(
clientID,
clientSecret,
usps.WithOAuthEnvironment("testing"),
)Returned for USPS API errors (4xx, 5xx responses):
type APIError struct {
StatusCode int
ErrorMessage *ErrorMessage
}
func (e *APIError) Error() string {
if e.ErrorMessage != nil && e.ErrorMessage.Error != nil {
return e.ErrorMessage.Error.Message
}
return fmt.Sprintf("API error (status %d)", e.StatusCode)
}Returned for OAuth authentication errors:
type OAuthError struct {
StatusCode int
ErrorMessage *OAuthErrorMessage
}Common OAuth error codes:
invalid_client- Invalid client credentialsinvalid_grant- Invalid authorization code or refresh tokeninvalid_request- Malformed requestunauthorized_client- Client not authorized for grant typeunsupported_grant_type- Grant type not supported
- USPS Addresses API v3 - Complete API specification
- USPS OAuth 2.0 API - OAuth authentication guide
- USPS Developer Portal - Register for API credentials
- pkg.go.dev - Full package documentation
- GitHub Repository - Source code and examples
- API Issues - USPS API Support
- Library Issues - Open an issue on GitHub
- Questions - Check existing issues or start a discussion
# Run all tests
go test ./...
# Run with coverage
go test -cover ./...
# Run with race detection
go test -race ./...Integration tests require valid USPS credentials:
export USPS_CLIENT_ID="your-client-id"
export USPS_CLIENT_SECRET="your-client-secret"
go test -v ./... -tags=integration# Go linting
go vet ./...
gofmt -l .
# Markdown linting
npx markdownlint-cli2 "**/*.md"- Go 1.19+ - Uses standard library features from Go 1.19
- USPS API Credentials - Obtain from USPS Developer Portal
For complete API documentation, see:
MIT License - See LICENSE file for details.
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes with tests
- Run linting and tests (
go test ./... && go vet ./...) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure your PR:
- ✓ Includes tests for new functionality
- ✓ Maintains or improves test coverage
- ✓ Follows Go best practices and idiomatic style
- ✓ Updates documentation as needed
- ✓ Passes all CI checks