Skip to content

v0.3.0-beta: merchant layer, extended v6.0 rate options, TIC data endpoints - #21

Merged
ericlakich merged 5 commits into
mainfrom
claude/python-sdk-api-updates-59f2c0
Aug 7, 2026
Merged

v0.3.0-beta: merchant layer, extended v6.0 rate options, TIC data endpoints#21
ericlakich merged 5 commits into
mainfrom
claude/python-sdk-api-updates-59f2c0

Conversation

@ericlakich

@ericlakich ericlakich commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Brings the SDK up to the v6.0 API surface documented at https://docs.zip.tax, validated against the handlers in ZipTax/ziptax-api.

What changed

The API grew a merchant layer: 19 endpoints on api.zip-tax.com that let a platform manage merchants and their transactions with a single Ziptax API key. Merchants are addressed by merchantId; per-merchant TaxCloud credentials are stored server-side rather than configured on the client.

Ziptax routes each call on the merchant's compliance model:

Self-managed TaxCloud-connected
Calculation In-process Ziptax rate engine Forwarded to TaxCloud
Persisted No Yes
Discounts Not supported Supported
Endpoints /merchant/cart/calculate only All
Plan Pro and Enterprise Enterprise

MerchantCalculateCartResponse marks connection_id, transaction_date, delivered_by_seller, and exemption optional so one model covers both response shapes.

Added

Merchant managementCreateMerchant, UpdateMerchant, DeleteMerchant, GetMerchant, ListMerchants, SetMerchantCredentials, DeleteMerchantCredentials

Merchant transactionsMerchantCalculateCart, MerchantCreateOrder, MerchantCreateOrderFromCart, MerchantGetOrder, MerchantUpdateOrder, MerchantCreateRefund

Exemption certificatesCreateExemptionCertificate, GetExemptionCertificate, ListExemptionCertificates, DeleteExemptionCertificate

TIC data and systemGetTicData, GetTicSearchSchema, GetAccountUsage, GetHealth, GetSystemMetadata

New v6.0 rate-lookup parametersadjustment, address_detail_extended, shipping_extended, sat_item_total, plus city/state on address lookups and taxability_code on geolocation lookups. Postal code lookups gain state, city, county, historical, sat_item_total. country_code now accepts the US territories (PRI, ASM, GUM, MNP, VIR).

New response modelsV60ProductDetail, V60TaxabilityCode, V60RateRule, V60AddressComponents, V60ShippingExtended, V60SingleArticleTax, plus the merchant/certificate/TIC/system set in the new src/ziptax/models/merchant.py. ProductCodeSearchResponse gains next_cursor and schema_url.

Fixed

RecommendProductCode no longer raises on a failed prediction. The API returns status: "fail" with error populated and every other field null, which previously raised a ValidationError. All ProductCodeRecommendation fields except status are now Optional. Callers should branch on status before reading tic_id.

Deprecated

The five direct-to-TaxCloud functions emit a DeprecationWarning. They call api.v3.taxcloud.com directly, which is no longer the documented path. Behaviour is unchanged and they continue to work.

Deprecated Replacement
CreateOrder MerchantCreateOrder
CreateOrderFromCart MerchantCreateOrderFromCart
GetOrder MerchantGetOrder
UpdateOrder MerchantUpdateOrder
RefundOrder MerchantCreateRefund

A migration guide is in the README.

Deliberately not exposed

Present in the API source but absent from the documentation and OpenAPI spec, so excluded per the documented-surface-only rule:

  • POST /merchant/credentials/get — returns stored TaxCloud credentials
  • GET /request/v10/v50 and their /account/vN0/metrics siblings
  • /request/v60/schema, /account/metadata, /metadata/response.json, /request/error
  • The tracerate=true query parameter (the API source labels it an undocumented diagnostic)

