Skip to content

Latest commit

 

History

62 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ziptax Python SDK

Official Python SDK for the Ziptax API - Get accurate sales and use tax rates for any US or Canadian address, with optional TaxCloud order management support.

Python Version License

Features

Core Features (ZipTax API)

  • 🚀 Simple and intuitive API
  • 🛒 Cart tax calculation with per-item tax rates
  • 🏷️ Product code (TIC) search, AI-powered recommendation, and full TIC data
  • 📐 Product rate rules, extended address components, and shipping rules on v6.0 lookups
  • 🔄 Automatic retry logic with exponential backoff
  • ✅ Input validation
  • 🔍 Type hints for better IDE support
  • 📦 Pydantic models for response validation
  • 🔒 Comprehensive error handling
  • ⚡ Support for concurrent operations
  • 🧪 Well-tested with high code coverage

Merchant Compliance (Platform Integrations)

  • 🏬 Merchant Management: Create, update, delete, and list merchants
  • 🧾 Transactions: Cart calculation, orders, and refunds per merchant
  • 📄 Exemption Certificates: Create, retrieve, list, and delete
  • 🔀 One API Key: Everything routes through api.zip-tax.com with your Ziptax key
  • 🧭 Both Compliance Models: Self-managed merchants use the Ziptax rate engine; TaxCloud-connected merchants forward to TaxCloud using stored credentials

TaxCloud Integration (Optional, Deprecated)

  • 📋 Order Management: Create, retrieve, and update orders
  • 💰 Refund Processing: Full and partial refund support
  • 🔐 Optional Configuration: TaxCloud features only enabled when credentials provided
  • ⚠️ Deprecated in 0.3.0: These call the TaxCloud API directly, which is no longer the documented integration path. Use the merchant functions instead (see Migrating to the merchant layer)

Installation

pip install ziptax-sdk

Quick Start

from ziptax import ZipTaxClient

# Initialize the client with your API key
client = ZipTaxClient.api_key("your-api-key-here")

# Get sales tax by address
response = client.request.GetSalesTaxByAddress(
    "200 Spectrum Center Drive, Irvine, CA 92618"
)

print(f"Address: {response.address_detail.normalized_address}")
if response.tax_summaries:
    for summary in response.tax_summaries:
        print(f"{summary.summary_name}: {summary.rate * 100:.2f}%")

# Always close the client when done
client.close()

Usage

Initialize the Client

from ziptax import ZipTaxClient

# Basic initialization
client = ZipTaxClient.api_key("your-api-key-here")

# With custom configuration
client = ZipTaxClient.api_key(
    "your-api-key-here",
    timeout=60,           # Request timeout in seconds
    max_retries=5,        # Maximum retry attempts
    retry_delay=2.0,      # Base delay between retries
)

# Using as a context manager (recommended)
with ZipTaxClient.api_key("your-api-key-here") as client:
    response = client.request.GetSalesTaxByAddress("123 Main St")

Get Sales Tax by Address

response = client.request.GetSalesTaxByAddress(
    address="200 Spectrum Center Drive, Irvine, CA 92618",
    country_code="USA",      # Optional: USA, CAN, PRI, ASM, GUM, MNP, VIR
    historical="202401",     # Optional: Historical date (YYYYMM format)
    format="json",           # Optional: Response format (default: "json")
)

# Access response data
print(response.address_detail.normalized_address)
print(response.address_detail.geo_lat)
print(response.address_detail.geo_lng)

# Response code
print(f"Response: {response.metadata.response.code} - {response.metadata.response.message}")

# Tax summaries with display rates
if response.tax_summaries:
    for summary in response.tax_summaries:
        print(f"{summary.summary_name}: {summary.rate}")
        for display_rate in summary.display_rates:
            print(f"  {display_rate.name}: {display_rate.rate}")

# Base rates by jurisdiction
if response.base_rates:
    for rate in response.base_rates:
        print(f"{rate.jur_name} ({rate.jur_type}): {rate.rate}")

# Sourcing rules
if response.sourcing_rules:
    print(f"Sourcing: {response.sourcing_rules.value}")

Extended v6.0 Options

GetSalesTaxByAddress and GetSalesTaxByGeoLocation accept additional parameters that enrich the response.

