Self-Managed Cart Calculation

Calculate sales tax for a cart on behalf of a self-managed merchant, using the Ziptax rate engine.

Available onProEnterprise

This capability is in active development. Request bodies, responses, and supported fields may change before general availability. Contact support@zip.tax for early access.

POST /merchant/cart/calculate serves both compliance models. When the merchantId you send belongs to a self-managed merchant, 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, the cart is forwarded to TaxCloud instead; that path is documented in Cart Tax Calculation.

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:

EndpointSelf-managed
POST /merchant/cart/calculateAvailable
POST /merchant/order/create403
POST /merchant/order/create-from-cart403
POST /merchant/order/get403
POST /merchant/order/update403
POST /merchant/cert/create403
POST /merchant/refund/create403

Not supported on this path in this release:

Not supportedDetail
DiscountsNeither line-item nor order-level discounts.
ExemptionsNo exemption field and no exemption certificates.
Cart persistenceThe returned cartId cannot be turned into an order. See cartId is not durable.
Orders, refunds, filingBlocked, per the table above.
Canadian cartscountryCode: "CA" is rejected. US addresses only.
deliveredBySellerSeller-delivery taxability rules are not applied.

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

1POST https://api.zip-tax.com/merchant/cart/calculate

Request body

The request contract is identical to the TaxCloud path. The limits below are the ones enforced on the self-managed path.

FieldTypeRequiredDescription
merchantIdstring (UUID)YesThe self-managed merchant to calculate for. Must be owned by the calling account. Consumed for routing and not part of the calculation.
itemsarray of CartYesThe carts to calculate tax for (1–100). Most integrations send a single cart.
transactionDatestring (RFC3339 date-time)NoThe datetime the carts are calculated for, e.g. 2026-08-04T14:00:00Z. Defaults to the time of calculation.

Cart object

FieldTypeRequiredDescription
cartIdstring (1–50 chars)NoYour 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.
customerIdstring (1–50 chars)YesYour identifier for the customer in your own system. Echoed back.
originAddressYesThe ship-from address of the sale. Used with destination to decide which address the tax is based on.
destinationAddressYesThe ship-to address of the sale.
currencyobjectYes{ "currencyCode": "USD" }. USD is the only accepted value; anything else returns 400.
lineItemsarray of Line itemYesThe line items in the cart (1–250). Tax is calculated and returned per item.
deliveredBySellerbooleanNot supported. Returns 400.
discountsobjectNot supported. Returns 400.
exemptionobjectNot supported. Returns 400.

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.

FieldTypeRequiredDescription
line1string (1–128 chars)YesFirst line of the address: street number and name, PO Box, or building.
line2string (max 128 chars)NoSecond line, if any (apartment, suite, unit).
citystring (1–50 chars)YesCity or post-town.
statestringYesTwo-letter state abbreviation, e.g. CA, NY, TX.
zipstring (1–16 chars)YesZIP code. Five-digit (94043) and ZIP+4 (94043-1351) formats are accepted.
countryCodestringNoMust be US, or omitted (defaults to US). CA returns 400.

Line item object

FieldTypeRequiredDescription
indexinteger (0–500)YesZero-based position of the item within the cart. Each item must have a unique index.
itemIdstring (1–50 chars)YesYour unique identifier for the line item (e.g. SKU or line reference). Echoed back so you can match tax to items.
pricenumber (≥ 0)YesUnit price of the item in USD.
quantitynumber (> 0, ≤ 99999.9999)YesQuantity of the item. Fractional quantities are allowed.
ticinteger (0–100000)NoTaxability code classifying the product. Uses the Ziptax code vocabulary, not TaxCloud’s — see 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 and the TIC catalog use. TaxCloud’s own codes are not interpreted here.

CodeMeaningHandling
0 or omittedGeneral tangible goodsTaxed at the combined rate for the sourced address.
10001Shipping, or shipping and handling combinedResolved through the shipping and handling rules.
11000Handling only (the labor portion)Resolved through the shipping and handling rules.
Any other valueZiptax product rule lookupPassed to the rate engine as a taxability code; product rules and any TIC overrides apply. Requires the product_rates entitlement, otherwise the request fails with code 113.

Migrating a merchant between models changes what tic means. TaxCloud’s shipping codes (1101011015) 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 return.

State freight rule10001 (shipping)11000 (handling)
Y — freight taxableTaxable at the combined rateTaxable at the combined rate
N — freight not taxableRate 0Rate 0
L — exempt when separately stated, labor taxableRate 0Taxable at the combined rate
Unknown or not publishedRate 0Rate 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 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.

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%.

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 -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 }
> ]
> }
> ]
> }'

Response

Returns the submitted carts with tax computed per line item.