Notes for review

  • CalculateCart is unchanged and not deprecated. It uses POST /calculate/cart, which still runs but has been dropped from the published v6.0 surface. Kept for backward compatibility; the README points platform integrations at MerchantCalculateCart.
  • Credentials never travel in transaction bodies. The proxy 400s on bodies containing apiKey, connectionId, or xApiKey. A test asserts the cart request body carries none of them.
  • GetAccountUsage is a new name, not a rename. GET /account/metrics and GET /account/v60/metrics return different shapes, so GetAccountMetrics keeps its existing v6.0 behaviour and the per-pool endpoint got a distinct name.
  • Postal code lookups still return V60PostalCodeResponse. Confirmed in controllers/api/v60/handler_legacy.go: the v6.0 postal-code-only path serves the legacy v5.0 shape.
  • One upstream discrepancy. The hand-written OpenAPI stub for /search/tic describes results as {tic, name, label}, but the endpoint is a verbatim passthrough to TaxCloud and both the guide and the live response use {ticId, label, naturalLabel, description, documentation, rank, score}. The SDK follows the guide, so ProductCodeSearchResult is unchanged. Worth flagging to whoever owns internal/middleware/huma_schemas.go.
  • next_cursor is returned but not consumable. /search/tic echoes a pagination cursor, but the Go handler marshals only {query} upstream, so a cursor sent by a client would be dropped. Exposed on the response for visibility; no cursor parameter added.

Verification

  • 249 tests pass (90 new: 57 in tests/test_merchant.py, 33 in tests/test_functions.py)
  • 98% coverage overall, 100% on resources/functions.py and both model modules
  • black, ruff, and mypy clean on src/ and tests/
  • All changes additive: no existing function changed signature, endpoint, or return type

Version bumped 0.2.6-beta0.3.0-beta. docs/spec.yaml, CLAUDE.md, README.md, and CHANGELOG.md updated; new examples/merchant_compliance.py.

Not exercised against the live API — no credentials were used. Everything is verified against the OpenAPI spec, the published docs, and the Go handlers.

🤖 Generated with Claude Code


Open in Devin Review

Brings the SDK up to the v6.0 API surface documented at https://docs.zip.tax.

The headline addition is the merchant layer: 19 endpoints on api.zip-tax.com
that let a platform manage merchants and their transactions with a single
Ziptax API key, replacing per-merchant TaxCloud credentials on the client.

Added:
- Merchant management: CreateMerchant, UpdateMerchant, DeleteMerchant,
  GetMerchant, ListMerchants, SetMerchantCredentials,
  DeleteMerchantCredentials
- Merchant transactions: MerchantCalculateCart, MerchantCreateOrder,
  MerchantCreateOrderFromCart, MerchantGetOrder, MerchantUpdateOrder,
  MerchantCreateRefund
- Exemption certificates: Create, Get, List, Delete
- TIC data and system: GetTicData, GetTicSearchSchema, GetAccountUsage,
  GetHealth, GetSystemMetadata
- v6.0 rate-lookup parameters: adjustment, address_detail_extended,
  shipping_extended, sat_item_total, plus city/state on address lookups and
  taxability_code on geolocation lookups
- Postal code parameters: state, city, county, historical, sat_item_total
- US territory country codes (PRI, ASM, GUM, MNP, VIR)
- Response models for productDetail, address components, extended shipping,
  Tennessee SAT, and TIC search pagination

Fixed:
- RecommendProductCode no longer raises on a failed prediction. The API
  returns status="fail" with every other field null; all
  ProductCodeRecommendation fields except status are now Optional.

Deprecated:
- CreateOrder, GetOrder, UpdateOrder, RefundOrder, and CreateOrderFromCart
  emit a DeprecationWarning. They call TaxCloud directly, which is no longer
  the documented path. Behaviour is unchanged.

Deliberately not exposed: POST /merchant/credentials/get returns stored
TaxCloud credentials and is absent from the docs and OpenAPI spec.

All changes are additive; no existing function changed its signature,
endpoint, or return type. Suite is 249 tests (90 new) at 98% coverage;
black, ruff, and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 7, 2026 16:45

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 6 potential issues.

Open in Devin Review