response = client.request.GetSalesTaxByAddress(
    address="200 Spectrum Center Drive, Irvine, CA 92618",
    taxability_code="20010",         # Adds product_detail with rate rules
    adjustment="auto",               # "auto" (default), "origin", "destination"
    address_detail_extended=True,    # Adds address_detail.address components
    shipping_extended=True,          # Adds shipping.shipping_extended
    city="Irvine",                   # Narrow the lookup
    state="CA",
)

# Product rate rules for the requested TIC
if response.product_detail:
    code = response.product_detail.taxability_code
    print(f"{code.title} ({code.rate_action_code}): {code.rate_action_message}")
    for rule in code.rate_rules or []:
        print(f"  {rule.jur_tax_code}: rate={rule.effective_tax_rate}")

# Geocoded address broken into parts
if response.address_detail.address:
    parts = response.address_detail.address
    print(f"{parts.house_number} {parts.street}, {parts.city} {parts.postal_code}")

# Detailed shipping rule.
# response.shipping is itself Optional - it is None for some regions.
if response.shipping and response.shipping.shipping_extended:
    rule = response.shipping.shipping_extended
    print(f"{rule.state_code}: {rule.rule} - {rule.description}")

rate_action_code reports the outcome of the TIC lookup: T00 (valid, rules listed), T01 (valid, no applicable rules), T02 (invalid TIC), T03 (invalid TIC format).

Get Sales Tax by Geolocation

response = client.request.GetSalesTaxByGeoLocation(
    lat="33.6489",
    lng="-117.8386",
    country_code="USA",
    format="json",
)

print(response.address_detail.normalized_address)

Get Rates by Postal Code

response = client.request.GetRatesByPostalCode(
    postal_code="92694",
    format="json",
)

# Response includes all tax jurisdictions for the postal code
for result in response.results:
    print(f"{result.geo_city}, {result.geo_state}")
    print(f"Sales Tax: {result.tax_sales * 100:.2f}%")
    print(f"Use Tax: {result.tax_use * 100:.2f}%")

Postal-code lookups also accept state, city, county, historical, and sat_item_total. Supplying sat_item_total on a Tennessee lookup adds a Single Article Tax breakdown:

response = client.request.GetRatesByPostalCode("37201", sat_item_total=1600.0)

if response.sat_tax_detail:
    print(f"Local tax total: {response.sat_tax_detail.local_tax_total}")

Get Account Metrics

# Simplified v6.0 counters (GET /account/v60/metrics)
metrics = client.request.GetAccountMetrics()

print(f"Requests: {metrics.request_count:,} / {metrics.request_limit:,}")
print(f"Usage: {metrics.usage_percent:.2f}%")
print(f"Account Active: {metrics.is_active}")
print(f"Message: {metrics.message}")

# Per-pool breakdown (GET /account/metrics)
usage = client.request.GetAccountUsage()

print(f"Core:     {usage.core_request_count:,} / {usage.core_request_limit:,}")
print(f"Geo:      {usage.geo_request_count:,} / {usage.geo_request_limit:,}")
print(f"Merchant: {usage.merchant_request_count:,} / {usage.merchant_request_limit:,}")
print(f"Geocoding enabled: {usage.geo_enabled}")

Get TIC Data

data = client.request.GetTicData()

for entry in data.tic_list[:5]:
    tic = entry.tic
    print(f"{tic.id}: {tic.title} (parent: {tic.parent or 'top-level'})")

Health and System Metadata

health = client.request.GetHealth()
print(f"{health.status} - taxdata {health.components.taxdata}")

metadata = client.request.GetSystemMetadata()
print(f"{metadata.hostname} on {metadata.go_version}")

Search Product Codes (TIC)

Search for Taxability Information Codes (TICs) using a natural language product description. Returns all matching codes ranked and scored by relevance.

response = client.request.SearchProductCodes(
    "baked goods sold in plastic packaging"
)

for result in response.results:
    print(f"TIC {result.tic_id}: {result.label}")
    print(f"  Score: {result.score:.4f} (Rank: {result.rank})")
    print(f"  Description: {result.description}")

# The API also returns a pagination cursor and its schema URL
print(response.next_cursor, response.schema_url)

Use the returned tic_id as the taxability_code parameter in rate requests or cart line items. For v60 rate requests, convert to str first:

