usps

package module
v0.0.0-...-bed8341 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2025 License: MIT Imports: 13 Imported by: 0

README

go-usps

A lightweight, production-grade Go client library for the USPS Addresses 3.0 REST API and OAuth 2.0 API.

Go Reference Go Report Card CI CodeQL Markdown Lint

Enterprise-grade address validation and standardization for Go applications.

Why go-usps?

  • 🎯 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

Table of Contents


Quick Start

Get started in 60 seconds:

go get github.com/my-eq/go-usps
package 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


Core Concepts

Understanding USPS Address Validation

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
Three Core Endpoints
1. Address Standardization (GetAddress)

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 info

Use when: You have a complete address and need to validate or standardize it.

2. City/State Lookup (GetCityState)

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.

3. ZIP Code Lookup (GetZIPCode)

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+4

Use when: You have an address without a ZIP code or need to find the ZIP+4.

Authentication

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 refreshed

Alternative (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.

Error Handling

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
}
Environments

The library supports both production and testing environments:

// Production (default)
client := usps.NewClientWithOAuth(clientID, clientSecret)

// Testing
client := usps.NewTestClientWithOAuth(clientID, clientSecret)

Usage Examples

E-commerce: Validate Checkout Addresses

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
}
Bulk Address Processing

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.

Auto-complete ZIP Codes

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
}
Verify Business Addresses

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
}
Format Addresses for Mailing

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
}

Address Parsing

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.

Why Use the Parser?
  • 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
Quick Example
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"
}
Integration with USPS API

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
}
Key Features

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 validation
Common Use Cases

Single-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.


Advanced Usage

Custom Token Provider

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",
})
Custom HTTP Client

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),
)
Retry Logic with Exponential Backoff

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")
}
Circuit Breaker Pattern

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
}
Request Middleware

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
}
Manual OAuth Management

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)
Testing with Mock Responses

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
}

Advanced Topics

Distributed Systems Considerations
Service-to-Service Authentication

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
}
Load Balancing and Failover

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}
}
Caching Strategies
In-Memory Cache with TTL

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
}
Redis-backed Cache

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

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.

Manual Rate Limiting (Advanced)

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)
}
Observability
Structured Logging

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
}
Metrics Collection

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
}
Health Checks

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",
        })
    }
}
Production Checklist

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

API Reference

Response Fields
AddressResponse
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
}
DomesticAddress
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)
}
AddressAdditionalInfo

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 deliverable
  • D - Address is deliverable but missing secondary (apt, suite)
  • S - Address is deliverable to building, but not to specific unit
  • N - Address is not deliverable
AddressCorrection

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 added

Why Corrections Matter:

  1. Audit Trail - Track exactly how user input was modified
  2. User Feedback - Inform users about changes to improve future submissions
  3. Data Quality - Identify patterns in address entry issues
  4. 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.

Configuration Options
Client Options
// 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"))
OAuth Provider Options
// 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"),
)
Error Types
APIError

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)
}
OAuthError

Returned for OAuth authentication errors:

type OAuthError struct {
    StatusCode   int
    ErrorMessage *OAuthErrorMessage
}

Common OAuth error codes:

  • invalid_client - Invalid client credentials
  • invalid_grant - Invalid authorization code or refresh token
  • invalid_request - Malformed request
  • unauthorized_client - Client not authorized for grant type
  • unsupported_grant_type - Grant type not supported

Additional Resources

Official Documentation
Go Package Documentation
Getting Help

Development

Running Tests
# Run all tests
go test ./...

# Run with coverage
go test -cover ./...

# Run with race detection
go test -race ./...
Integration Tests

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
Linting
# Go linting
go vet ./...
gofmt -l .

# Markdown linting
npx markdownlint-cli2 "**/*.md"

Requirements

  • Go 1.19+ - Uses standard library features from Go 1.19
  • USPS API Credentials - Obtain from USPS Developer Portal

API Documentation

For complete API documentation, see:

License