Comment thread src/ziptax/models/merchant.py
Comment thread src/ziptax/resources/functions.py
Comment thread src/ziptax/models/merchant.py
Comment thread src/ziptax/resources/functions.py
Comment thread src/ziptax/models/responses.py
Comment thread README.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR bumps the ZipTax Python SDK to 0.3.0-beta and expands it to match the documented v6.0 surface (https://docs.zip.tax), primarily by adding the merchant-layer API (merchant management + merchant-scoped transactions), additional v6.0 rate-lookup options, and new TIC/system endpoints and models.

Changes:

  • Added merchant-layer endpoints (management, carts/orders/refunds, exemption certificates) with new Pydantic models and a full test suite for both compliance models.
  • Extended v6.0 rate lookup/query support (adjustment, extended address/shipping detail flags, SAT item total, additional narrowing params) and added response models for the new enriched fields.
  • Deprecated direct-to-TaxCloud order functions via DeprecationWarning while preserving behavior; improved TIC search/recommendation response handling (pagination fields; recommendation failure parsing).

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_merchant.py Comprehensive tests for merchant management/transactions, exemption certificates, TIC data, and system endpoints
tests/test_functions.py Adds deprecation-warning tests for direct TaxCloud calls and tests for new v6.0 parameters/response models
src/ziptax/utils/validation.py Extends country-code validation and adds validators for adjustment and merchant_id
src/ziptax/resources/functions.py Implements merchant-layer endpoint methods; adds v6.0 query params; emits deprecation warnings for direct TaxCloud calls
src/ziptax/models/responses.py Adds v6.0 response models for shipping/address/product rules and SAT breakdown; enhances TIC search and recommendation models
src/ziptax/models/merchant.py New merchant/certificate/TIC/system model module supporting the merchant layer
src/ziptax/models/init.py Exports new merchant and v6.0 models from the models package
src/ziptax/init.py Re-exports new public models and bumps __version__ to 0.3.0-beta
README.md Documents merchant layer, extended v6.0 options, new endpoints, and migration guidance for deprecations
pyproject.toml Version bump to 0.3.0-beta
examples/taxcloud_orders.py Marks TaxCloud direct example as deprecated and points to merchant-layer example
examples/merchant_compliance.py New end-to-end merchant-layer walkthrough example
docs/spec.yaml Updates SDK spec metadata and documents merchant layer + excluded endpoints policy
CLAUDE.md Updates repository guidance to reflect merchant layer, endpoints, and test layout
CHANGELOG.md Adds detailed 0.3.0-beta release notes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ziptax/models/merchant.py
Comment thread src/ziptax/utils/validation.py
- AccountMetrics docstring named the wrong functions. It is returned by
  GetAccountUsage (GET /account/metrics), not GetAccountMetrics, and the
  referenced GetAccountMetricsV60 does not exist.
- README extended-options example dereferenced response.shipping without
  guarding it; the field is Optional and is None for some regions.
- validate_merchant_id docstring claimed UUID validation but only checks for
  a non-empty string. Reworded to state that intentionally: the server is the
  authority on valid IDs and returns 403/404, so strict client-side format
  checking risks rejecting IDs the API would accept.
- Documented the ProductCodeSearchResponse.results relaxation in the
  CHANGELOG. It defaults to [] because the published spec lists only $schema
  and query as required.

Also added comments at the two places where the API is genuinely snake_case
among camelCase siblings, both verified against the OpenAPI spec and the API
source, so they are not "corrected" later:
- merchant_type on POST /merchant/create
- sat_item_total on GET /request/v60

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 7, 2026 16:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/ziptax/models/responses.py:208

  • V60AddressComponents.house_number is described as "when available" but is currently required (...). If the geocoder doesn't return houseNumber (common for some partial/PO Box inputs), response parsing will raise. Consider making it optional to match the description and avoid failing the whole response.
    house_number: str = Field(
        ..., alias="houseNumber", description="House/street number, when available"
    )

src/ziptax/models/responses.py:198

  • V60AddressComponents.county is documented as "when available" but is modeled as a required field (...). If the API omits county for some geocoded addresses, this will raise a ValidationError and break parsing for address_detail_extended=True responses. Making it optional aligns the model with the field description and makes the client more robust to partial geocoder output.

This issue also appears on line 206 of the same file.

    county: str = Field(..., description="County name, when available")

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 17:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/ziptax/utils/validation.py:104

  • The validate_merchant_id docstring says UUID format is deliberately not enforced, but the implementation does enforce UUID parsing (uuid.UUID(...)). This is misleading for users and future maintainers; either remove the UUID enforcement or update the docstring to match the current behavior.
    The API issues merchant IDs as UUIDs, but this deliberately does not
    enforce UUID format: the server is the authority on which IDs are valid
    and returns 403/404 for unknown or unowned merchants. Validating only
    the obvious client-side mistake avoids rejecting an ID the API would
    have accepted.

ericlakich and others added 2 commits August 7, 2026 10:30
Re-verified POST /merchant/create against both sources:

- https://docs.zip.tax/openapi/api-reference.json lists the body property as
  "merchant_type". "merchantType" does not appear in the schema.
- CreateMerchantRequest in the API source tags the field
  `json:"merchant_type"` (controllers/merchant/models.go:25). The only
  "merchantType" occurrences in the API are local Go identifiers, not JSON
  keys.

So the SDK's snake_case is correct and no alias should be added. The
inconsistency is real but it is the API's, not the SDK's.

Getting this wrong fails silently rather than loudly, which is why it is
worth pinning: an unrecognized key would leave the field empty, and
normalizeMerchantType("") returns MerchantTypeTaxCloud with ok=true
(controllers/merchant/handler.go:214-223). A caller asking for a
self-managed merchant would get a TaxCloud merchant and an invite flow with
no error surfaced anywhere.

The test already asserted the snake_case key is present; it now also asserts
"merchantType" is absent, and the docstring records why. Verified the guard
fires: adding alias="merchantType" makes the test fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to 26b229d, which added UUID validation to validate_merchant_id.
Keeping that behaviour; fixing three things it left behind:

- The raise was 92 characters, so ruff E501 and black would both have failed
  the lint job. Wrapped.
- `import uuid` was inside the function body. Moved to the module imports
  alongside `re`, matching the rest of the file.
- The docstring body still said the function "deliberately does not enforce
  UUID format", contradicting the new code directly below it. Rewritten to
  describe what it now does, and to keep the ownership boundary clear: a
  well-formed UUID the account does not own is still a server-side 403.

Also narrowed `except (ValueError, TypeError)` to `ValueError`. The isinstance
guard above already rejects non-strings, so TypeError was unreachable.

Added six tests, which the autofix shipped without: malformed UUIDs are
rejected before any HTTP call, and the two forms uuid.UUID accepts
(surrounding whitespace, unhyphenated) are confirmed to pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 7, 2026 17:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

Suppressed comments (5)

src/ziptax/resources/functions.py:1186

  • GetMerchant() validates merchant_id using .strip(), but then sends the unstripped value in the request body. If callers pass a UUID with surrounding whitespace (which the validator explicitly allows), the request can still fail. Strip before sending.
        validate_merchant_id(merchant_id)

        response_data = self._merchant_post(
            "/merchant/get", {"merchantId": merchant_id}
        )

src/ziptax/resources/functions.py:1266

  • DeleteMerchantCredentials() validates merchant_id but sends the original value (potentially whitespace-padded) in the request body. Strip it before POSTing so padded UUIDs don’t unexpectedly fail at the API boundary.
        validate_merchant_id(merchant_id)

        response_data = self._merchant_post(
            "/merchant/credentials/delete", {"merchantId": merchant_id}
        )

src/ziptax/models/responses.py:208

  • V60AddressComponents.house_number is described as "when available" but is currently required. If geocoding can’t resolve a house number, this will cause response parsing to fail. Make it optional (default None) to match the description and avoid unnecessary ValidationErrors.
    house_number: str = Field(
        ..., alias="houseNumber", description="House/street number, when available"
    )

src/ziptax/resources/functions.py:1158

  • validate_merchant_id() accepts whitespace-padded UUIDs, but DeleteMerchant() sends the original merchant_id (including surrounding whitespace) in the request body. That can cause the server-side UUID parse / ownership check to fail even though client-side validation passed. Strip the value before building the JSON body so validation and request behavior match.

This issue also appears in the following locations of the same file:

  • line 1182
  • line 1262
        validate_merchant_id(merchant_id)

        response_data = self._merchant_post(
            "/merchant/delete", {"merchantId": merchant_id}
        )

src/ziptax/models/responses.py:198

  • V60AddressComponents.county is documented as "when available" but is currently required. If the API omits county for some geocodes, parsing will raise a ValidationError even when address_detail_extended=True is used correctly. Make it optional with a None default to align with the field description.

This issue also appears on line 206 of the same file.

    county: str = Field(..., description="County name, when available")

@ericlakich
ericlakich merged commit ae362c1 into main Aug 7, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants