feat!: realign SDK with current Ziptax API surface (v1.0.0-beta) - #10
Merged
Conversation
TaxCloud access has moved from a direct connection to Ziptax-proxied merchant endpoints, so cart, order, and refund calls now go through api.zip-tax.com keyed by merchantId rather than api.v3.taxcloud.com. Merchant credentials are stored server-side once via /merchant/credentials/set, which retires the taxCloudConnectionId and taxCloudAPIKey client options. Added, all documented at docs.zip.tax: - Merchant management: create/update/get/list/delete plus credentials - Exemption certificates: create/get/list/delete with cursor pagination - Data and system: /data/tic, /schemas/ticsearch, /system/health, /system/metadata - /account/metrics, with core/geo/merchant quotas broken out separately - Webhook signature verification and typed rate.updated payloads - Rate lookup params: city/state/stateCode/county, adjustment, satItemTotal, addressDetailExtended, shippingExtended, and the productDetail response object - X-ENV LIVE/TEST targeting for merchant transactions Fixed: - ProductCodeSearchResult ticId/rank/score were typed string; the API returns numbers - Rate lookups hit /request/v60 without a trailing slash, which was costing a 301 on every call - taxabilityCode now accepts alphanumeric override codes (CIR00001) - 403 raises ZiptaxAPIError rather than ZiptaxAuthenticationError; on merchant routes it signals an unknown merchant or unavailable operation, not a credential problem Merchant Management is Private Preview and Merchant Transactions is in active development, so both are annotated @experimental. Nexus Management and Economic Thresholds are platform-UI only and have no API surface. /merchant/credentials/get and the legacy /calculate/cart are live but absent from the public API Reference, so neither is exposed. Replaces the hand-maintained docs/spec.yaml with a snapshot of the live OpenAPI spec plus docs/api-coverage.md recording exposure decisions. 230 tests passing, 99.68% coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ions Two defects found in review of this PR. 1. Non-idempotent merchant writes were retried automatically. HTTPClient.request applied one retry policy to every request, and the default shouldRetry returns true for network errors and any 5xx. That silently re-sent createOrder, createOrderFromCart, updateOrder, createExemptionCertificate, deleteExemptionCertificate, and refundOrder on a 502, 504, or client-side timeout — precisely the cases where the outcome is unknown and a second attempt can duplicate an order, a certificate, or a refund. The README told users to treat those statuses as "unknown outcome" and confirm before retrying, which was impossible when the SDK had already retried. The retry policy is now chosen per operation: writes never retried (NO_RETRY) calculateCart retried only when no response arrived at all reads unchanged: network errors and 5xx calculateCart is deliberately not retried on 5xx. The service answered, so it may already have calculated and stored a cart, and every call is metered against the merchant allowance. A connection failure or timeout means nothing was received, which is the one case the API docs sanction retrying. HTTPClient's get/post/patch accept a retryOptions override that is merged over the client's own and stripped before the config reaches axios. RequestOptions.retryOptions exposes this per call, so a caller with its own idempotency handling can opt a write back in. 2. Closed unions excluded Canadian and territorial values. countryCode=CAN is served by a separate handler whose response differs: jurType is GST or PST, taxType is Sales, and both service and sourcingRules are absent from the struct entirely. None of that appears in the published OpenAPI enums, which describe only the USA path, so: - V60BaseRate.jurType and V60TaxSummary.taxType are now open unions that still autocomplete the known values - V60Response.service and .sourcingRules are optional - V60Shipping.taxable accepts 'L' alongside V60Service.taxable. Both are assigned verbatim from the same tax-table taxability vocabulary; only the service branch renders distinct text for L, which hid that the value reaches shipping.taxable too. V60PostalCodeResult.txbService and .txbFreight likewise. All four share a new V60Taxability type Adds tests/retry-policy.test.ts, which asserts each write sends exactly one request on 502, 504, and timeout, that reads and the per-call override still retry, and that retryOptions never leaks into the axios config. Adds tests/response-types.test.ts, which fails to compile if the unions are narrowed again or the optional fields are made required. 275 tests passing, 99.69% coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MerchantType was copied from the API's OpenAPI enum, which lists four values. Only `taxcloud` and `self-managed` are documented; the JSDoc above the type, the README table, and docs/api-coverage.md all describe just those two, so `connected` and `offline` were offered by the type and described nowhere. They are legacy aliases. controllers/merchant/handler.go declares them as legacyMerchantTypeTaxCloud and legacyMerchantTypeSelfManaged, and the API normalizes them to `taxcloud` and `self-managed` before use. New callers have no reason to send either. Safe to narrow because merchant_type is request-only: it appears on CreateMerchantRequest and is not returned by /merchant/get or /merchant/list, which report the compliance model through `status` instead. Restricting a request field only limits what the SDK sends. This is the opposite of a response field, where a closed union makes a valid payload unassignable — the note added to docs/api-coverage.md records that distinction so the two cases are not conflated. Adds a @ts-expect-error guard so the build fails if either alias becomes assignable again by copying the OpenAPI enum back in. 278 tests passing, 99.69% coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The API's feature list has moved on substantially since
0.2.3-beta. This brings the SDK back in line with docs.zip.tax, validated against the live OpenAPI spec and the ziptax-api Go source.The headline change is breaking: TaxCloud access has moved from a direct connection to Ziptax-proxied merchant endpoints. Cart, order, and refund calls now go through
api.zip-tax.comkeyed bymerchantIdinstead ofapi.v3.taxcloud.com. Credentials are stored server-side once via/merchant/credentials/set, which retires thetaxCloudConnectionIdandtaxCloudAPIKeyclient options.Version:
0.2.3-beta→1.0.0-beta.What's new
createMerchant,updateMerchant,getMerchant,listMerchants,deleteMerchant,setMerchantCredentials,deleteMerchantCredentialscreateExemptionCertificate,getExemptionCertificate,listExemptionCertificates(cursor pagination),deleteExemptionCertificategetTicData,getTicSearchSchema,getHealth,getSystemMetadatagetAccountUsage—/account/metrics, with core/geo/merchant quotas separatedverifyWebhookSignature,parseWebhookEvent,parseWebhookTimestamp+ typedrate.updatedpayloadsAlso added:
X-ENV: LIVE|TESTenvironment targeting, cart/orderdiscounts,exemption,batchId,channel, credit orders viakind, theproductDetailresponse object,addressDetailExtended/shippingExtended,city/state/stateCode/countydisambiguators,satItemTotal, and US territory country codes.Bugs found and fixed
ProductCodeSearchResult.ticId,.rank,.scorewere typedstring— the API returns numbers. Anyone doingparseFloat(result.score)was working around a type that was simply wrong. Same forProductCodeRecommendation.ticId./request/v60/; only/request/v60exists. Verified live: trailing slash returns301, without returns401.taxabilityCoderejected valid override codes likeCIR00001— validation was numeric-only, but the API accepts^[A-Za-z0-9]{1,10}$.validateProductQuerycapped at 500 chars; the API allows 1024.extractErrorMessageonly looked atmessage/error. It now also readsdetailandtitle, so operation-level validation errors surface properly.Deliberately not exposed
Per the rule that only documented endpoints get SDK methods:
POST /merchant/credentials/getPOST /calculate/cart/merchant/cart/calculate/request/v10–v50,/request/v40/activateGET /account/v50/metricsNexus Management and Economic Thresholds are documented but platform-UI only — no endpoints exist, so there's nothing to wrap.
Review notes
Three things worth a look:
docs/spec.yamlis deleted. It was a 2,865-line hand-maintained codegen template describing the old API, referenced only fromCLAUDE.mdand not consumed by any tooling. Hand-patching it would have reproduced the drift. Replaced withdocs/openapi.json(snapshot of the live spec) plusdocs/api-coverage.mdrecording the endpoint→method map and every exposure decision. Happy to restore it if it's used somewhere I couldn't see.403now raisesZiptaxAPIError, notZiptaxAuthenticationError. On merchant routes 403 means unknown merchant, wrong account, or an operation unavailable to a self-managed merchant — none of which are credential problems. This changes behavior for anyone catching the auth error broadly.Everything preview-stage is annotated
@experimental. Merchant Management is Private Preview and Merchant Transactions is "in active development — may change before general availability", so consumers get a warning in their IDE rather than a silent stability promise.Migration
README.mdhas a Migrating from 0.2.x section with diffs for all seven breaking changes. The short version: drop the TaxCloud client options, addmerchantIdto transaction calls, switch cart addresses from a single string toline1/city/state/zip, add a unique line-itemindex, renametaxabilityCode→tic, and pass order/refund arguments as a single object.parseAddressString()is removed — the endpoints take structured addresses directly, so there's nothing left to parse.Testing
npm run prepublishOnly(build → test → lint → type-check) exits 0. All three build targets emit, examples type-check, and Prettier is clean. Verified the built CJS bundle exposes 28 client methods and 5 webhook helpers.Out of scope
GET /system/metadatareturns the internal RDS hostname to unauthenticated callers (db_host, not part of its declared schema). That's anziptax-apiissue, not an SDK one — flagged separately.🤖 Generated with Claude Code