MIT License - See LICENSE file for details.


Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes with tests
  4. Run linting and tests (go test ./... && go vet ./...)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. 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

Documentation

Overview

Package usps provides a lightweight, production-grade Go client library for the USPS Addresses 3.0 REST API and OAuth 2.0 API.

The package implements all three USPS Addresses 3.0 API endpoints:

  • Address Standardization (GetAddress)
  • City/State Lookup (GetCityState)
  • ZIP Code Lookup (GetZIPCode)

And the USPS OAuth 2.0 API endpoints:

  • Token Generation (PostToken) - supports Client Credentials, Refresh Token, and Authorization Code grants
  • Token Revocation (PostRevoke)

Quick Start

The easiest way to use the library is with the convenience function that creates a client with automatic OAuth token management:

client := usps.NewClientWithOAuth("client-id", "client-secret")

Alternatively, create the provider and client separately:

tokenProvider := usps.NewOAuthTokenProvider("client-id", "client-secret")
client := usps.NewClient(tokenProvider)

Standardize an address:

req := &models.AddressRequest{
    StreetAddress: "123 Main St",
    City:          "New York",
    State:         "NY",
}
resp, err := client.GetAddress(context.Background(), req)

Look up city and state by ZIP code:

req := &models.CityStateRequest{ZIPCode: "10001"}
resp, err := client.GetCityState(context.Background(), req)

Look up ZIP code by address:

req := &models.ZIPCodeRequest{
    StreetAddress: "123 Main St",
    City:          "New York",
    State:         "NY",
}
resp, err := client.GetZIPCode(context.Background(), req)

OAuth Authentication

The simplest approach is to use the convenience functions:

client := usps.NewClientWithOAuth("client-id", "client-secret")
// Or for testing environment:
client := usps.NewTestClientWithOAuth("client-id", "client-secret")

The library also provides automatic OAuth token management via OAuthTokenProvider:

tokenProvider := usps.NewOAuthTokenProvider(
    "your-client-id",
    "your-client-secret",
    usps.WithOAuthScopes("addresses tracking"),
    usps.WithTokenRefreshBuffer(10 * time.Minute),
)
client := usps.NewClient(tokenProvider)

The OAuthTokenProvider automatically:

  • Acquires tokens using client credentials flow
  • Caches tokens to minimize API calls
  • Refreshes tokens before expiration (default: 5 minutes before)
  • Handles concurrent access safely

For manual token management, use OAuthClient:

oauthClient := usps.NewOAuthClient()
req := &models.ClientCredentials{
    GrantType:    "client_credentials",
    ClientID:     "your-client-id",
    ClientSecret: "your-client-secret",
    Scope:        "addresses tracking labels",
}
result, err := oauthClient.PostToken(context.Background(), req)

Access tokens expire after 8 hours. Refresh tokens can be used to obtain new access tokens:

req := &models.RefreshTokenCredentials{
    GrantType:    "refresh_token",
    ClientID:     "your-client-id",
    ClientSecret: "your-client-secret",
    RefreshToken: "your-refresh-token",
}
result, err := oauthClient.PostToken(context.Background(), req)

Revoke a refresh token when no longer needed:

req := &models.TokenRevokeRequest{
    Token:         "refresh-token-to-revoke",
    TokenTypeHint: "refresh_token",
}
err := oauthClient.PostRevoke(context.Background(), "client-id", "client-secret", req)

For static tokens (not recommended for production), use StaticTokenProvider:

tokenProvider := usps.NewStaticTokenProvider("your-oauth-token")
client := usps.NewClient(tokenProvider)

Configuration

The client can be configured with various options:

client := usps.NewClient(
    tokenProvider,
    usps.WithTimeout(60 * time.Second),
    usps.WithBaseURL("https://custom.url.com"),
)

For testing, use the test environment:

client := usps.NewTestClient(tokenProvider)
oauthClient := usps.NewOAuthTestClient()

Error Handling

API errors are returned as *APIError with detailed error information:

