Skip to content

feat!: realign SDK with current Ziptax API surface (v1.0.0-beta) - #10

Merged
ericlakich merged 4 commits into
mainfrom
claude/node-sdk-api-updates-3b54e1
Aug 7, 2026
Merged

feat!: realign SDK with current Ziptax API surface (v1.0.0-beta)#10
ericlakich merged 4 commits into
mainfrom
claude/node-sdk-api-updates-3b54e1

Conversation

@ericlakich

@ericlakich ericlakich commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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.com keyed by merchantId instead of api.v3.taxcloud.com. Credentials are stored server-side once via /merchant/credentials/set, which retires the taxCloudConnectionId and taxCloudAPIKey client options.

Version: 0.2.3-beta1.0.0-beta.

What's new

Area Methods
Merchant management createMerchant, updateMerchant, getMerchant, listMerchants, deleteMerchant, setMerchantCredentials, deleteMerchantCredentials
Exemption certificates createExemptionCertificate, getExemptionCertificate, listExemptionCertificates (cursor pagination), deleteExemptionCertificate
Data & system getTicData, getTicSearchSchema, getHealth, getSystemMetadata
Account getAccountUsage/account/metrics, with core/geo/merchant quotas separated
Webhooks verifyWebhookSignature, parseWebhookEvent, parseWebhookTimestamp + typed rate.updated payloads

Also added: X-ENV: LIVE|TEST environment targeting, cart/order discounts, exemption, batchId, channel, credit orders via kind, the productDetail response object, addressDetailExtended / shippingExtended, city/state/stateCode/county disambiguators, satItemTotal, and US territory country codes.

Bugs found and fixed

  • ProductCodeSearchResult.ticId, .rank, .score were typed string — the API returns numbers. Anyone doing parseFloat(result.score) was working around a type that was simply wrong. Same for ProductCodeRecommendation.ticId.
  • Every rate lookup was paying a 301. The SDK requested /request/v60/; only /request/v60 exists. Verified live: trailing slash returns 301, without returns 401.
  • taxabilityCode rejected valid override codes like CIR00001 — validation was numeric-only, but the API accepts ^[A-Za-z0-9]{1,10}$.
  • validateProductQuery capped at 500 chars; the API allows 1024.
  • 422 errors collapsed to a generic message because extractErrorMessage only looked at message/error. It now also reads detail and title, so operation-level validation errors surface properly.

Deliberately not exposed

Per the rule that only documented endpoints get SDK methods:

Endpoint Why
POST /merchant/credentials/get Live, absent from the API Reference
POST /calculate/cart Live, absent from the API Reference; superseded by /merchant/cart/calculate
/request/v10v50, /request/v40/activate Superseded / internal
GET /account/v50/metrics Superseded

Nexus 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:

  1. docs/spec.yaml is deleted. It was a 2,865-line hand-maintained codegen template describing the old API, referenced only from CLAUDE.md and not consumed by any tooling. Hand-patching it would have reproduced the drift. Replaced with docs/openapi.json (snapshot of the live spec) plus docs/api-coverage.md recording the endpoint→method map and every exposure decision. Happy to restore it if it's used somewhere I couldn't see.

  2. 403 now raises ZiptaxAPIError, not ZiptaxAuthenticationError. 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.

  3. 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.md has a Migrating from 0.2.x section with diffs for all seven breaking changes. The short version: drop the TaxCloud client options, add merchantId to transaction calls, switch cart addresses from a single string to line1/city/state/zip, add a unique line-item index, rename taxabilityCodetic, 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

Test Suites: 8 passed, 8 total
Tests:       230 passed, 230 total
Coverage:    99.68% statements, 99.05% branches, 100% functions

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/metadata returns the internal RDS hostname to unauthenticated callers (db_host, not part of its declared schema). That's an ziptax-api issue, not an SDK one — flagged separately.

🤖 Generated with Claude Code


Open in Devin Review

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>

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 13 potential issues.

Open in Devin Review

Comment thread src/client.ts
Comment thread src/models/responses.ts Outdated
Comment thread src/models/responses.ts Outdated
Comment thread src/models/responses.ts
Comment thread src/client.ts
Comment thread src/models/merchant.ts Outdated
Comment thread src/client.ts Outdated
Comment thread src/models/responses.ts
Comment thread src/models/responses.ts Outdated
Comment thread src/client.ts
ericlakich and others added 3 commits August 7, 2026 10:29
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>
@ericlakich
ericlakich merged commit c8a1563 into main Aug 7, 2026
13 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.

1 participant