# Use with rate requests (taxability_code is a string parameter)
tic = response.results[0].tic_id
tax = client.request.GetSalesTaxByAddress(
    "200 Spectrum Center Dr, Irvine, CA 92618",
    taxability_code=str(tic),
)

# Use with cart line items (taxability_code is an integer)
CartLineItem(item_id="item-1", price=10.00, quantity=1, taxability_code=tic)

Recommend Product Code (TIC)

Get an AI-powered product code recommendation. Returns a single best-match TIC with higher accuracy than the standard search. Has slightly higher latency due to the AI processing step.

response = client.request.RecommendProductCode(
    "baked goods sold in plastic packaging"
)

prediction = response.predictions[0]
if prediction.status == "success":
    print(f"Recommended TIC: {prediction.tic_id} ({prediction.label})")
    print(f"  TIC Description: {prediction.tic_description}")
    print(f"  Product Description: {prediction.product_description}")
else:
    print(f"Recommendation failed: {prediction.error}")

Calculate Cart Tax

Calculate sales tax for a shopping cart with multiple line items. CalculateCart uses dual-routing: when TaxCloud credentials are configured on the client, the request is automatically routed to the TaxCloud API; otherwise it is sent to the ZipTax API. The input is the same CalculateCartRequest in both cases, but the response type differs:

  • Without TaxCloud credentials -- returns a CalculateCartResponse (ZipTax API)
  • With TaxCloud credentials -- returns a TaxCloudCalculateCartResponse (TaxCloud API)
from ziptax.models import (
    CalculateCartRequest,
    CartItem,
    CartAddress,
    CartCurrency,
    CartLineItem,
)

# Build the cart request
request = CalculateCartRequest(
    items=[
        CartItem(
            customer_id="customer-453",
            currency=CartCurrency(currency_code="USD"),
            destination=CartAddress(
                address="200 Spectrum Center Dr, Irvine, CA 92618"
            ),
            origin=CartAddress(
                address="323 Washington Ave N, Minneapolis, MN 55401"
            ),
            line_items=[
                CartLineItem(
                    item_id="item-1",
                    price=10.75,
                    quantity=1.5,
                ),
                CartLineItem(
                    item_id="item-2",
                    price=25.00,
                    quantity=2.0,
                    taxability_code=0,
                ),
            ],
        )
    ]
)

# Calculate tax (routes to ZipTax or TaxCloud based on client config)
result = client.request.CalculateCart(request)

# Access results
cart = result.items[0]
print(f"Cart ID: {cart.cart_id}")
for item in cart.line_items:
    print(f"  {item.item_id}: rate={item.tax.rate}, amount=${item.tax.amount:.2f}")

Validation

The cart models enforce constraints at construction time via Pydantic:

  • items must contain exactly 1 cart
  • line_items must contain 1-250 items
  • price and quantity must be greater than 0
  • currency_code must be "USD"
from pydantic import ValidationError

try:
    CartLineItem(item_id="item-1", price=-5.00, quantity=1.0)
except ValidationError as e:
    print(e)  # price must be greater than 0

Merchant Compliance

Platform integrations that serve multiple merchants use the merchant layer. Everything runs through api.zip-tax.com with your Ziptax API key; each call identifies the merchant with a merchant_id.

Ziptax routes on the merchant's compliance model:

Self-managed TaxCloud-connected
Created with merchant_type="self-managed" merchant_type="taxcloud" (default)
Active Immediately, no invite Once the merchant connects
Tax calculation Ziptax rate engine, in-process Forwarded to TaxCloud
Persisted No Yes
Discounts Not supported Supported
Orders / refunds / certificates Not available (403) Available
Plan Pro and Enterprise Enterprise

Create a Merchant

from ziptax import ZipTaxClient
from ziptax.models import CreateMerchantRequest, MerchantType

client = ZipTaxClient.api_key("your-ziptax-api-key")

merchant = client.request.CreateMerchant(
    CreateMerchantRequest(
        merchant_name="Acme Supply Co",
        contact_email="ops@acme.example",
        reference_id="your-internal-id-1042",
        merchant_type=MerchantType.SELF_MANAGED,
    )
)
print(merchant.merchant_id)  # UUID to use on every later call