resp, err := client.GetAddress(ctx, req)
if err != nil {
    if apiErr, ok := err.(*usps.APIError); ok {
        fmt.Printf("API Error: %s\n", apiErr.ErrorMessage.Error.Message)
    }
    return
}

OAuth errors are returned as *OAuthError:

result, err := oauthClient.PostToken(ctx, req)
if err != nil {
    if oauthErr, ok := err.(*usps.OAuthError); ok {
        fmt.Printf("OAuth Error: %s\n", oauthErr.ErrorMessage.Error)
    }
    return
}

For more information, see:

Index

Examples

Constants

View Source
const (
	// ProductionBaseURL is the base URL for the USPS production API
	ProductionBaseURL = "https://apis.usps.com/addresses/v3"
	// TestingBaseURL is the base URL for the USPS testing API
	TestingBaseURL = "https://apis-tem.usps.com/addresses/v3"
	// DefaultTimeout is the default timeout for HTTP requests
	DefaultTimeout = 30 * time.Second
)
View Source
const (
	// OAuthProductionBaseURL is the base URL for the USPS OAuth production API
	OAuthProductionBaseURL = "https://apis.usps.com/oauth2/v3"
	// OAuthTestingBaseURL is the base URL for the USPS OAuth testing API
	OAuthTestingBaseURL = "https://apis-tem.usps.com/oauth2/v3"
)
View Source
const (
	// DefaultTokenRefreshBuffer is the default time before token expiration to refresh.
	// Tokens are refreshed 5 minutes before they expire by default.
	DefaultTokenRefreshBuffer = 5 * time.Minute

	// MaxInvalidExpirationRetries is the maximum number of consecutive times
	// the provider will retry when receiving invalid token expiration (<=0).
	// After this limit is exceeded, GetToken will return an error.
	MaxInvalidExpirationRetries = 3
)

Variables

This section is empty.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	StatusCode   int
	ErrorMessage models.ErrorMessage
}

APIError represents an error returned by the USPS API

func (*APIError) Error

func (e *APIError) Error() string

Error implements the error interface

type AddressResult

type AddressResult struct {
	Index    int
	Request  *models.AddressRequest
	Response *models.AddressResponse
	Error    error
}

AddressResult represents the result of a bulk address validation

type BulkConfig

type BulkConfig struct {
	// MaxConcurrency is the maximum number of concurrent requests (default: 10)
	MaxConcurrency int
	// RequestsPerSecond is the rate limit for API requests (default: 10)
	RequestsPerSecond int
	// MaxRetries is the maximum number of retry attempts for failed requests (default: 3)
	MaxRetries int
	// RetryBackoff is the base duration for exponential backoff (default: 1 second)
	RetryBackoff time.Duration
	// ProgressCallback is called after each request completes (optional)
	ProgressCallback func(completed, total int, err error)
}

BulkConfig contains configuration options for bulk operations

func DefaultBulkConfig

func DefaultBulkConfig() *BulkConfig

DefaultBulkConfig returns a BulkConfig with sensible defaults

Example

ExampleDefaultBulkConfig demonstrates using the default bulk configuration

client := usps.NewClientWithOAuth("client-id", "client-secret")

// Use default configuration
processor := usps.NewBulkProcessor(client, usps.DefaultBulkConfig())

requests := []*models.AddressRequest{
	{StreetAddress: "475 L'Enfant Plaza SW", City: "Washington", State: "DC"},
}

results := processor.ProcessAddresses(context.Background(), requests)

fmt.Printf("Processed %d addresses\n", len(results))
Output:

Processed 1 addresses

type BulkProcessor

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

BulkProcessor handles bulk operations with rate limiting and retries

func NewBulkProcessor

func NewBulkProcessor(client *Client, config *BulkConfig) *BulkProcessor

NewBulkProcessor creates a new BulkProcessor with the given client and config

func (*BulkProcessor) ProcessAddresses

func (bp *BulkProcessor) ProcessAddresses(ctx context.Context, requests []*models.AddressRequest) []*AddressResult

