> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.zip.tax/v-6-0/guides/merchant-compliance-solutions/self-managed-cart-calculation/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.zip.tax/_mcp/server. # Self-Managed Cart Calculation `POST /merchant/cart/calculate` serves both compliance models. When the `merchantId` you send belongs to a [self-managed merchant](self-managed-merchants), Ziptax calculates the tax itself with its own rate engine — no TaxCloud connection and no merchant credentials are involved. When it belongs to a [TaxCloud-connected merchant](taxcloud-connected-merchants), the cart is forwarded to TaxCloud instead; that path is documented in [Cart Tax Calculation](cart). You do not choose the engine. Ziptax routes on the merchant's compliance model, so you send the same request body either way and never branch on merchant type when building a request. ## Calculation only Self-managed cart calculation is **stateless**. Nothing is persisted: no cart is stored, no order can be created from the result, and no exemption certificates are held. It answers one question — how much tax is due on these line items, right now — and returns. Every other Merchant Transactions endpoint returns `403` for a self-managed merchant: | Endpoint | Self-managed | | --------------------------------------- | ------------- | | `POST /merchant/cart/calculate` | **Available** | | `POST /merchant/order/create` | `403` | | `POST /merchant/order/create-from-cart` | `403` | | `POST /merchant/order/get` | `403` | | `POST /merchant/order/update` | `403` | | `POST /merchant/cert/create` | `403` | | `POST /merchant/refund/create` | `403` | Not supported on this path in this release: | Not supported | Detail | | ----------------------- | ------------------------------------------------------------------------------------------------------------ | | Discounts | Neither line-item nor order-level `discounts`. | | Exemptions | No `exemption` field and no [exemption certificates](exemption-certificates). | | Cart persistence | The returned `cartId` cannot be turned into an order. See [`cartId` is not durable](#cartid-is-not-durable). | | Orders, refunds, filing | Blocked, per the table above. | | Canadian carts | `countryCode: "CA"` is rejected. US addresses only. | | `deliveredBySeller` | Seller-delivery taxability rules are not applied. | > **Warning** > > Unsupported fields are **rejected with `400`**, not silently ignored. If you > send a 20% order discount and Ziptax quietly returned tax on the undiscounted > amount, you would have a compliance problem you could not see. Strip these > fields before calling on behalf of a self-managed merchant. ## Endpoint ```http POST https://api.zip-tax.com/merchant/cart/calculate ``` ## Request body The request contract is identical to the [TaxCloud path](cart#request-body). The limits below are the ones enforced on the self-managed path. | Field | Type | Required | Description | | ----------------- | ----------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `merchantId` | string (UUID) | Yes | The self-managed merchant to calculate for. Must be owned by the calling account. Consumed for routing and not part of the calculation. | | `items` | array of [Cart](#cart-object) | Yes | The carts to calculate tax for (1–100). Most integrations send a single cart. | | `transactionDate` | string (RFC3339 date-time) | No | The datetime the carts are calculated for, e.g. `2026-08-04T14:00:00Z`. Defaults to the time of calculation. | ### Cart object | Field | Type | Required | Description | | ------------------- | --------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cartId` | string (1–50 chars) | No | Your identifier for this cart. Echoed back if supplied; a UUID is generated if not. A correlation token only — it is **not** durable and cannot be used to create an order. | | `customerId` | string (1–50 chars) | Yes | Your identifier for the customer in your own system. Echoed back. | | `origin` | [Address](#address-object) | Yes | The ship-from address of the sale. Used with `destination` to decide [which address the tax is based on](#which-address-the-tax-is-based-on). | | `destination` | [Address](#address-object) | Yes | The ship-to address of the sale. | | `currency` | object | Yes | `{ "currencyCode": "USD" }`. `USD` is the only accepted value; anything else returns `400`. | | `lineItems` | array of [Line item](#line-item-object) | Yes | The line items in the cart (1–250). Tax is calculated and returned per item. | | `deliveredBySeller` | boolean | — | **Not supported.** Returns `400`. | | `discounts` | object | — | **Not supported.** Returns `400`. | | `exemption` | object | — | **Not supported.** Returns `400`. | > **Info** > > The TaxCloud path allows up to 500 line items per cart, implied by the `index` > bound. The self-managed engine caps a cart at **250** line items. If you build > one request payload for both merchant types, size carts to the lower limit. ### Address object US addresses only. | Field | Type | Required | Description | | ------------- | ---------------------- | -------- | ----------------------------------------------------------------------------- | | `line1` | string (1–128 chars) | Yes | First line of the address: street number and name, PO Box, or building. | | `line2` | string (max 128 chars) | No | Second line, if any (apartment, suite, unit). | | `city` | string (1–50 chars) | Yes | City or post-town. | | `state` | string | Yes | Two-letter state abbreviation, e.g. `CA`, `NY`, `TX`. | | `zip` | string (1–16 chars) | Yes | ZIP code. Five-digit (`94043`) and ZIP+4 (`94043-1351`) formats are accepted. | | `countryCode` | string | No | Must be `US`, or omitted (defaults to `US`). `CA` returns `400`. | ### Line item object | Field | Type | Required | Description | | ---------- | -------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `index` | integer (0–500) | Yes | Zero-based position of the item within the cart. Each item must have a unique index. | | `itemId` | string (1–50 chars) | Yes | Your unique identifier for the line item (e.g. SKU or line reference). Echoed back so you can match tax to items. | | `price` | number (≥ 0) | Yes | Unit price of the item in USD. | | `quantity` | number (> 0, ≤ 99999.9999) | Yes | Quantity of the item. Fractional quantities are allowed. | | `tic` | integer (0–100000) | No | Taxability code classifying the product. Uses the **Ziptax** code vocabulary, not TaxCloud's — see [Taxability codes](#taxability-codes). Defaults to `0` (general tangible goods). | ## Taxability codes This is the one place where the same request body means different things for the two merchant models. On the self-managed path, `tic` is interpreted as a **Ziptax taxability code** — the same vocabulary the [rate endpoints](../rest-api/overview) and the [TIC catalog](../product-rules/taxability-information-codes) use. TaxCloud's own codes are not interpreted here. | Code | Meaning | Handling | | --------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `0` or omitted | General tangible goods | Taxed at the combined rate for the sourced address. | | `10001` | Shipping, or shipping and handling combined | Resolved through the [shipping and handling rules](#shipping-and-handling). | | `11000` | Handling only (the labor portion) | Resolved through the [shipping and handling rules](#shipping-and-handling). | | Any other value | Ziptax product rule lookup | Passed to the rate engine as a taxability code; product rules and any TIC overrides apply. Requires a plan that includes product rules, otherwise the request fails with code `113`. | > **Warning** > > **Migrating a merchant between models changes what `tic` means.** TaxCloud's > shipping codes (`11010`–`11015`) and the Colorado retail delivery fee code > (`11098`) carry no special meaning on the self-managed path. Send `11010` for a > shipping line on a self-managed cart and it is treated as an ordinary product > code and taxed at the general rate, rather than run through the shipping rules > below. Use `10001` and `11000` for self-managed merchants. ### Shipping and handling Whether freight and handling are taxable depends on the state the tax is sourced to. Ziptax resolves this from the state's freight taxability rule, the same `shipping.taxable` value the [rate endpoints](../rest-api/by-address) return. | State freight rule | `10001` (shipping) | `11000` (handling) | | -------------------------------------------------- | ---------------------------- | ---------------------------- | | `Y` — freight taxable | Taxable at the combined rate | Taxable at the combined rate | | `N` — freight not taxable | Rate `0` | Rate `0` | | `L` — exempt when separately stated, labor taxable | Rate `0` | Taxable at the combined rate | | Unknown or not published | Rate `0` | Rate `0` | A non-taxable shipping line is still returned, with `tax.rate` and `tax.amount` set to `0`. Do not infer non-taxability from a missing line — every line item you send comes back. ## Which address the tax is based on Ziptax picks the sourcing address from the cart's `origin` and `destination`: 1. **Origin and destination are in different states** → tax is based on the **destination**. 2. **Same state, and that state is origin-sourced** (the state's `sourcingRules.value` is `O`) → tax is based on the **origin**. 3. **Otherwise** → tax is based on the **destination**. Most states are destination-sourced. A handful source intrastate sales to the origin, which is why an in-state sale can be taxed at the ship-from rate. See [Collection Requirements and Nexus](../reference/collection-requirements-and-nexus) for background. If one of the two addresses cannot be resolved, Ziptax falls back to the other and continues. If neither resolves, the request fails with the corresponding [error code](#error-codes). ## Rounding For each line item: ``` subtotal = price × quantity taxAmount = subtotal × effectiveRate ``` Both `tax.rate` and `tax.amount` are rounded independently to **5 decimal places** (half away from zero). Rates are returned as decimal fractions, so `0.08875` means 8.875%. > **Info** > > Ziptax returns tax at 5 decimal places per line item rather than rounding to > cents. Round to your currency's precision at the point you total the cart, not > per line, so the displayed total matches the sum of what you charge. ## Example #### cURL ```bash curl -X POST "https://api.zip-tax.com/merchant/cart/calculate" \ -H "X-API-KEY: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13", "transactionDate": "2026-08-04T14:00:00Z", "items": [ { "cartId": "my-cart-1", "customerId": "customer-453", "currency": { "currencyCode": "USD" }, "origin": { "line1": "1600 Amphitheatre Pkwy", "city": "Mountain View", "state": "CA", "zip": "94043" }, "destination": { "line1": "350 5th Ave", "city": "New York", "state": "NY", "zip": "10118" }, "lineItems": [ { "index": 0, "itemId": "sku-1", "price": 10.75, "quantity": 1.5, "tic": 0 }, { "index": 1, "itemId": "ship", "price": 8.95, "quantity": 1, "tic": 10001 } ] } ] }' ``` #### Python ```python import requests res = requests.post( "https://api.zip-tax.com/merchant/cart/calculate", headers={"X-API-KEY": "YOUR_API_KEY"}, json={ "merchantId": "9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13", "transactionDate": "2026-08-04T14:00:00Z", "items": [ { "cartId": "my-cart-1", "customerId": "customer-453", "currency": {"currencyCode": "USD"}, "origin": {"line1": "1600 Amphitheatre Pkwy", "city": "Mountain View", "state": "CA", "zip": "94043"}, "destination": {"line1": "350 5th Ave", "city": "New York", "state": "NY", "zip": "10118"}, "lineItems": [ {"index": 0, "itemId": "sku-1", "price": 10.75, "quantity": 1.5, "tic": 0}, {"index": 1, "itemId": "ship", "price": 8.95, "quantity": 1, "tic": 10001}, ], } ], }, ) res.raise_for_status() cart = res.json() ``` #### Node.js ```javascript const res = await fetch("https://api.zip-tax.com/merchant/cart/calculate", { method: "POST", headers: { "X-API-KEY": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ merchantId: "9f4c1e2a-7b3d-4c5e-8a91-2f6b0d4e7c13", transactionDate: "2026-08-04T14:00:00Z", items: [ { cartId: "my-cart-1", customerId: "customer-453", currency: { currencyCode: "USD" }, origin: { line1: "1600 Amphitheatre Pkwy", city: "Mountain View", state: "CA", zip: "94043" }, destination: { line1: "350 5th Ave", city: "New York", state: "NY", zip: "10118" }, lineItems: [ { index: 0, itemId: "sku-1", price: 10.75, quantity: 1.5, tic: 0 }, { index: 1, itemId: "ship", price: 8.95, quantity: 1, tic: 10001 }, ], }, ], }), }); const cart = await res.json(); ``` ## Response Returns the submitted carts with tax computed per line item. | Field | Type | Description | | ---------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- | | `transactionDate` | string (RFC3339 date-time) | The datetime the carts were calculated for: the value you sent, or the calculation time if you omitted it. | | `items` | array | One calculated cart per submitted cart, in the same order. | | `items[].cartId` | string | The cart identifier you supplied, or a generated UUID. Not durable. | | `items[].customerId` | string | Your customer identifier, as submitted. | | `items[].currency` | object | Always `{ "currencyCode": "USD" }`. | | `items[].origin` / `items[].destination` | [Address](#address-object) | The addresses, as submitted. | | `items[].lineItems[].index` | integer | Zero-based position of the item within the cart, as submitted. | | `items[].lineItems[].itemId` | string | Your line item identifier, as submitted. | | `items[].lineItems[].tic` | integer or null | The taxability code the item was calculated under. `null` when you omitted it. | | `items[].lineItems[].price` | number | The unit price tax was calculated on. | | `items[].lineItems[].originalPrice` | number | Equal to `price`, since discounts are not supported. | | `items[].lineItems[].quantity` | number | Quantity of the item, as submitted. | | `items[].lineItems[].tax.rate` | number | The effective combined rate applied, as a decimal fraction, to 5 decimal places. | | `items[].lineItems[].tax.amount` | number | The calculated tax for the line item, to 5 decimal places. | ```json { "transactionDate": "2026-08-04T14:00:00Z", "items": [ { "cartId": "my-cart-1", "customerId": "customer-453", "currency": { "currencyCode": "USD" }, "origin": { "line1": "1600 Amphitheatre Pkwy", "city": "Mountain View", "state": "CA", "zip": "94043", "countryCode": "US" }, "destination": { "line1": "350 5th Ave", "city": "New York", "state": "NY", "zip": "10118", "countryCode": "US" }, "lineItems": [ { "index": 0, "itemId": "sku-1", "tic": 0, "price": 10.75, "originalPrice": 10.75, "quantity": 1.5, "tax": { "rate": 0.08875, "amount": 1.43109 } }, { "index": 1, "itemId": "ship", "tic": 10001, "price": 8.95, "originalPrice": 8.95, "quantity": 1, "tax": { "rate": 0.08875, "amount": 0.79431 } } ] } ] } ``` ### How the response differs from the TaxCloud path | Field | TaxCloud path | Self-managed | | --------------------------- | -------------------------------------------------- | --------------------------------------------- | | `connectionId` | The TaxCloud connection the calculation ran under | **Omitted** — there is no TaxCloud connection | | `items[].exemption` | The exemption applied | **Omitted** — exemptions are not supported | | `items[].deliveredBySeller` | Echoed | **Omitted** — not a supported input | | `items[].cartId` | Durable; can be passed to `order/create-from-cart` | Correlation token only, not durable | | `lineItems[].price` | The discounted price | Always equal to `originalPrice` | | `lineItems[].originalPrice` | The pre-discount price | Retained, equal to `price` | `originalPrice` is kept and set equal to `price` deliberately, so a parser that handles the TaxCloud response works unchanged on a self-managed response. ### `cartId` is not durable Nothing is stored. `cartId` exists so you can correlate a response with the request that produced it and trace it in your own logs. It is **not** a handle to a saved cart: * You cannot pass it to [`order/create-from-cart`](orders#create-an-order-from-a-cart) — that endpoint returns `403` for self-managed merchants. * Recalculating the same cart produces a fresh calculation, not a lookup. If you need the tax figures later, store the response on your side. ## Worked examples > **Info** > > The rates below are illustrative. Actual rates come from the live rate tables at > the time of the request. **Interstate sale, general goods plus shipping.** Ship-from Mountain View, CA to New York, NY. The states differ, so tax is based on the destination. New York treats freight as taxable, so the shipping line is taxed at the same combined rate. | Item | `tic` | Price | Qty | Subtotal | Effective rate | Tax | | ------- | ------- | ----- | --- | -------- | -------------- | ------- | | `sku-1` | `0` | 10.75 | 1.5 | 16.125 | 0.08875 | 1.43109 | | `ship` | `10001` | 8.95 | 1 | 8.95 | 0.08875 | 0.79431 | **Intrastate sale in an origin-sourced state.** Ship-from Austin, TX to Houston, TX. Same state, and Texas sources this sale to the origin, so the Austin rate applies even though the goods ship to Houston. | Item | `tic` | Price | Qty | Subtotal | Effective rate | Tax | | ------- | ----- | ------ | --- | -------- | -------------- | -------- | | `sku-9` | `0` | 100.00 | 2 | 200.00 | 0.0825 | 16.50000 | **Non-taxable freight with separately stated handling.** Destination in a state whose freight rule is `L` — freight is exempt when separately stated, but the labor portion is taxable. | Item | `tic` | Price | Qty | Subtotal | Effective rate | Tax | | ---------- | ------- | ----- | --- | -------- | -------------- | ------- | | `sku-3` | `0` | 50.00 | 1 | 50.00 | 0.07 | 3.50000 | | `freight` | `10001` | 12.00 | 1 | 12.00 | 0 | 0.00000 | | `handling` | `11000` | 5.00 | 1 | 5.00 | 0.07 | 0.35000 | ## Errors A single invalid cart fails the whole request — partial results are not returned, since a checkout cannot act on a half-calculated cart. Errors identify the failing cart by `cartId` (or its index) and, where it applies, the `itemId`. | HTTP | When it happens | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Malformed JSON; missing or invalid `merchantId`; a validation failure; a non-USD currency; `countryCode: "CA"`; or one of the unsupported fields (`discounts`, `exemption`, `deliveredBySeller`). | | `401` | Missing, invalid, or inactive API key. | | `403` | Merchant not owned by your account, a plan-gated feature (see the code table below), or a non-calculate operation attempted for a self-managed merchant. | | `404` | Merchant not found, or no rate found for the supplied address. | | `413` | Request body larger than 5 MiB. | | `429` | Rate limit exceeded (response code `108`). | | `500` | Internal error. | > **Info** > > `502` and `504` cannot occur on this path. There is no upstream service call to > be unreachable or time out — a distinction worth keeping in mind if your retry > logic keys on those statuses for the [TaxCloud path](transactions#retries-and-idempotency). ### Error codes Failures from the rate engine map to these [response codes](../reference/response-codes): | Code | HTTP | Meaning | | ----- | ----- | ------------------------------------------ | | `101` | `401` | Invalid API key | | `102` | `400` | State is not in a valid format | | `103` | `400` | City is not in a valid format | | `104` | `400` | Postal code is not in a valid format | | `105` | `400` | Query format is not valid | | `106` | `500` | Unknown API error | | `107` | `403` | Feature not enabled for your plan | | `108` | `429` | Rate limit exceeded | | `109` | `400` | Address is missing, incomplete, or invalid | | `110` | `404` | No tax rate found for the supplied address | | `111` | `400` | Historical parameter is not valid | | `112` | `403` | International rates not enabled | | `113` | `403` | Product rate rules not enabled | ## Usage and limits | | Detail | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Merchant requests** | One merchant request per API call, regardless of how many carts or line items it contains, and regardless of outcome. | | **Geo requests** | One geo request per distinct address per cart — normally two, for the origin and the destination. Sending many line items, or many taxability codes, does not increase this. | | **Rate limiting** | Per-key, the same as the rest of the API. Exceeding it returns `429` with code `108`. | Geo request limits include the standard overage allowance. See [Account Metrics](../reference/account-metrics) to track usage against your plan, and [Rate Limiting & Errors](../reference/rate-limiting-errors) for backoff guidance. ## Nexus is your responsibility on this path > **Warning** > > Ziptax returns the tax rate for the sourced address whether or not the merchant > has an obligation to collect there. It does not check the merchant's > registrations or nexus footprint before calculating. Deciding **where** a merchant must collect stays with you and the merchant. Use [Nexus Management](nexus-management) and [Economic Thresholds](economic-thresholds) to track where a merchant has physical or economic nexus, and call this endpoint for the states where they have determined they need to collect. See [Collection Requirements and Nexus](../reference/collection-requirements-and-nexus) for how obligations arise. This is the same division of responsibility as the general-purpose [rate endpoints](../rest-api/overview). If you want registration, filing, and remittance handled for the merchant as well, that is the [TaxCloud-connected model](taxcloud-connected-merchants). ## Moving a merchant between models A self-managed merchant can be [invited to TaxCloud](self-managed-merchants#inviting-a-self-managed-merchant-to-taxcloud) later, which converts them to the connected model, and a connected merchant can be moved back with `setMerchantType: "self-managed"`. Two things change for cart calculation in either direction, and neither is retroactive: 1. **The engine changes.** Calculations run through TaxCloud once the merchant is connected, and through the Ziptax engine once they are self-managed. Rates for the same cart may differ between the two, which shows up as a discontinuity in your own reporting. Keep the calculation results you stored under each model distinguishable. 2. **`tic` values change meaning.** Switch shipping lines from `10001` to TaxCloud's shipping codes when the merchant becomes connected, and back when they become self-managed. See [Taxability codes](#taxability-codes). Allow up to a minute after a `setMerchantType` change for cart calculation to start using the new engine. Merchants are never migrated between models automatically. ## Related #### [Self-Managed Merchants](self-managed-merchants) Create merchants that handle their own registration, filing, and remittance. #### [Cart Tax Calculation](cart) The same endpoint for a TaxCloud-connected merchant. #### [Taxability Information Codes](../product-rules/taxability-information-codes) The Ziptax code catalog this path calculates against. #### [Merchant Transactions](transactions) Shared conventions, errors, and usage. > Calculate sales tax for a cart on behalf of a self-managed merchant, using the Ziptax rate engine.