To connect a merchant who already has a TaxCloud account, create them with merchant_type="taxcloud" and store their credentials:

from ziptax.models import SetMerchantCredentialsRequest

client.request.SetMerchantCredentials(
    SetMerchantCredentialsRequest(
        merchant_id=merchant.merchant_id,
        connection_id="25eb9b97-5acb-492d-b720-c03e79cf715a",
        api_key="the-merchants-taxcloud-api-key",
    )
)

To invite a merchant who does not yet use TaxCloud, set send_taxcloud_invite=True on CreateMerchant instead.

Check where a merchant stands with GetMerchant:

merchant = client.request.GetMerchant("9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13")
# status is one of: taxcloud_invited, taxcloud_connected,
# taxcloud_disconnected, external_compliance (self-managed)
print(merchant.status)

Calculate Cart Tax for a Merchant

The same request body works for both compliance models.

from ziptax.models import (
    MerchantAddress,
    MerchantCalculateCartRequest,
    MerchantCart,
    MerchantCartLineItem,
    MerchantCurrency,
)

result = client.request.MerchantCalculateCart(
    MerchantCalculateCartRequest(
        merchant_id="9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13",
        items=[
            MerchantCart(
                cart_id="my-cart-1",
                customer_id="customer-453",
                currency=MerchantCurrency(currency_code="USD"),
                origin=MerchantAddress(
                    line1="1600 Amphitheatre Pkwy",
                    city="Mountain View",
                    state="CA",
                    zip="94043",
                ),
                destination=MerchantAddress(
                    line1="350 5th Ave",
                    city="New York",
                    state="NY",
                    zip="10118",
                ),
                line_items=[
                    MerchantCartLineItem(
                        index=0, item_id="sku-1", price=10.75, quantity=1.5, tic=0
                    ),
                    MerchantCartLineItem(
                        index=1, item_id="ship", price=8.95, quantity=1, tic=10001
                    ),
                ],
            )
        ],
    )
)

for item in result.items:
    for line in item.line_items:
        print(f"{line.item_id}: rate={line.tax.rate} amount={line.tax.amount}")

connection_id and transaction_date are present only on the TaxCloud-connected path. On the self-managed path they are None, because nothing is persisted and no TaxCloud call is made.

Orders and Refunds

Available to TaxCloud-connected merchants only.

from ziptax.models import (
    MerchantCreateOrderFromCartRequest,
    MerchantCreateRefundRequest,
    MerchantGetOrderRequest,
    MerchantRefundItem,
)

# Capture a calculated cart as an order
order = client.request.MerchantCreateOrderFromCart(
    MerchantCreateOrderFromCartRequest(
        merchant_id="9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13",
        cart_id="my-cart-1",
        order_id="my-order-1",
        completed=True,
    )
)

# Retrieve it, including refunds
order = client.request.MerchantGetOrder(
    MerchantGetOrderRequest(
        merchant_id="9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13",
        order_id="my-order-1",
        expand="refunds",
    )
)

# Partial refund (omit items for a full refund)
refund = client.request.MerchantCreateRefund(
    MerchantCreateRefundRequest(
        merchant_id="9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13",
        order_id="my-order-1",
        items=[MerchantRefundItem(item_id="sku-1", quantity=1.0)],
    )
)

Exemption Certificates

from ziptax.models import (
    CreateExemptionCertificateRequest,
    ExemptionCertificateBusinessType,
    ExemptionCertificateReason,
    ExemptState,
    ListExemptionCertificatesRequest,
    MerchantAddress,
)

cert = client.request.CreateExemptionCertificate(
    CreateExemptionCertificateRequest(
        merchant_id="9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13",
        customer_id="customer-453",
        customer_name="Acme Resale LLC",
        customer_business_type=ExemptionCertificateBusinessType.RETAIL_TRADE,
        reason=ExemptionCertificateReason.RESALE,
        reason_description="Resale",
        address=MerchantAddress(
            line1="350 5th Ave", city="New York", state="NY", zip="10118"
        ),
        states=[ExemptState(abbreviation="NY")],
    )
)

# Reference cert.certificate_id as exemption_id on later carts and orders.