ProcessAddresses validates multiple addresses concurrently with rate limiting

Example

ExampleBulkProcessor_ProcessAddresses demonstrates bulk address validation

// Create client (in production, use real credentials)
client := usps.NewClientWithOAuth("client-id", "client-secret")

// 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)
	},
}

processor := usps.NewBulkProcessor(client, config)

// Prepare requests
requests := []*models.AddressRequest{
	{StreetAddress: "123 Main St", City: "New York", State: "NY"},
	{StreetAddress: "456 Oak Ave", City: "Los Angeles", State: "CA"},
	{StreetAddress: "789 Elm Blvd", City: "Chicago", State: "IL"},
}

// Process bulk addresses
results := processor.ProcessAddresses(context.Background(), requests)

// 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)
}

func (*BulkProcessor) ProcessCityStates

func (bp *BulkProcessor) ProcessCityStates(ctx context.Context, requests []*models.CityStateRequest) []*CityStateResult

ProcessCityStates looks up city/state for multiple ZIP codes concurrently with rate limiting

Example

ExampleBulkProcessor_ProcessCityStates demonstrates bulk city/state lookup

client := usps.NewClientWithOAuth("client-id", "client-secret")
processor := usps.NewBulkProcessor(client, usps.DefaultBulkConfig())

requests := []*models.CityStateRequest{
	{ZIPCode: "10001"},
	{ZIPCode: "90210"},
	{ZIPCode: "60601"},
}

results := processor.ProcessCityStates(context.Background(), requests)

for _, result := range results {
	if result.Error != nil {
		log.Printf("ZIP %s failed: %v", result.Request.ZIPCode, result.Error)
		continue
	}

	fmt.Printf("%s: %s, %s\n",
		result.Request.ZIPCode,
		result.Response.City,
		result.Response.State)
}

func (*BulkProcessor) ProcessZIPCodes

func (bp *BulkProcessor) ProcessZIPCodes(ctx context.Context, requests []*models.ZIPCodeRequest) []*ZIPCodeResult

ProcessZIPCodes looks up ZIP codes for multiple addresses concurrently with rate limiting

Example

ExampleBulkProcessor_ProcessZIPCodes demonstrates bulk ZIP code lookup

client := usps.NewClientWithOAuth("client-id", "client-secret")

// Custom configuration for high-volume processing
config := &usps.BulkConfig{
	MaxConcurrency:    20, // Higher concurrency
	RequestsPerSecond: 50, // Higher rate limit (if your API plan allows)
	MaxRetries:        5,  // More retries for critical operations
}

processor := usps.NewBulkProcessor(client, config)

requests := []*models.ZIPCodeRequest{
	{StreetAddress: "123 Main St", City: "New York", State: "NY"},
	{StreetAddress: "456 Oak Ave", City: "Los Angeles", State: "CA"},
}

results := processor.ProcessZIPCodes(context.Background(), requests)

for _, result := range results {
	if result.Error != nil {
		log.Printf("Request %d failed: %v", result.Index, result.Error)
		continue
	}

	zip := result.Response.Address.ZIPCode
	if result.Response.Address.ZIPPlus4 != nil && *result.Response.Address.ZIPPlus4 != "" {
		zip = fmt.Sprintf("%s-%s", zip, *result.Response.Address.ZIPPlus4)
	}

	fmt.Printf("ZIP Code: %s\n", zip)
}

type CityStateResult

type CityStateResult struct {
	Index    int
	Request  *models.CityStateRequest
	Response *models.CityStateResponse
	Error    error
}

CityStateResult represents the result of a bulk city/state lookup

type Client

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

Client is the USPS API client

func NewClient

func NewClient(tokenProvider TokenProvider, opts ...Option) *Client

NewClient creates a new USPS API client

func NewClientWithOAuth

func NewClientWithOAuth(clientID, clientSecret string, opts ...OAuthTokenOption) *Client