FieldTypeDescription
transactionDatestring (RFC3339 date-time)The datetime the carts were calculated for: the value you sent, or the calculation time if you omitted it.
itemsarrayOne calculated cart per submitted cart, in the same order.
items[].cartIdstringThe cart identifier you supplied, or a generated UUID. Not durable.
items[].customerIdstringYour customer identifier, as submitted.
items[].currencyobjectAlways { "currencyCode": "USD" }.
items[].origin / items[].destinationAddressThe addresses, as submitted.
items[].lineItems[].indexintegerZero-based position of the item within the cart, as submitted.
items[].lineItems[].itemIdstringYour line item identifier, as submitted.
items[].lineItems[].ticinteger or nullThe taxability code the item was calculated under. null when you omitted it.
items[].lineItems[].pricenumberThe unit price tax was calculated on.
items[].lineItems[].originalPricenumberEqual to price, since discounts are not supported.
items[].lineItems[].quantitynumberQuantity of the item, as submitted.
items[].lineItems[].tax.ratenumberThe effective combined rate applied, as a decimal fraction, to 5 decimal places.
items[].lineItems[].tax.amountnumberThe calculated tax for the line item, to 5 decimal places.
1{
2 "transactionDate": "2026-08-04T14:00:00Z",
3 "items": [
4 {
5 "cartId": "my-cart-1",
6 "customerId": "customer-453",
7 "currency": { "currencyCode": "USD" },
8 "origin": { "line1": "1600 Amphitheatre Pkwy", "city": "Mountain View", "state": "CA", "zip": "94043", "countryCode": "US" },
9 "destination": { "line1": "350 5th Ave", "city": "New York", "state": "NY", "zip": "10118", "countryCode": "US" },
10 "lineItems": [
11 {
12 "index": 0,
13 "itemId": "sku-1",
14 "tic": 0,
15 "price": 10.75,
16 "originalPrice": 10.75,
17 "quantity": 1.5,
18 "tax": { "rate": 0.08875, "amount": 1.43109 }
19 },
20 {
21 "index": 1,
22 "itemId": "ship",
23 "tic": 10001,
24 "price": 8.95,
25 "originalPrice": 8.95,
26 "quantity": 1,
27 "tax": { "rate": 0.08875, "amount": 0.79431 }
28 }
29 ]
30 }
31 ]
32}

How the response differs from the TaxCloud path

FieldTaxCloud pathSelf-managed
connectionIdThe TaxCloud connection the calculation ran underOmitted — there is no TaxCloud connection
items[].exemptionThe exemption appliedOmitted — exemptions are not supported
items[].deliveredBySellerEchoedOmitted — not a supported input
items[].cartIdDurable; can be passed to order/create-from-cartCorrelation token only, not durable
lineItems[].priceThe discounted priceAlways equal to originalPrice
lineItems[].originalPriceThe pre-discount priceRetained, 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 — 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

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.

ItemticPriceQtySubtotalEffective rateTax
sku-1010.751.516.1250.088751.43109
ship100018.9518.950.088750.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.

ItemticPriceQtySubtotalEffective rateTax
sku-90100.002200.000.082516.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.

ItemticPriceQtySubtotalEffective rateTax
sku-3050.00150.000.073.50000
freight1000112.00112.0000.00000
handling110005.0015.000.070.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.

HTTPWhen it happens
400Malformed JSON; missing or invalid merchantId; a validation failure; a non-USD currency; countryCode: "CA"; or one of the unsupported fields (discounts, exemption, deliveredBySeller).
401Missing, invalid, or inactive API key.
403Merchant 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.
404Merchant not found, or no rate found for the supplied address.
413Request body larger than 5 MiB.
429Rate limit exceeded (response code 108).
500Internal error.

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.

Error codes

Failures from the rate engine map to these response codes:

CodeHTTPMeaning
101401Invalid API key
102400State is not in a valid format
103400City is not in a valid format
104400Postal code is not in a valid format
105400Query format is not valid
106500Unknown API error
107403Feature not enabled for your plan
108429Rate limit exceeded
109400Address is missing, incomplete, or invalid
110404No tax rate found for the supplied address
111400Historical parameter is not valid
112403International rates not enabled
113403Product rate rules not enabled

Usage, limits, and entitlements

Detail
Merchant requestsOne merchant request per API call, regardless of how many carts or line items it contains, and regardless of outcome.
Geo requestsOne 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.
geo_enabledRequired. Cart calculation is address-based; without the entitlement the request fails with code 107.
product_ratesRequired only when a line item carries a taxability code other than 0, 10001, or 11000. Otherwise the request fails with code 113.
Rate limitingPer-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 to track usage against your plan, and Rate Limiting & Errors for backoff guidance.

Nexus is your responsibility on this path

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 and 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 for how obligations arise.

This is the same division of responsibility as the general-purpose rate endpoints. If you want registration, filing, and remittance handled for the merchant as well, that is the TaxCloud-connected model.

Moving a merchant between models

A self-managed merchant can be invited to TaxCloud later, which converts them to the connected model. Two things change for cart calculation when that happens, and neither is retroactive:

  1. The engine changes. Calculations run through TaxCloud from that point on. Rates for the same cart may differ from what the Ziptax engine returned before, 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. See Taxability codes.

Merchants are never migrated between models automatically.