# List with pagination
page = client.request.ListExemptionCertificates(
    ListExemptionCertificatesRequest(
        merchant_id="9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13", limit=20
    )
)
while page.next_cursor:
    page = client.request.ListExemptionCertificates(
        ListExemptionCertificatesRequest(
            merchant_id="9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13",
            limit=20,
            cursor=page.next_cursor,
        )
    )

Migrating to the Merchant Layer

The direct-to-TaxCloud functions still work but emit a DeprecationWarning as of 0.3.0. The merchant equivalents take a merchant_id instead of client-level TaxCloud credentials, and use structured addresses.

Deprecated Replacement
CreateOrder(request) MerchantCreateOrder(request)
CreateOrderFromCart(request) MerchantCreateOrderFromCart(request)
GetOrder(order_id) MerchantGetOrder(request)
UpdateOrder(order_id, request) MerchantUpdateOrder(request)
RefundOrder(order_id, request) MerchantCreateRefund(request)

To migrate:

  1. Call CreateMerchant once per merchant, then SetMerchantCredentials with the Connection ID and TaxCloud API key you currently pass to ZipTaxClient.api_key(...). Store the returned merchant_id.
  2. Drop taxcloud_connection_id and taxcloud_api_key from client init.
  3. Swap each call for its replacement above, passing merchant_id.

CalculateCart is not deprecated. It uses the account-level POST /calculate/cart endpoint, which still runs but is no longer part of the published v6.0 API surface. For platform integrations serving multiple merchants, prefer MerchantCalculateCart.

TaxCloud Order Management

Deprecated as of 0.3.0. These functions call the TaxCloud API directly, which is no longer the documented integration path. See Migrating to the merchant layer.

The SDK includes optional support for TaxCloud order management features. To use these features, you need both a ZipTax API key and TaxCloud credentials (Connection ID and API Key).

Initialize Client with TaxCloud Support

from ziptax import ZipTaxClient

# Initialize with TaxCloud credentials
client = ZipTaxClient.api_key(
    api_key="your-ziptax-api-key",
    taxcloud_connection_id="25eb9b97-5acb-492d-b720-c03e79cf715a",
    taxcloud_api_key="your-taxcloud-api-key",
)

# TaxCloud features are now available via client.request

Create an Order

from ziptax.models import (
    CreateOrderRequest,
    TaxCloudAddress,
    CartItemWithTax,
    Tax,
    Currency,
)

# Prepare order request
order_request = CreateOrderRequest(
    order_id="my-order-1",
    customer_id="customer-453",
    transaction_date="2024-01-15T09:30:00Z",
    completed_date="2024-01-15T09:30:00Z",
    origin=TaxCloudAddress(
        line1="323 Washington Ave N",
        city="Minneapolis",
        state="MN",
        zip="55401-2427",
    ),
    destination=TaxCloudAddress(
        line1="323 Washington Ave N",
        city="Minneapolis",
        state="MN",
        zip="55401-2427",
    ),
    line_items=[
        CartItemWithTax(
            index=0,
            item_id="item-1",
            price=10.8,
            quantity=1.5,
            tax=Tax(amount=1.31, rate=0.0813),
        )
    ],
    currency=Currency(currency_code="USD"),
)

# Create the order
order = client.request.CreateOrder(order_request)
print(f"Created order: {order.order_id}")
print(f"Tax amount: ${order.line_items[0].tax.amount}")

Retrieve an Order

# Get an existing order by ID
order = client.request.GetOrder("my-order-1")

print(f"Order ID: {order.order_id}")
print(f"Customer ID: {order.customer_id}")
print(f"Completed Date: {order.completed_date}")
print(f"Total Tax: ${sum(item.tax.amount for item in order.line_items)}")

Update an Order

from ziptax.models import UpdateOrderRequest

# Update the order's completed date
update_request = UpdateOrderRequest(
    completed_date="2024-01-16T10:00:00Z"
)

updated_order = client.request.UpdateOrder("my-order-1", update_request)
print(f"Updated completed date: {updated_order.completed_date}")

Create a Refund

from ziptax.models import (
    RefundTransactionRequest,
    CartItemRefundWithTaxRequest,
)

# Partial refund - specify items and quantities
refund_request = RefundTransactionRequest(
    items=[
        CartItemRefundWithTaxRequest(
            item_id="item-1",
            quantity=1.0,
        )
    ]
)
refunds = client.request.RefundOrder("my-order-1", refund_request)
print(f"Refunded tax: ${refunds[0].items[0].tax.amount}")

