v0.3.0-beta: merchant layer, extended v6.0 rate options, TIC data endpoints - #21
Conversation
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>
There was a problem hiding this comment.
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
DeprecationWarningwhile 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.
- 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>
There was a problem hiding this comment.
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_numberis described as "when available" but is currently required (...). If the geocoder doesn't returnhouseNumber(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.countyis documented as "when available" but is modeled as a required field (...). If the API omitscountyfor some geocoded addresses, this will raise aValidationErrorand break parsing foraddress_detail_extended=Trueresponses. 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>
There was a problem hiding this comment.
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.
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>
There was a problem hiding this comment.
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()validatesmerchant_idusing.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()validatesmerchant_idbut 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_numberis 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 (defaultNone) 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, butDeleteMerchant()sends the originalmerchant_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.countyis documented as "when available" but is currently required. If the API omitscountyfor some geocodes, parsing will raise a ValidationError even whenaddress_detail_extended=Trueis used correctly. Make it optional with aNonedefault 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")
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.comthat let a platform manage merchants and their transactions with a single Ziptax API key. Merchants are addressed bymerchantId; per-merchant TaxCloud credentials are stored server-side rather than configured on the client.Ziptax routes each call on the merchant's compliance model:
/merchant/cart/calculateonlyMerchantCalculateCartResponsemarksconnection_id,transaction_date,delivered_by_seller, andexemptionoptional so one model covers both response shapes.Added
Merchant management —
CreateMerchant,UpdateMerchant,DeleteMerchant,GetMerchant,ListMerchants,SetMerchantCredentials,DeleteMerchantCredentialsMerchant transactions —
MerchantCalculateCart,MerchantCreateOrder,MerchantCreateOrderFromCart,MerchantGetOrder,MerchantUpdateOrder,MerchantCreateRefundExemption certificates —
CreateExemptionCertificate,GetExemptionCertificate,ListExemptionCertificates,DeleteExemptionCertificateTIC data and system —
GetTicData,GetTicSearchSchema,GetAccountUsage,GetHealth,GetSystemMetadataNew v6.0 rate-lookup parameters —
adjustment,address_detail_extended,shipping_extended,sat_item_total, pluscity/stateon address lookups andtaxability_codeon geolocation lookups. Postal code lookups gainstate,city,county,historical,sat_item_total.country_codenow accepts the US territories (PRI,ASM,GUM,MNP,VIR).New response models —
V60ProductDetail,V60TaxabilityCode,V60RateRule,V60AddressComponents,V60ShippingExtended,V60SingleArticleTax, plus the merchant/certificate/TIC/system set in the newsrc/ziptax/models/merchant.py.ProductCodeSearchResponsegainsnext_cursorandschema_url.Fixed
RecommendProductCodeno longer raises on a failed prediction. The API returnsstatus: "fail"witherrorpopulated and every other field null, which previously raised aValidationError. AllProductCodeRecommendationfields exceptstatusare nowOptional. Callers should branch onstatusbefore readingtic_id.Deprecated
The five direct-to-TaxCloud functions emit a
DeprecationWarning. They callapi.v3.taxcloud.comdirectly, which is no longer the documented path. Behaviour is unchanged and they continue to work.CreateOrderMerchantCreateOrderCreateOrderFromCartMerchantCreateOrderFromCartGetOrderMerchantGetOrderUpdateOrderMerchantUpdateOrderRefundOrderMerchantCreateRefundA 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 credentialsGET /request/v10–/v50and their/account/vN0/metricssiblings/request/v60/schema,/account/metadata,/metadata/response.json,/request/errortracerate=truequery parameter (the API source labels it an undocumented diagnostic)Notes for review
CalculateCartis unchanged and not deprecated. It usesPOST /calculate/cart, which still runs but has been dropped from the published v6.0 surface. Kept for backward compatibility; the README points platform integrations atMerchantCalculateCart.apiKey,connectionId, orxApiKey. A test asserts the cart request body carries none of them.GetAccountUsageis a new name, not a rename.GET /account/metricsandGET /account/v60/metricsreturn different shapes, soGetAccountMetricskeeps its existing v6.0 behaviour and the per-pool endpoint got a distinct name.V60PostalCodeResponse. Confirmed incontrollers/api/v60/handler_legacy.go: the v6.0 postal-code-only path serves the legacy v5.0 shape./search/ticdescribes 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, soProductCodeSearchResultis unchanged. Worth flagging to whoever ownsinternal/middleware/huma_schemas.go.next_cursoris returned but not consumable./search/ticechoes 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
tests/test_merchant.py, 33 intests/test_functions.py)resources/functions.pyand both model modulesblack,ruff, andmypyclean onsrc/andtests/Version bumped
0.2.6-beta→0.3.0-beta.docs/spec.yaml,CLAUDE.md,README.md, andCHANGELOG.mdupdated; newexamples/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