NewClientWithOAuth creates a new USPS API client with automatic OAuth token management. This is a convenience function that creates an OAuthTokenProvider and a Client in one step.

The OAuth provider automatically handles token acquisition, caching, and refresh. Additional OAuth options can be passed to customize the provider behavior.

Example:

client := usps.NewClientWithOAuth("client-id", "client-secret")

Example with options:

client := usps.NewClientWithOAuth(
    "client-id",
    "client-secret",
    usps.WithOAuthScopes("addresses tracking"),
    usps.WithTokenRefreshBuffer(10 * time.Minute),
)
Example
// Create a client with automatic OAuth token management in one step
client := usps.NewClientWithOAuth("your-client-id", "your-client-secret")

// Use the client - tokens are managed automatically
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("Address: %s\n", resp.Address.StreetAddress)

func NewTestClient

func NewTestClient(tokenProvider TokenProvider, opts ...Option) *Client

NewTestClient creates a new USPS API client configured for the testing environment

Example
// Create a token provider with your test OAuth token
tokenProvider := usps.NewStaticTokenProvider("your-test-oauth-token")

// Create a client configured for the testing environment
client := usps.NewTestClient(tokenProvider)

req := &models.AddressRequest{
	StreetAddress: "123 Test St",
	City:          "Test City",
	State:         "NY",
}