# Full refund - omit items parameter
full_refunds = client.request.RefundOrder("my-order-2")
print("Full refund created")

Create an Order from Cart

If you've already calculated cart tax via CalculateCart with TaxCloud credentials, you can convert that cart directly into a finalized order using the returned cart_id:

from ziptax.models import CreateOrderFromCartRequest, UpdateOrderRequest

# Use the cart_id from a previous CalculateCart response
request = CreateOrderFromCartRequest(
    cart_id="ce4a1234-5678-90ab-cdef-1234567890ab",
    order_id="my-order-1",
)

order = client.request.CreateOrderFromCart(request)
print(f"Created order: {order.order_id}")
print(f"Transaction date: {order.transaction_date}")
print(f"Tax amount: ${order.line_items[0].tax.amount}")

# TaxCloud automatically commits the order at creation time.
# To set a completed date, use UpdateOrder after creation:
update_request = UpdateOrderRequest(completed_date="2024-01-16T10:00:00Z")
updated = client.request.UpdateOrder(order.order_id, update_request)

TaxCloud Error Handling

from ziptax import ZipTaxCloudConfigError

try:
    # Attempt to use TaxCloud feature without credentials
    order = client.request.GetOrder("my-order-1")

except ZipTaxCloudConfigError as e:
    # TaxCloud credentials not configured
    print(f"TaxCloud error: {e.message}")
    print("Please provide taxcloud_connection_id and taxcloud_api_key")

except ZipTaxNotFoundError as e:
    # Order not found
    print(f"Order not found: {e.message}")

Configuration

You can configure the client using dict-style access:

client = ZipTaxClient.api_key("your-api-key-here")

# Set configuration options
client.config["format"] = "json"
client.config["timeout"] = 60

# Get configuration options
timeout = client.config["timeout"]

Error Handling

The SDK provides comprehensive error handling with specific exception types:

from ziptax import (
    ZipTaxClient,
    ZipTaxValidationError,
    ZipTaxAuthenticationError,
    ZipTaxRateLimitError,
    ZipTaxServerError,
    ZipTaxError,
)

client = ZipTaxClient.api_key("your-api-key-here")

try:
    response = client.request.GetSalesTaxByAddress("123 Main St")

except ZipTaxValidationError as e:
    # Input validation errors
    print(f"Validation error: {e.message}")

except ZipTaxAuthenticationError as e:
    # Authentication failures (401)
    print(f"Authentication error: {e.message}")

except ZipTaxRateLimitError as e:
    # Rate limit exceeded (429)
    print(f"Rate limit error: {e.message}")
    if e.retry_after:
        print(f"Retry after {e.retry_after} seconds")

except ZipTaxServerError as e:
    # Server errors (5xx)
    print(f"Server error: {e.message}")

except ZipTaxError as e:
    # General Ziptax errors
    print(f"Ziptax error: {e.message}")

Exception Hierarchy

ZipTaxError
├── ZipTaxAPIError
│   ├── ZipTaxAuthenticationError (401)
│   ├── ZipTaxAuthorizationError (403)
│   ├── ZipTaxNotFoundError (404)
│   ├── ZipTaxRateLimitError (429)
│   └── ZipTaxServerError (5xx)
├── ZipTaxValidationError
├── ZipTaxConnectionError
├── ZipTaxTimeoutError
├── ZipTaxRetryError
└── ZipTaxCloudConfigError (TaxCloud credentials not configured)

Async Operations

For concurrent operations, you can use asyncio with the SDK:

import asyncio
from concurrent.futures import ThreadPoolExecutor
from ziptax import ZipTaxClient

async def get_tax_rates_async(client, addresses):
    loop = asyncio.get_event_loop()
    with ThreadPoolExecutor() as executor:
        tasks = [
            loop.run_in_executor(
                executor,
                client.request.GetSalesTaxByAddress,
                address
            )
            for address in addresses
        ]
        return await asyncio.gather(*tasks)

# Usage
client = ZipTaxClient.api_key("your-api-key-here")
addresses = ["123 Main St, CA", "456 Oak Ave, NY"]
responses = asyncio.run(get_tax_rates_async(client, addresses))

See examples/async_usage.py for more examples.

Response Models

All API responses are validated using Pydantic models:

V60Response

class V60Response:
    metadata: V60Metadata                           # Response metadata with code/message
    base_rates: Optional[List[V60BaseRate]]        # Tax rates by jurisdiction
    service: Optional[V60Service]                   # Service taxability (None for some regions)
    shipping: Optional[V60Shipping]                 # Shipping taxability (None for some regions)
    sourcing_rules: Optional[V60SourcingRules]     # Origin/Destination rules
    tax_summaries: Optional[List[V60TaxSummary]]   # Tax summaries with display rates
    address_detail: V60AddressDetail                # Address details
    product_detail: Optional[V60ProductDetail]     # Product rate rules (with taxability_code)

address_detail.address (a V60AddressComponents) is populated only when the request set address_detail_extended=True. shipping.shipping_extended (a V60ShippingExtended) is populated only when the request set shipping_extended=True.

V60Metadata

class V60Metadata:
    version: str                    # API version (e.g., "v60")
    response: V60ResponseInfo       # Response info object

class V60ResponseInfo:
    code: int                       # Response code (100 = success)
    name: str                       # Response code name
    message: str                    # Response message
    definition: str                 # Schema definition URL

V60TaxSummary

class V60TaxSummary:
    rate: float                                    # Summary tax rate
    tax_type: str                                  # Tax type (e.g., "SALES_TAX")
    summary_name: str                              # Summary description
    display_rates: List[V60DisplayRate]           # Display rates breakdown

class V60DisplayRate:
    name: str                                      # Display rate name
    rate: float                                    # Display rate value

V60AccountMetrics

class V60AccountMetrics:
    request_count: int       # Number of API requests made
    request_limit: int       # Maximum allowed API requests
    usage_percent: float     # Percentage of request limit used
    is_active: bool          # Whether the account is currently active
    message: str             # Account status or informational message

Note: Uses extra="allow" to accept any additional fields the API may return.

AccountMetrics

Returned by GetAccountUsage().

class AccountMetrics:
    core_request_count: int          # Core (tax lookup) requests consumed
    core_request_limit: int          # Core request limit
    core_usage_percent: float        # Core usage as a percentage of the limit
    geo_enabled: bool                # Whether geocoding is entitled
    geo_request_count: int           # Geocoding requests consumed
    geo_request_limit: int           # Geocoding request limit
    geo_usage_percent: float         # Geocoding usage as a percentage
    merchant_request_count: int      # Merchant requests consumed
    merchant_request_limit: int      # Merchant request limit
    merchant_usage_percent: float    # Merchant usage as a percentage
    is_active: bool                  # Whether the account is active
    message: str                     # Informational message

ProductCodeSearchResponse

class ProductCodeSearchResponse:
    query: str                                          # The original search query
    results: List[ProductCodeSearchResult]              # Ranked results
    next_cursor: Optional[str]                          # Cursor for the next page
    schema_url: Optional[str]                           # JSON Schema URL ($schema)

class ProductCodeSearchResult:
    tic_id: int             # Taxability Information Code (parsed from string)
    label: str              # TIC label
    natural_label: str      # Natural language label
    description: str        # Full TIC description
    documentation: str      # Long-form TIC documentation
    rank: int               # Result rank (1 = best match, parsed from string)
    score: float            # Confidence score 0.0-1.0 (parsed from string)

ProductCodeRecommendationResponse

class ProductCodeRecommendationResponse:
    predictions: List[ProductCodeRecommendation]        # AI recommendations

class ProductCodeRecommendation:
    status: str                              # "success" or "fail"
    error: Optional[str]                     # Error message when status is "fail"
    tic_id: Optional[int]                    # Recommended TIC
    label: Optional[str]                     # TIC label
    natural_label: Optional[str]             # Natural language label
    tic_description: Optional[str]           # Full TIC description
    product_description: Optional[str]       # Original product description from query

Note: Only status is guaranteed. When status is "fail" the API returns error populated and every other field null, so always branch on status before reading tic_id.

See the models documentation for complete model definitions.

Development

Setup

# Clone the repository
git clone https://github.com/ziptax/ziptax-python.git
cd ziptax-python

# Install dependencies
pip install -e ".[dev]"

Running Tests

# Run all tests
pytest