resp, err := client.GetAddress(context.Background(), req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

fmt.Printf("Test Address: %s\n", resp.Address.StreetAddress)

func NewTestClientWithOAuth

func NewTestClientWithOAuth(clientID, clientSecret string, opts ...OAuthTokenOption) *Client

NewTestClientWithOAuth creates a new USPS API client with automatic OAuth token management configured for the testing environment. This is a convenience function that combines NewOAuthTestTokenProvider and NewTestClient.

Example:

client := usps.NewTestClientWithOAuth("test-client-id", "test-client-secret")
Example
// Create a test client with automatic OAuth token management
client := usps.NewTestClientWithOAuth("test-client-id", "test-client-secret")

req := &models.CityStateRequest{
	ZIPCode: "10001",
}

resp, err := client.GetCityState(context.Background(), req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

fmt.Printf("City: %s, State: %s\n", resp.City, resp.State)

func (*Client) GetAddress

func (c *Client) GetAddress(ctx context.Context, req *models.AddressRequest) (*models.AddressResponse, error)

GetAddress standardizes a street address

Example
// Create a token provider with your OAuth token
tokenProvider := usps.NewStaticTokenProvider("your-oauth-token")

// Create a new client
client := usps.NewClient(tokenProvider)

// Prepare the address request
req := &models.AddressRequest{
	StreetAddress: "123 Main St",
	City:          "New York",
	State:         "NY",
}

// Get the standardized address
resp, err := client.GetAddress(context.Background(), req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

fmt.Printf("Standardized Address: %s, %s, %s %s\n",
	resp.Address.StreetAddress,
	resp.Address.City,
	resp.Address.State,
	resp.Address.ZIPCode)

func (*Client) GetCityState

func (c *Client) GetCityState(ctx context.Context, req *models.CityStateRequest) (*models.CityStateResponse, error)

GetCityState returns the city and state for a given ZIP code

Example
// Create a token provider with your OAuth token
tokenProvider := usps.NewStaticTokenProvider("your-oauth-token")

// Create a new client
client := usps.NewClient(tokenProvider)

// Prepare the city-state request
req := &models.CityStateRequest{
	ZIPCode: "10001",
}

// Get the city and state
resp, err := client.GetCityState(context.Background(), req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

fmt.Printf("City: %s, State: %s\n", resp.City, resp.State)

func (*Client) GetZIPCode

func (c *Client) GetZIPCode(ctx context.Context, req *models.ZIPCodeRequest) (*models.ZIPCodeResponse, error)

GetZIPCode returns the ZIP code for a given address

Example
// Create a token provider with your OAuth token
tokenProvider := usps.NewStaticTokenProvider("your-oauth-token")

// Create a new client
client := usps.NewClient(tokenProvider)

// Prepare the ZIP code request
req := &models.ZIPCodeRequest{
	StreetAddress: "123 Main St",
	City:          "New York",
	State:         "NY",
}

// Get the ZIP code
resp, err := client.GetZIPCode(context.Background(), req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

fmt.Printf("ZIP Code: %s", resp.Address.ZIPCode)
if resp.Address.ZIPPlus4 != nil {
	fmt.Printf("-%s", *resp.Address.ZIPPlus4)
}
fmt.Println()

type OAuthClient

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

OAuthClient is the USPS OAuth API client for managing OAuth 2.0 tokens. It supports Client Credentials, Refresh Token, and Authorization Code grant types.

func NewOAuthClient

func NewOAuthClient(opts ...Option) *OAuthClient

NewOAuthClient creates a new USPS OAuth API client configured for the production environment. Use functional options to customize the client configuration.

Example:

client := usps.NewOAuthClient()
client := usps.NewOAuthClient(usps.WithTimeout(60 * time.Second))

func NewOAuthTestClient

func NewOAuthTestClient(opts ...Option) *OAuthClient

NewOAuthTestClient creates a new USPS OAuth API client configured for the testing environment. This is equivalent to calling NewOAuthClient with WithBaseURL(OAuthTestingBaseURL).

Example:

client := usps.NewOAuthTestClient()
Example
// Create an OAuth client configured for the testing environment
client := usps.NewOAuthTestClient()

req := &models.ClientCredentials{
	GrantType:    "client_credentials",
	ClientID:     "test-client-id",
	ClientSecret: "test-client-secret",
}

result, err := client.PostToken(context.Background(), req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

accessTokenResp := result.(*models.ProviderAccessTokenResponse)
fmt.Printf("Test Access Token obtained (expires in %d seconds)\n", accessTokenResp.ExpiresIn)

func (*OAuthClient) PostRevoke

func (c *OAuthClient) PostRevoke(ctx context.Context, clientID, clientSecret string, req *models.TokenRevokeRequest) error

PostRevoke revokes an OAuth token using HTTP Basic Authentication. This method is used to invalidate refresh tokens that are no longer needed or suspected of being compromised.

The clientID and clientSecret are used for Basic Authentication as required by the USPS OAuth API. The request specifies which token to revoke and optionally provides a hint about the token type.

Example:

req := &models.TokenRevokeRequest{
    Token:         "refresh-token-to-revoke",
    TokenTypeHint: "refresh_token",
}
err := client.PostRevoke(ctx, "client-id", "client-secret", req)
Example
// Create an OAuth client
client := usps.NewOAuthClient()

// Prepare the revoke request
req := &models.TokenRevokeRequest{
	Token:         "refresh-token-to-revoke",
	TokenTypeHint: "refresh_token",
}

// Revoke the token
err := client.PostRevoke(context.Background(), "your-client-id", "your-client-secret", req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

fmt.Println("Token revoked successfully")

func (*OAuthClient) PostToken

func (c *OAuthClient) PostToken(ctx context.Context, req interface{}) (interface{}, error)

PostToken generates OAuth tokens based on the grant type. It supports three grant types:

  • Client Credentials: Pass *models.ClientCredentials to get an access token
  • Refresh Token: Pass *models.RefreshTokenCredentials to refresh an access token
  • Authorization Code: Pass *models.AuthorizationCodeCredentials to exchange an auth code

The method returns either *models.ProviderAccessTokenResponse (for client credentials) or *models.ProviderTokensResponse (for grants that include a refresh token).

Access tokens are valid for 8 hours. Refresh tokens are valid for 7 days.

Example (Client Credentials):

req := &models.ClientCredentials{
    GrantType:    "client_credentials",
    ClientID:     "your-client-id",
    ClientSecret: "your-client-secret",
    Scope:        "addresses tracking",
}
result, err := client.PostToken(ctx, req)
if err != nil {
    return err
}
accessTokenResp := result.(*models.ProviderAccessTokenResponse)

Example (Refresh Token):

req := &models.RefreshTokenCredentials{
    GrantType:    "refresh_token",
    ClientID:     "your-client-id",
    ClientSecret: "your-client-secret",
    RefreshToken: "your-refresh-token",
}
result, err := client.PostToken(ctx, req)
if err != nil {
    return err
}
tokensResp := result.(*models.ProviderTokensResponse)
Example (ClientCredentials)
// Create an OAuth client
client := usps.NewOAuthClient()

// Prepare the client credentials request
req := &models.ClientCredentials{
	GrantType:    "client_credentials",
	ClientID:     "your-client-id",
	ClientSecret: "your-client-secret",
	Scope:        "addresses tracking labels",
}

// Get an access token
result, err := client.PostToken(context.Background(), req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

accessTokenResp := result.(*models.ProviderAccessTokenResponse)
fmt.Printf("Access Token: %s\n", accessTokenResp.AccessToken)
fmt.Printf("Expires In: %d seconds\n", accessTokenResp.ExpiresIn)
Example (RefreshToken)
// Create an OAuth client
client := usps.NewOAuthClient()

// Prepare the refresh token request
req := &models.RefreshTokenCredentials{
	GrantType:    "refresh_token",
	ClientID:     "your-client-id",
	ClientSecret: "your-client-secret",
	RefreshToken: "your-refresh-token",
}

// Get a new access token and refresh token
result, err := client.PostToken(context.Background(), req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

tokensResp := result.(*models.ProviderTokensResponse)
fmt.Printf("Access Token: %s\n", tokensResp.AccessToken)
fmt.Printf("New Refresh Token: %s\n", tokensResp.RefreshToken)

type OAuthError

type OAuthError struct {
	StatusCode   int
	ErrorMessage models.StandardErrorResponse
}

OAuthError represents an error returned by the USPS OAuth API

func (*OAuthError) Error

func (e *OAuthError) Error() string

Error implements the error interface

type OAuthTokenOption

type OAuthTokenOption func(*OAuthTokenProvider)

OAuthTokenOption is a functional option for configuring OAuthTokenProvider.

func WithOAuthEnvironment

func WithOAuthEnvironment(env string) OAuthTokenOption

WithOAuthEnvironment configures the OAuth environment. Use "production" (default) or "testing" to set the OAuth base URL.

func WithOAuthScopes

func WithOAuthScopes(scopes string) OAuthTokenOption

WithOAuthScopes sets the OAuth scopes for token requests. Multiple scopes should be space-separated (e.g., "addresses tracking labels").

func WithRefreshTokens

func WithRefreshTokens(enabled bool) OAuthTokenOption

WithRefreshTokens enables the use of refresh tokens when available. When enabled, the provider will use refresh tokens to obtain new access tokens instead of always using client credentials. This is more efficient and allows for longer-lived sessions. Default is false (always use client credentials).

func WithTokenRefreshBuffer

func WithTokenRefreshBuffer(duration time.Duration) OAuthTokenOption

WithTokenRefreshBuffer sets how early before expiration to refresh the token. Default is DefaultTokenRefreshBuffer (5 minutes) before the token expires. This ensures tokens are refreshed proactively before they expire.

type OAuthTokenProvider

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

OAuthTokenProvider is a TokenProvider that automatically manages OAuth 2.0 tokens. It handles token acquisition, caching, and automatic refresh before expiration. This provider is thread-safe and suitable for concurrent use in production environments.

func NewOAuthTestTokenProvider

func NewOAuthTestTokenProvider(clientID, clientSecret string, opts ...OAuthTokenOption) *OAuthTokenProvider

NewOAuthTestTokenProvider creates a new OAuthTokenProvider configured for the testing environment. This is equivalent to calling NewOAuthTokenProvider with WithOAuthEnvironment("testing").

Example:

provider := usps.NewOAuthTestTokenProvider("test-client-id", "test-client-secret")
client := usps.NewTestClient(provider)

func NewOAuthTokenProvider

func NewOAuthTokenProvider(clientID, clientSecret string, opts ...OAuthTokenOption) *OAuthTokenProvider

NewOAuthTokenProvider creates a new OAuthTokenProvider that automatically manages OAuth 2.0 tokens using the client credentials flow.

The provider handles:

  • Initial token acquisition
  • Token caching
  • Automatic refresh before expiration (default: 5 minutes before expiry)
  • Thread-safe concurrent access

Access tokens from USPS are valid for 8 hours. The provider will automatically refresh the token 5 minutes before expiration (configurable via WithTokenRefreshBuffer).

Example:

provider := usps.NewOAuthTokenProvider("client-id", "client-secret")
client := usps.NewClient(provider)

Example with options:

provider := usps.NewOAuthTokenProvider(
    "client-id",
    "client-secret",
    usps.WithOAuthScopes("addresses tracking"),
    usps.WithTokenRefreshBuffer(10 * time.Minute),
    usps.WithOAuthEnvironment("testing"),
)
Example
// Create an OAuth token provider with your client credentials
// This will automatically handle token acquisition and refresh
tokenProvider := usps.NewOAuthTokenProvider("your-client-id", "your-client-secret")

// Create a client using the OAuth token provider
client := usps.NewClient(tokenProvider)

// The token provider automatically manages tokens
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("Address: %s\n", resp.Address.StreetAddress)
Example (WithOptions)
// Create an OAuth token provider with custom options
tokenProvider := usps.NewOAuthTokenProvider(
	"your-client-id",
	"your-client-secret",
	usps.WithOAuthScopes("addresses tracking labels"),
	usps.WithTokenRefreshBuffer(10*time.Minute),
	usps.WithOAuthEnvironment("testing"),
	usps.WithRefreshTokens(true),
)

// Use with the USPS client
client := usps.NewClient(tokenProvider)

req := &models.CityStateRequest{
	ZIPCode: "10001",
}

resp, err := client.GetCityState(context.Background(), req)
if err != nil {
	log.Fatalf("Error: %v", err)
}

fmt.Printf("City: %s, State: %s\n", resp.City, resp.State)

func (*OAuthTokenProvider) GetToken

func (p *OAuthTokenProvider) GetToken(ctx context.Context) (string, error)

GetToken returns a valid OAuth token, refreshing it if necessary. This method is thread-safe and implements the TokenProvider interface.

type Option

type Option func(*Client)

Option is a functional option for configuring the Client

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL sets a custom base URL for the client

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) Option

WithHTTPClient sets a custom HTTP client

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout sets a custom timeout for the HTTP client

type StaticTokenProvider

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

StaticTokenProvider is a simple TokenProvider that returns a fixed token

func NewStaticTokenProvider

func NewStaticTokenProvider(token string) *StaticTokenProvider

NewStaticTokenProvider creates a new StaticTokenProvider with the given token

func (*StaticTokenProvider) GetToken

func (p *StaticTokenProvider) GetToken(ctx context.Context) (string, error)

GetToken returns the static token

type TokenProvider

type TokenProvider interface {
	// GetToken returns the current OAuth token
	GetToken(ctx context.Context) (string, error)
}

TokenProvider is an interface for providing OAuth tokens

type ZIPCodeResult

type ZIPCodeResult struct {
	Index    int
	Request  *models.ZIPCodeRequest
	Response *models.ZIPCodeResponse
	Error    error
}

ZIPCodeResult represents the result of a bulk ZIP code lookup

Directories

Path Synopsis
Package models provides strongly-typed data structures for the USPS Addresses API and OAuth 2.0 API.
Package models provides strongly-typed data structures for the USPS Addresses API and OAuth 2.0 API.
Package parser provides comprehensive parsing for free-form address entry.
Package parser provides comprehensive parsing for free-form address entry.

Jump to

Keyboard shortcuts

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