# Run with coverage
pytest --cov=src/ziptax --cov-report=html

# Run specific test file
pytest tests/test_client.py

Code Quality

# Format code
black src/ tests/

# Lint code
ruff src/ tests/

# Type checking
mypy src/

Examples

See the examples/ directory for complete examples:

API Reference

ZipTaxClient

Main client for interacting with the Ziptax API.

Methods

  • api_key(api_key, **kwargs) - Create a client instance with an API key
  • close() - Close the HTTP client session

Properties

  • config - Configuration object (dict-like access)
  • request - Functions object for making API requests

Functions

API endpoint functions accessible via client.request.

ZipTax API Methods

  • GetSalesTaxByAddress(address, **kwargs) - Get tax rates by address
  • GetSalesTaxByGeoLocation(lat, lng, **kwargs) - Get tax rates by coordinates
  • GetRatesByPostalCode(postal_code, **kwargs) - Get tax rates by US postal code
  • GetAccountMetrics(**kwargs) - Get account metrics in the simplified v6.0 format
  • GetAccountUsage(**kwargs) - Get usage across the core, geo, and merchant pools
  • SearchProductCodes(query) - Search for product codes (TICs) by description
  • RecommendProductCode(query) - Get an AI-powered TIC recommendation
  • GetTicData() - Retrieve the full TIC list, including the category hierarchy
  • GetTicSearchSchema() - Retrieve the JSON Schema for the TIC search response
  • CalculateCart(request) - Calculate sales tax for a shopping cart
  • GetHealth() - Check API availability and component health
  • GetSystemMetadata() - Retrieve build and host information

Merchant Methods

All merchant endpoints authenticate with your Ziptax API key and take a merchant_id. No TaxCloud credentials are needed on the client.

Management (Pro and Enterprise plans)

  • CreateMerchant(request) - Create a merchant
  • UpdateMerchant(request) - Update a merchant
  • DeleteMerchant(merchant_id) - Soft-delete a merchant
  • GetMerchant(merchant_id) - Retrieve a merchant
  • ListMerchants() - List every active merchant on the account
  • SetMerchantCredentials(request) - Store a merchant's TaxCloud credentials
  • DeleteMerchantCredentials(merchant_id) - Remove stored credentials

Transactions

  • MerchantCalculateCart(request) - Calculate cart tax (both compliance models)
  • MerchantCreateOrder(request) - Record an order (Enterprise)
  • MerchantCreateOrderFromCart(request) - Capture a calculated cart as an order (Enterprise)
  • MerchantGetOrder(request) - Retrieve an order (Enterprise)
  • MerchantUpdateOrder(request) - Update an order's completed date (Enterprise)
  • MerchantCreateRefund(request) - Full or partial refund (Enterprise)

Exemption certificates (Enterprise)

  • CreateExemptionCertificate(request) - Create a certificate
  • GetExemptionCertificate(request) - Retrieve a certificate
  • ListExemptionCertificates(request) - List certificates (paginated)
  • DeleteExemptionCertificate(request) - Delete a certificate

MerchantCalculateCart is the only transaction endpoint available to self-managed merchants. The order, refund, and certificate endpoints return 403 for them.

TaxCloud API Methods (Optional, Deprecated)

Requires taxcloud_connection_id and taxcloud_api_key in client initialization. Each of these emits a DeprecationWarning; see Migrating to the merchant layer.

  • CreateOrder(request, **kwargs) - Create an order in TaxCloud
  • CreateOrderFromCart(request) - Create an order from a previously calculated cart
  • GetOrder(order_id) - Retrieve an order by ID
  • UpdateOrder(order_id, request) - Update an order's completed date
  • RefundOrder(order_id, request) - Create a full or partial refund

Requirements

  • Python 3.8+
  • requests >= 2.28.0
  • pydantic >= 2.0.0

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes and write tests
  4. Bump the version using python scripts/bump_version.py patch (or minor/major)
  5. Update CHANGELOG.md with your changes
  6. Commit your changes (git commit -m 'Add some amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

Note: All PRs require a version bump. See docs/VERSIONING.md for details on our versioning strategy.

Changelog

See CHANGELOG.md for version history and changes.


Made with ❤️ by the Ziptax Team

About

Ziptax Python SDK

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages