New France e-invoicing mandate goes live September 2026 — our implementation is ready. See all mandates →
Tax Calculations

Tax Calculations

The Tax Calculation API determines the correct tax amount for any transaction — VAT, GST, sales tax, and other indirect taxes — across 76 countries in a single API call. You send a customer and a list of line items — your own supplier side is optional and auto-filled from your entity's master data; Clearvo resolves the applicable jurisdiction, classifies each product, applies the correct rate band, and returns a fully broken-down tax result. (seller is a deprecated alias for supplier — see Quickstart.)

Key capabilities

  • 76 countries — EU multi-band VAT (standard, middle, reduced, super-reduced, special), US sales tax (all 50 states, 13k+ jurisdictions), Canadian GST/HST/PST, and more (see Supported Jurisdictions)
  • AI-powered product classification — describe a product in plain language; no HS codes or custom slug mapping required
  • Jurisdiction resolution — determines where tax is owed based on shipping address, billing address, IP geolocation, or card BIN country (2-of-3 rule for B2C digital goods)
  • B2B reverse charge and EU IOSS — applies the correct treatment automatically based on seller and buyer tax IDs
  • EN16931 tax codes — output maps directly to Clearvo e-invoicing fields with no additional transformation
  • Flexible input — send country names, ISO alpha-3 codes, currency names, US state names, or informal customer type labels; all normalised server-side before validation
  • Graceful degradation — never errors on rate-service failures; returns a result with degraded: true and a zero rate
  • Recorded by default, preview on request — every calculation is persisted to your audit trail unless you send "commit": false for a quote/preview that shouldn't count toward usage or thresholds (see Calculate)
ℹ
Tax Calculation uses the same base URL as the core API (https://api.clearvo.io/v1) and the same csk_live_* / csk_test_* key system. No separate credentials are needed — the same key you use for e-invoicing and TIN validation works here.
Tax Calculations

Authentication

Tax Calculations uses the same x-api-key header, key scoping, and X-Entity-Id rules as every other Clearvo product — see Authentication in the Account & Platform reference for the full header table and examples.

Use a csk_test_* key for sandbox requests — rate lookups and jurisdiction resolution run normally, but no external rate-authority calls are made and results are not billed. See the Sandbox section for details.

Tax Calculations

Quickstart

The simplest possible calculation: a B2C digital product sold to a customer in Germany. Clearvo classifies the product, resolves the jurisdiction, and returns the German VAT rate (19%) with an EN16931 tax code ready for e-invoicing. This example pins amountIncludesTax: false for clarity — non-US accounts default to tax-inclusive pricing (defaultPriceIncludesTax, see Account Settings) unless overridden per line or per account, as shown here.

curl
curl https://api.clearvo.io/v1/tax/calculate \
  -X POST \
  -H "x-api-key: csk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "EUR",
    "supplier": {
      "billingAddress": { "country": "IE" },
      "taxId": "IE1234567T"
    },
    "customer": {
      "billingAddress": { "country": "DE" }
    },
    "lineItems": [
      {
        "id": "line-1",
        "amount": 100.00,
        "amountIncludesTax": false,
        "productName": "Annual SaaS subscription"
      }
    ]
  }'
JSON Response
{
  "calculationId": "cl_calc_01j4...",
  "entityId": "ent_01hx...",
  "apiVersion": "2026-01-01",
  "resolvedAt": "2026-07-08T10:00:00.000Z",
  "sandbox": true,
  "committed": true,
  "jurisdiction": {
    "country": "DE",
    "resolutionMethod": "BILLING_ADDRESS",
    "resolutionPrecision": "COUNTRY",
    "conflicts": []
  },
  "entityRole": "supplier",
  "supplier": {
    "country": "IE",
    "taxId": "IE1234567T",
    "isEntity": true
  },
  "customer": {
    "country": "DE",
    "isEntity": false,
    "b2bOverride": false,
    "taxIdValidated": false
  },
  "sellerRegistration": {
    "status": "REGISTERED",
    "canCollectTax": true,
    "pricingModel": "TAX_EXCLUSIVE"
  },
  "taxTreatment": "STANDARD",
  "taxCode": "S",
  "transactionType": "invoice",
  "lineItems": [
    {
      "id": "line-1",
      "taxCategory": "digital_services",
      "taxCode": "S",
      "rate": 0.19,
      "taxableAmount": 100.00,
      "taxAmount": 19.00,
      "totalAmount": 119.00,
      "classificationStatus": "APPROVED",
      "classificationConfidence": 0.97
    }
  ],
  "totalTax": 19.00,
  "summary": {
    "totalAmount": 100.00,
    "totalDiscount": 0.00,
    "totalTax": 19.00,
    "totalAmountWithTax": 119.00
  }
}

The rate: 0.19 on each line item flows directly into a Clearvo e-invoice submission as taxRate: 19 — no category mapping required (the invoice line never carries a taxCode). See E-Invoicing Integration.

This example uses a csk_test_* sandbox key, so it's safe to copy-paste and run before you're ready to touch production — sandbox calculations never count toward usage or Compliance Radar totals. Note that commit was not sent in this request either way: an omitted commit defaults to true, so it was still recorded ("committed": true) to your sandbox audit trail exactly as it would be in production. Run the identical request with a csk_live_* key and it is recorded for real — see the Calculate section below for what a recorded calculation does and how to opt out with "commit": false.

Tax Calculations

Changelog

Behavioural and breaking changes across Clearvo's API. Entries 1 and 2 are corrections to POST /v1/tax/calculate's place-of-supply logic and tax-code selection, not new opt-in features — they change the output for the specific transaction shapes described. Entry 3 changes POST /v1/tax/calculate's default recording behaviour for any call that omits commit. Entry 4 is E-Invoicing's buyer → customer vocabulary rename and is still in draft — see its own status marker.

1
General B2C services are now taxed at the seller's country, not the customer's

Affects: B2C sales of general (non-digital) services — consulting, legal, accounting, financial services, insurance, healthcare — where taxCategory resolves to a GENERAL_SERVICE place-of-supply category (e.g. professional_services, financial_services, insurance, healthcare). Does not affect B2B, digital services, or physical goods.

Geographic scope: applied by Clearvo for any seller/buyer country pair — not limited to EU sellers or EU buyers, even though the citation (EU VAT Directive Art. 45) is EU law.

Before: a general B2C service line fell through to the same customer-location rule used for digital services (Art. 58), so it was destination-sourced to the customer's country. A German consultancy (standard VAT 19%) invoicing a French consumer (standard VAT 20%) for a one-off advisory engagement of €1,000 charged French VAT: jurisdiction.country: "FR", rate: 0.20, taxAmount: 200.00.

After: the same line is sourced to the seller's own establishment country (Art. 45 — general B2C services are taxed where the supplier is established, not the customer). The same invoice now charges German VAT: jurisdiction.country: "DE", jurisdiction.resolutionMethod: "SELLER_ADDRESS", rate: 0.19, taxAmount: 190.00.

OSS impact: One-Stop Shop (OSS Union) exists specifically to let a seller report and remit VAT on B2C sales destination-sourced to other EU countries through a single return, avoiding registration in each buyer's country. Since a GENERAL_SERVICE B2C sale is now sourced to the seller's own country instead, it is a domestic sale for VAT purposes and is not OSS-eligible — it never appears on an OSS return and does not count toward OSS turnover. This was already true of the correct legal position; the earlier destination-sourced behaviour risked a merchant over-reporting these lines through OSS as if they were cross-border. eligibleSchemes for GENERAL_SERVICE categories has always excluded OSS schemes (see GET /v1/tax/categories), so no eligibility data changed — only the sourced jurisdiction and, for cross-border cases, the rate did.

2
EU→EU B2B service invoices now use tax code AE, not K

Affects: B2B sales where both the seller and the buyer are in the EU, are in different EU countries, and the line is a DIGITAL_SERVICE or GENERAL_SERVICE category (e.g. SaaS, API/data services, streaming, consulting, insurance). Does not affect B2B goods, domestic B2B, EU→non-EU exports, non-EU→EU B2B, or non-EU→non-EU B2B — all unchanged.

Geographic scope: EU-only — this is specifically the EU-seller-to-EU-buyer cross-border branch (Art. 44/196 vs. Art. 138), which by definition only exists within the EU.

Before: every EU seller → EU buyer cross-border B2B line emitted tax code K (the intra-Community supply of goods code, Art. 138) regardless of category. An Irish SaaS company invoicing a German business customer €1,000/month for a software subscription got taxCode: "K", rate: 0, treatment ZERO_RATED — the correct treatment (reverse charge, buyer self-accounts) but the wrong EN16931 code for the e-invoice, since K specifically denotes a zero-rated goods dispatch, not a service reverse charge.

After: the same line gets taxCode: "AE" (VAT reverse charge, Art. 44/196) — the correct code for a B2B services reverse charge. Treatment (REVERSE_CHARGE) and rate (0%) are unchanged; only the EN16931 tax code differs. K is now reserved exclusively for EU→EU cross-border GOODS lines, matching its Art. 138 legal basis.

If you generate e-invoices (or downstream accounting entries) keyed off taxCode, re-check any logic that branched on K for EU cross-border B2B service invoices — those lines now arrive as AE. No change is needed for goods invoices, which continue to receive K exactly as before.

Both changes also introduced three additive fields on each response line item — jurisdiction (this line's own resolved destination, including the new SELLER_ADDRESS resolution method), placeOfSupplyRule, and sourcingRationale — so you can see exactly how each line was sourced without inferring it from rate/treatment alone. See the Sourcing matrix in Jurisdiction Resolution below for the full table, and the OpenAPI spec for the field-level schema.

3
Effective 2026-09-12: commit now defaults to true — calculations are recorded unless you opt out

Affects: any caller of POST /v1/tax/calculate that omits the commit field entirely. Every request that already sends commit explicitly — true or false — is completely unaffected. Internal integrations (WooCommerce, Shopify, TikTok Shop, BigCommerce, Coupa) already pass commit explicitly on every call and are unaffected.

Before: an omitted commit defaulted to false — the calculation was priced but never recorded: no audit-trail entry, no dashboard entry, no Compliance Radar contribution, and idempotencyKey was silently ignored.

After: an omitted commit now defaults to true — the calculation is recorded exactly as if you had sent "commit": true. Consequences, each independent of the others:

  • The calculation is persisted to your audit trail, appears in the dashboard, and counts toward your calculation usage. The response now always includes "committed": true so you can confirm this synchronously — see Response Reference.
  • It contributes to your Compliance Radar economic-nexus threshold totals for its jurisdiction. This can only ever inflate an apparent threshold total, never deflate one, and can trigger a false APPROACHING/BREACH transition or a new mandate-obligation alert if you call /calculate more than once per real sale (e.g. a "quote" pattern) without passing commit: false for the quote calls.
  • A transactionType: "credit_note" request that omits both commit and relatedCalculationId now fails with a 422 instead of succeeding as an ephemeral preview — see the reworded validation message in Credit Notes & Refunds.
  • A supplied idempotencyKey is now honoured (previously silently ignored when commit was omitted): the first request for a given key wins the claim, a concurrent duplicate gets 409 idempotency_key_in_progress, and a later request with the same key replays the original stored response.
  • The full request body is retained for the audit-retention period per the Privacy Policy; not deletable via the API — a credit note or refund reverses its effect on totals but does not remove the record.

To keep the old ephemeral (preview/quoting) behaviour for a given call, send "commit": false explicitly — nothing else about the request or response shape changes.

4
DRAFT — pending release, not yet live 2026-09-21 (breaking change): E-invoicing party renamed buyer → customer

Affects: E-Invoicing only (POST /v1/send, bulk CSV, webhooks, exports, MCP submit_invoice, and every ERP/sales-channel mapper). Does not affect Tax Calculations.

Every public field, error code, and stored value that named the invoice counterparty buyer is renamed to customer, platform-wide, with no alias — a request or CSV upload still using a retired buyer* key is rejected (UNKNOWN_FIELD_BUYER_RENAMED / UNKNOWN_HEADER_BUYER_RENAMED; see Errors).

  • Request/response fields: buyer → customer, plus every buyer*-prefixed field (buyerType, notifyBuyer, buyerName, buyerTaxId, buyerNotification, notifyBuyerByDefault, and the country-specific countrySpecific.*.buyer* fields) across POST /v1/send, GET /v1/invoices, GET /v1/export, GET /v1/mandates, exemptions, transactions, and NEEDS_INFO payloads.
  • ~24 error codes containing BUYER renamed to their CUSTOMER equivalent, one-for-one, no alias: MISSING_BUYER_NAME → MISSING_CUSTOMER_NAME, MISSING_BUYER_ADDRESS_CITY → MISSING_CUSTOMER_ADDRESS_CITY, MISSING_BUYER_ADDRESS_COUNTRY → MISSING_CUSTOMER_ADDRESS_COUNTRY, INVALID_BUYER_ADDRESS_COUNTRY → INVALID_CUSTOMER_ADDRESS_COUNTRY, INVALID_BUYER_TYPE → INVALID_CUSTOMER_TYPE, MISSING_RO_BUYER_COUNTY_CODE → MISSING_RO_CUSTOMER_COUNTY_CODE, UNKNOWN_JO_BUYER_ID_TYPE → UNKNOWN_JO_CUSTOMER_ID_TYPE, MISSING_PEPPOL_BUYER_ENDPOINT_ID → MISSING_PEPPOL_CUSTOMER_ENDPOINT_ID, BUYER_COUNTRY_AMBIGUOUS → CUSTOMER_COUNTRY_AMBIGUOUS, BUYER_COUNTRY_NORMALIZED → CUSTOMER_COUNTRY_NORMALIZED, BUYER_POSTCODE_NORMALIZED → CUSTOMER_POSTCODE_NORMALIZED, BUYER_REGION_NORMALIZED → CUSTOMER_REGION_NORMALIZED, BUYER_TYPE_TAX_ID_MISMATCH → CUSTOMER_TYPE_TAX_ID_MISMATCH, REVERSE_CHARGE_MISSING_BUYER_VATID → REVERSE_CHARGE_MISSING_CUSTOMER_VATID, BUYER_SELLER_SAME_TAX_ID → CUSTOMER_SUPPLIER_SAME_TAX_ID, BUYER_TAX_ID_COUNTRY_MISMATCH → CUSTOMER_TAX_ID_COUNTRY_MISMATCH, INVALID_HU_BUYER_TAX_NUMBER → INVALID_HU_CUSTOMER_TAX_NUMBER, MISSING_HU_BUYER_ADDRESS → MISSING_HU_CUSTOMER_ADDRESS, BUYER_TAX_ID_MISMATCH → CUSTOMER_TAX_ID_MISMATCH, MISSING_BUYER_DATA → MISSING_CUSTOMER_DATA, MISSING_BUYER_REGISTRATION → MISSING_CUSTOMER_REGISTRATION, MISSING_BUYER_EMAIL → MISSING_CUSTOMER_EMAIL, MISSING_BUYER_FISCAL_CODE → MISSING_CUSTOMER_FISCAL_CODE, and INVALID_BUYER_CUIT → INVALID_CUSTOMER_CUIT. See Error Reference for the current catalogue.
  • CSV headers: bulk send and export columns buyer_country, buyer_vat_number, buyer_name, buyer_address_line1, buyer_city, buyer_postal_code, buyer_tax_id renamed to their customer_* equivalents.
  • Webhook keys: payload fields buyer* / buyerNotification renamed to customer* / customerNotification, and invoice.rejected's rejectedBy: 'buyer' is now rejectedBy: 'customer' ('authority' unchanged) — see Webhooks.
  • Stored records rewritten in place: existing invoice records, transaction rows, and webhook-delivery history are rewritten to the new keys on deploy of this change — there is nothing to migrate or re-fetch on your end.

Unchanged: buyerReference (EN16931 BT-10 "Buyer reference") keeps its name everywhere — it names the standard's own element, not the invoice counterparty, so it was deliberately not renamed. Tax Calculations' importerOfRecord SELLER / BUYER enum is a customs role (who is liable for import duty), unrelated to this e-invoicing party rename, and is also unchanged.

Tax Calculations

Input Formats

Clearvo normalises incoming field values server-side before validation runs. You can send data in the format your billing system or platform already produces — no pre-processing or mapping layer needed on your end.

Countries

The country field on any address accepts ISO 3166-1 alpha-2, alpha-3, or an English country name. All forms are resolved to alpha-2 internally.

What you sendResolved toNotes
"DE""DE"Already canonical — passed through unchanged
"DEU""DE"ISO 3166-1 alpha-3
"GBR""GB"As returned by Apple StoreKit storefront
"USA""US"
"Germany""DE"English country name
"United States""US"
"Netherlands" / "Holland""NL"Common informal names are also recognised
"Czechia" / "Czech Republic""CZ"

Currencies

The currency and reportingCurrency fields accept ISO 4217 codes or English currency names.

What you sendResolved to
"EUR" / "eur""EUR"
"Euro" / "euros""EUR"
"US Dollar" / "Dollar""USD"
"British Pound" / "Pound Sterling""GBP"
"Japanese Yen""JPY"
"Canadian Dollar""CAD"

Regions (US states and Canadian provinces)

The region field on US and Canadian addresses accepts full state or province names as well as the standard 2-letter codes.

What you sendResolved toNotes
"California""CA"As returned by Google Play buyerState
"New York""NY"
"District of Columbia" / "Washington DC""DC"
"British Columbia""BC"
"Ontario""ON"
"Québec" / "Quebec""QC"Accented forms accepted

B2B override

By default, Clearvo infers B2B treatment from a valid customer.taxId — no additional flag is needed for standard B2B checkouts. For cases where you know the buyer is a business but don't have their VAT number at checkout time, set customer.b2bOverride: true.

ScenarioWhat to send
B2C consumer saleOmit taxId and b2bOverride — B2C treatment is the default
B2B sale, VAT number knownSupply customer.taxId — Clearvo verifies it and applies reverse charge or zero-rating automatically
B2B sale, VAT number not availableSet customer.b2bOverride: true — skips VAT number validation and applies full B2B treatment for the buyer's location

Platform integration examples

These patterns work without any field transformation on your side.

Google Play — buyerCountry + buyerState direct
// Google Play Orders API returns buyerCountry (alpha-2) + buyerState (full name)
{
  "currency": "USD",
  "customer": {
    "billingAddress": {
      "country": "US",
      "region": "California"
    }
  },
  "lineItems": [{ "id": "1", "amount": 9.99, "productName": "App subscription" }]
}
// no taxId/b2bOverride → B2C treatment (the default); "California" → "CA" resolved before validation
Apple StoreKit — alpha-3 storefront code direct
// Apple StoreKit 2 returns storefront as ISO 3166-1 alpha-3
{
  "currency": "USD",
  "customer": {
    "billingAddress": { "country": "GBR" }
  },
  "lineItems": [{ "id": "1", "amount": 4.99, "productName": "In-app purchase" }]
}
// "GBR" → "GB" — UK VAT (20%) applied
Tax Calculations

Calculate

⚠
Effective 2026-09-12: commit defaults to true. An omitted commit is no longer a safe preview — it is recorded exactly like "commit": true.
  • The calculation is persisted to your audit trail, appears in your dashboard, and counts toward your calculation usage — confirmed synchronously by "committed": true in the response.
  • It contributes to your Compliance Radar nexus-threshold totals for its jurisdiction — this can only ever inflate the total, never deflate it, and can trigger a false APPROACHING/BREACH transition.
  • A transactionType: "credit_note" request with no relatedCalculationId now fails with 422 instead of previewing.
  • A supplied idempotencyKey is now honoured — a duplicate in-flight request gets 409, and a later request with the same key replays the stored response.
  • The full request is retained for the audit-retention period per the Privacy Policy; not deletable via the API (see Credit Notes & Refunds for the only reversal path).
Send "commit": false explicitly on any call that doesn't represent a real sale (quoting, cart estimates) to keep the old ephemeral behaviour.

Calculates tax for a single transaction. As of 2026-09-12, the result is recorded by default — persisted, counted toward your usage, and folded into Compliance Radar's threshold totals — unless you send "commit": false to calculate an ephemeral preview instead. See the alert above for the full list of what recording controls.

Endpoint

POST https://api.clearvo.io/v1/tax/calculate

Request body

Two request shapes below: a recorded calculation (the default — commit simply isn't sent) and a preview one (commit: false, for quoting). Both use the real request shape: supplier is optional (auto-filled from your entity's own registered address and VAT number when omitted), nested as supplier.billingAddress.country (seller/seller.address.country is a deprecated alias, see the field table below); the customer's address is customer.billingAddress / customer.shippingAddress (customer has no plain address field on this endpoint); ipAddress/binCountry live under a separate top-level evidence object, not on customer.

curl — recorded (commit omitted)
curl https://api.clearvo.io/v1/tax/calculate \
  -X POST \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "EUR",
    "idempotencyKey": "order-abc-123",
    "supplier": {
      "billingAddress": { "country": "IE" },
      "taxId": "IE1234567T"
    },
    "customer": {
      "taxId": "DE987654321",
      "billingAddress": {
        "country": "DE",
        "postalCode": "10115"
      }
    },
    "lineItems": [
      {
        "id": "line-1",
        "amount": 500.00,
        "quantity": 5,
        "productName": "Enterprise software licence",
        "taxCategory": "digital-services"
      },
      {
        "id": "line-2",
        "amount": 120.00,
        "quantity": 2,
        "productName": "Printed user manual"
      }
    ],
    "vatUnverifiableFallback": "conservative"
  }'
curl — preview (commit: false)
curl https://api.clearvo.io/v1/tax/calculate \
  -X POST \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "EUR",
    "commit": false,
    "customer": {
      "billingAddress": { "country": "DE" }
    },
    "lineItems": [
      { "id": "cart-line-1", "amount": 500.00, "productName": "Enterprise software licence" }
    ]
  }'

The preview response has the identical shape (same jurisdiction, taxTreatment, lineItems fields) but "committed": false — nothing is persisted, and it makes no contribution to your Compliance Radar totals. idempotencyKey has no effect on a preview call — it is only ever consulted when the resolved commit is true.

Full field-by-field shape, for reference (every field on the single-item schema — not every field would appear together on a real call):

JSON — full request shape
{
  "currency": "EUR",
  "reportingCurrency": "USD",
  "date": "2026-09-01",
  "commit": true,
  "idempotencyKey": "order-abc-123",
  "vatUnverifiableFallback": "conservative",
  "vatValidation": "full",
  "merchantRef": "order-abc-123",
  "paymentMethod": "card",
  "isMarketplaceFacilitatedSale": false,
  "transactionType": "invoice",
  "transactionDirection": "sale",
  "supplier": {
    "name": "Acme Ireland Ltd",
    "taxId": "IE1234567T",
    "billingAddress": { "country": "IE" }
  },
  "customer": {
    "name": "Beispiel GmbH",
    "taxId": "DE987654321",
    "b2bOverride": false,
    "exemptionRef": "cert_01j4...",
    "ref": "crm-cust-9001",
    "billingAddress": { "country": "DE", "postalCode": "10115" },
    "shippingAddress": { "country": "DE", "postalCode": "10117" }
  },
  "shipFrom": { "country": "IE" },
  "shipTo": { "country": "DE", "postalCode": "10117" },
  "incoterms": "DDP",
  "importerOfRecord": "SELLER",
  "evidence": {
    "ipAddress": "203.0.113.42",
    "binCountry": "DE"
  },
  "lineItems": [
    {
      "id": "line-1",
      "productName": "Enterprise software licence",
      "productDescription": "Annual subscription, 5 seats",
      "productCode": "SKU-1001",
      "taxCategory": "digital-services",
      "classificationCodes": [{ "system": "stripe", "code": "txcd_10103000" }],
      "amount": 500.00,
      "quantity": 5,
      "amountIncludesTax": false,
      "discount": 50.00,
      "discountAmount": 50.00,
      "exempt": false,
      "taxTreatmentOverride": "STANDARD"
    },
    {
      "id": "line-2",
      "productName": "Standard shipping",
      "taxCategory": "shipping_handling",
      "amount": 12.00,
      "shippingCarrier": "common",
      "chargeAvoidable": false,
      "actualCostOfShipment": true
    },
    {
      "id": "line-3",
      "productName": "Children's cotton t-shirt",
      "amount": 45.00,
      "commodityCode": "6109100010"
    }
  ]
}

Request fields

Transaction

FieldTypeRequiredDescription
currencystringYesISO 4217 currency code for all amounts in the request (e.g. "EUR", "USD", "GBP"). Exactly 3 characters. (Request-only — there is no top-level currency field on the response; see Response Reference.)
reportingCurrencystringNoISO 4217 currency code to additionally convert the calculation's totals into — e.g. your group reporting currency when currency is the transaction's local currency. Adds reportingCurrencyAmounts to the response (see Response Reference).
datestringNoISO 8601. Selects which rules, rates, and registrations were in force on that date — it does not affect which Compliance Radar period this calculation is windowed into. A committed calculation always counts toward the period containing its real receipt time (resolvedAt), regardless of a back-dated date — to load genuinely historical transactions into Compliance Radar's own threshold history, use its historical-import route, not a back-dated calculation call.
commitbooleanNoDefault: true (as of 2026-09-12). When the resolved value is true (sent explicitly, or omitted), the calculation is persisted to your audit trail, appears in the dashboard, counts toward your calculation usage, and contributes to Compliance Radar's nexus-threshold totals. A recorded calculation cannot be un-recorded — see Credit Notes & Refunds for the only reversal path (a credit note or refund, which nets its effect on totals but does not delete the row). Send "commit": false explicitly for a preview/quote that should not be recorded, billed, or counted toward any threshold.
idempotencyKeystringNoA unique key you provide (e.g. your order ID) — only ever consulted when the resolved commit is true; has no effect on a preview call. The first request for a given (account, key) pair wins the claim and runs the calculation; a concurrent duplicate while that first request is still in flight gets 409 idempotency_key_in_progress; once the first request completes, every later request with the same key replays its stored response verbatim, regardless of what request body it sends — there is no request-body comparison. Use a fresh key for a genuinely different transaction.
vatUnverifiableFallback"conservative" | "permissive"NoDefault: "conservative". Controls behaviour when VIES cannot verify a customer's VAT ID within the timeout. "conservative" treats the customer as B2C (applies standard local rate). "permissive" applies B2B reverse charge rules anyway and marks the result as degraded: true.
vatValidation"full" | "format" | "none"NoControls how a B2B customer's VAT ID is validated. No request-level default — an omitted value falls back to your account's own setting (Account Settings, itself "full" unless changed). "full" — live VIES lookup with a 300ms timeout. "format" — format check only, no network call; a structurally valid ID is trusted without a degraded flag. "none" — no validation at all; the VAT ID is trusted entirely.
merchantRefstringNoMerchant-of-Record reference to differentiate end customers, max 100 characters. Stored against the calculation and echoed back.
paymentMethod"card" | "bank_transfer" | "digital_wallet" | "cash" | "other"NoInformational only — echoed on the response, does not affect the calculation.
isMarketplaceFacilitatedSalebooleanNoSet when this whole order was sold through a marketplace facilitator (e.g. TikTok Shop, Amazon) that collects and remits tax on your behalf. Excludes the transaction from a Compliance Radar nexus-threshold rule's revenue sum in the US states that exclude marketplace-facilitated sales from the seller's own economic-nexus threshold. Does not change the calculated tax itself. Omit (or false) for a direct sale.
transactionType"invoice" | "credit_note"NoDefault: "invoice". Set to "credit_note" when issuing a credit note against a prior invoice. The engine negates all line item amounts before calculation. See Credit Notes & Refunds.
relatedCalculationIdstringConditionally requiredThe calculationId of the original invoice being reversed. Required when transactionType is "credit_note" and the resolved commit is true (the default, unless you send commit: false) — omitting both fields on a credit note now returns a 422. The referenced calculation must belong to the same entity. Maximum 50 characters.
transactionDirection"sale" | "purchase"NoDefault: "sale". Set to "purchase" for an incoming, input-tax transaction — see Purchase Mode below.
clientTaxCodestringNoYour own ERP tax code (e.g. a SAP two-digit code) for the whole invoice, forward-resolved against your own configured client tax codes and applied to every line lacking its own lineItems[].clientTaxCode/taxCode/taxTreatmentOverride. An unresolvable code rejects the whole calculation as one atomic 422.

Parties — seller, customer

FieldTypeRequiredDescription
customerobjectYes, for a "sale" — optional for a "purchase"The counterparty on a sale (required — 422 CUSTOMER_REQUIRED if missing). On a "purchase", this is instead your own entity: omit it to auto-fill from your entity's own master data, or supply it and its taxId must be one of the entity's own registered tax IDs, any country (422 CUSTOMER_TAX_ID_MISMATCH on a mismatch) — see Purchase Mode. This object has no plain address field on this endpoint — use billingAddress/shippingAddress below.
supplierobjectYes, for a "purchase" — optional for a "sale"The actual vendor on a purchase (required — 422 SUPPLIER_REQUIRED if missing; see Purchase Mode for its field meanings there). On a "sale", this is instead your own entity: omit it to auto-fill from your entity's own master data, or supply it and its taxId must be one of the entity's own registered tax IDs, any country (422 SUPPLIER_TAX_ID_MISMATCH on a mismatch).
supplier.billingAddress.countrystringYes, if supplier is sentISO 3166-1 alpha-2 country code of the supplier's address — on a purchase, the vendor's; on a sale, your own entity's registered address.
supplier.taxIdstringNoOn a purchase, the vendor's VAT number/tax ID — used to determine reverse-charge/import treatment. On a sale, validated against the entity's own registered tax IDs (see above).
supplier.namestringNoFree-text display name, informational only.
supplier.refstringNoPurchase only. Your own reference for this vendor (e.g. ERP vendor ID) — resolves saved supplier master data (POST/GET /suppliers), filling in name/taxId/billingAddress you omit here (explicit fields win). A ref that does not resolve to a saved supplier of this entity returns 422 SUPPLIER_REF_NOT_FOUND. Ignored when supplier is the entity side (a sale).
customer.billingAddress.countrystringYesISO 3166-1 alpha-2 country code of the customer's billing address — the primary jurisdiction signal for B2B and most B2C transactions.
customer.billingAddress.regionstringYes (US)State code (e.g. "CA", "TX"). Required whenever the resolved country is "US" — no country-level US rate exists. For Canada, optional; supply the province code for accurate provincial rate lookup.
customer.billingAddress.postalCodestringNoUsed for special-territory detection — e.g. Canary Islands postal codes (35xxx–38xxx) are treated as outside the EU VAT area despite an ES country code.
customer.shippingAddressobjectNoSame shape as billingAddress. Takes precedence over billingAddress for physical goods when both are supplied.
customer.taxIdstringNoCustomer's VAT number. When supplied, Clearvo verifies it against VIES and applies B2B treatment — reverse charge, intra-community zero-rating, or export zero-rating — based on the seller and buyer locations.
customer.b2bOverridebooleanNoWhen true, applies B2B treatment immediately without requiring a VAT number. Use when the buyer is a known business but their VAT ID is not available at request time.
customer.exemptionRefstringNoReference (certificate_ref) to an exemption certificate stored in Clearvo ECM. Applies the exemption to eligible line items when an active matching certificate exists.
customer.refstringNoYour own reference for this customer (e.g. CRM ID). Does not resolve any stored customer master data — it only auto-looks up active ECM exemption certificates for this customer and applies them to eligible US line items. Contrast supplier.ref above, which on a purchase does fill name/taxId/address from the Suppliers master.
sellerobjectNo — deprecatedDeprecated alias for supplier on a "sale" only ({ name?, taxId?, address: { country } }) — normalised to supplier before validation runs. Prefer supplier directly (above). Rejected with 422 SELLER_ALIAS_NOT_ALLOWED_FOR_PURCHASE on a "purchase".
ℹ
A type field on customer (e.g. "B2B") is not read by this endpoint — it is silently dropped during validation. Use b2bOverride and/or taxId above instead.

Addresses — shipFrom, shipTo, incoterms

FieldTypeRequiredDescription
shipFrom.countrystringNoPhysical dispatch country for goods, when different from supplier.billingAddress.country. Relevant to Art. 138 (intra-Community) vs. Art. 146 (export) classification for cross-border goods.
shipToobjectNoPhysical receiving address, same shape as shipFrom. Accepted, persisted, and echoed back — not yet used to determine the tax result for either sale or purchase. customer.shippingAddress remains the field jurisdiction resolution actually consumes for a sale.
incotermsstringNoIncoterms® 2020 rule for the shipment (e.g. "DDP", "EXW"). Does not gate IOSS eligibility: IOSS applies whenever ship-from is non-EU, destination is EU, and value is ≤€150, regardless of the incoterms supplied. Above that threshold, this determines who is liable for import VAT: omitting it, or supplying "DDP" (seller is importer of record), leaves the ordinary registration-based treatment unchanged. Any other value — EXW, FCA, FAS, FOB, CFR, CIF, CPT, CIP, DAP, DPU (buyer is importer of record) — makes that line outside the scope of Clearvo tax calculation (taxCode: "O", tax charged: 0). Never affects B2B transactions.
importerOfRecord"SELLER" | "BUYER"NoExplicit override for who is the importer of record. Accepted, persisted, and echoed back — contract-only in this release, not yet consumed by jurisdiction/treatment resolution.
shipFrom.customsStatus"free_circulation" | "bonded"NoRead only when duties are requested. "bonded" marks stock held under customs control, so leaving it is a customs crossing even within one territory. Also accepted on a per-line shipFrom.
insuranceobjectNo{ amount, currency? }. Consignment insurance, a customs-value component under CIF. Read only when duties are requested.
dutiesobjectNo{ include, collectDeposit?, defaultCountryOfOrigin?, ratePolicyForHs6? }. Estimated import duty, import VAT/GST and customs fees. include overrides the account dutiesEnabled setting either way. Never changes a tax figure. See Customs duties and landed cost and POST /v1/duties/quote.
ℹ
On every address object (seller.address, customer.billingAddress/shippingAddress, shipFrom, shipTo) only country, region, and postalCode affect tax determination. line1, line2, and city are stored for your own records and never influence jurisdiction resolution or rate lookup.

Evidence

FieldTypeRequiredDescription
evidence.ipAddressstringNoCustomer's IP address — a top-level object, not nested under customer. Used as a secondary jurisdiction signal for B2C digital services under the EU 2-of-3 evidence rule (CIR (EU) 282/2011 Art. 24b/24f).
evidence.binCountrystringNoISO country code derived from the customer's card BIN. Used as a third corroborating signal under the same 2-of-3 rule. Advanced/payment-processor use only.

Line items

FieldTypeRequiredDescription
lineItems[].idstringYesYour identifier for this line item. Echoed back in the response.
lineItems[].amountnumberYesLine item amount in the transaction currency. Whether this is tax-exclusive or tax-inclusive is resolved by lineItems[].amountIncludesTax if set, else the product's own configured pricing, else your account's defaultPriceIncludesTax setting (Account Settings) — exclusive by default for a US-resolved line regardless of your account setting, inclusive by default everywhere else.
lineItems[].quantitynumberNoDefault: 1. Informational only — amount should already be the total line amount, not a per-unit price.
lineItems[].productNamestringNoPlain-language product name, max 200 characters. Used to classify the product when no explicit taxCategory is supplied and none of the classificationCodes map. Not required — an omitted name with no other classification signal falls back to a generic goods category (see Product Classification).
lineItems[].productDescriptionstringNoAdditional descriptive text, max 500 characters, stored for your records only.
lineItems[].productCodestringNoYour own product/SKU code, max 40 characters, stored for your records and used to key the per-account classification cache.
lineItems[].amountIncludesTaxbooleanNoExplicitly marks this line's amount as tax-inclusive (true) or tax-exclusive (false), for every jurisdiction including the US. Omit to fall through to the account's defaultPriceIncludesTax setting.
lineItems[].taxCategorystringNoExplicit product category slug, skipping catalogue lookup entirely (e.g. "digital-services", "books"). See Product Classification for available slugs.
lineItems[].classificationCodesarrayNoUp to 5 outside product codes for this line, each { "system": "stripe" | "shopify" | "hs" | …, "code": "…" }. Tried in the order given; the first one that maps to a tax category is authoritative, ahead of the per-account product catalogue lookup. system is lowercase letters, digits or underscore (1–30 characters). See Product Classification for how the code map works.
lineItems[].taxCodestringNoEN16931 escape hatch — directly forces the output taxCode for this line. Rare; prefer taxTreatmentOverride or clientTaxCode.
lineItems[].discountnumberNoReduces the taxable base: taxableAmount = amount − discount.
lineItems[].discountAmountnumberNoAudit-trail/display only — stored verbatim, never affects taxableAmount/taxAmount. Distinct from discount above, which does reduce the taxable base.
lineItems[].exemptbooleanNoClaims exemption for this line. If no active ECM certificate matches, a PENDING_CERTIFICATE record is created (see pendingCertificates[] in Response Reference) — you remain liable until it's substantiated.
lineItems[].taxTreatmentOverridestringNoCaller-forced rate band — one of STANDARD, MIDDLE, REDUCED, SUPER_REDUCED, SPECIAL, ZERO, EXEMPT. A band only, never a raw percentage. Rejected (422) for a US-resolved line except ZERO/EXEMPT — see Commodity & Tariff Classification for the full precedence contract.
lineItems[].commodityCodestringNoTariff/customs classification code (HS/CN/UK Trade Tariff). Two uses: (1) may set this line's rate band, consulted only when taxTreatmentOverride is absent; (2) on an IOSS-treated calculation, drives the response's customsDuty object regardless of any override — lines sharing the same normalised classification count as one duty item. See Commodity & Tariff Classification.
lineItems[].commodityCodeScheme"HS6" | "CN8" | "TARIC10" | "UK10" | "HTS10"NoScheme of this line's commodityCode, read only when duties are requested. Omitted: inferred from the digit count and the destination. Not the rules-engine fact customProperties.commodityCodeScheme.
lineItems[].countryOfOriginstringNoISO 3166-1 alpha-2 country the goods were made in, read only when duties are requested. Never inferred from shipFrom.
lineItems[].weightobjectNo{ value, unit } (kg, g, lb, oz), per unit; the line weight is value × quantity. Read only when duties are requested.
lineItems[].clientTaxCodestringNoYour own ERP tax code for this line only — wins over the invoice-header clientTaxCode for this line specifically.
lineItems[].shippingCarrier"common" | "seller_vehicle"NoUS shipping_handling lines only, ignored elsewhere. Omitted resolves conservatively to "seller_vehicle" — there is deliberately no request-level default that would grant an unattested carrier-conditioned exemption.
lineItems[].chargeAvoidablebooleanNoUS shipping_handling lines only. Whether the customer could have avoided this shipping charge (e.g. in-store pickup). Omitted resolves conservatively to false.
lineItems[].actualCostOfShipmentbooleanNoUS shipping_handling lines only. Whether the charge passes through your actual shipment cost rather than a flat/marked-up fee. Omitted resolves conservatively to false.
ℹ
Request limits. The request body must be under 512KB (524288 bytes); a larger body is rejected with 413 and { "error": "Request body too large", "hint": "Body must be under 524288 bytes." }. Line item string fields are capped: lineItems[].id at 200 characters, taxCategory and taxCode at 100 characters each, and commodityCode at 40 characters.

Purchase Mode

Every example so far has been a sale — the entity is the supplier. Set "transactionDirection": "purchase" to calculate the input tax on something your entity bought instead — an AP/procurement invoice. Party roles invert: supplier becomes the required counterparty field — the actual vendor on the purchase invoice (same shape as customer) — and your entity moves to customer, which is now optional: omit it to auto-fill from your entity's own master data, or supply it and its taxId must be one of the entity's own registered tax IDs, any country (422 CUSTOMER_TAX_ID_MISMATCH on a mismatch — see Registrations). A supplier.ref that does not resolve to a saved supplier of this entity is rejected with 422 SUPPLIER_REF_NOT_FOUND — create it with POST /suppliers, or send the vendor's taxId/billingAddress directly. seller (the deprecated alias for supplier) has no meaning on a purchase and is rejected outright with 422 SELLER_ALIAS_NOT_ALLOWED_FOR_PURCHASE. The response's top-level entityRole tells you which response block (supplier or customer) is your own entity — "customer" for a purchase, "supplier" for a sale.

Purchase-direction calculations are recorded/metered exactly like a sale (the same commit default applies), but are excluded from your own selling-side Compliance Radar thresholds and auto-discovery — transactionDirection: 'sale' is what those queries filter on. A purchase resolving to a US jurisdiction is rejected as 422 us_jurisdiction_purchase_unsupported (no use-tax self-assessment computation exists yet); a genuinely cross-border GOODS purchase is not a whole-calculation rejection — it is computed per line instead, with the per-line import/intra-community-acquisition/own-goods treatment surfaced in lineItems[].movement/.outcome (see Response Reference).

curl — purchase mode
curl https://api.clearvo.io/v1/tax/calculate \
  -X POST \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "EUR",
    "transactionDirection": "purchase",
    "supplier": {
      "name": "Acme Cloud Services Ltd",
      "taxId": "DE123456789",
      "billingAddress": { "country": "DE" }
    },
    "lineItems": [
      {
        "id": "line-1",
        "productName": "Cloud hosting — September",
        "amount": 800.00,
        "supplyType": "SERVICES",
        "statedTaxAmount": 152.00
      },
      {
        "id": "line-2",
        "productName": "Office furniture",
        "amount": 300.00,
        "supplyType": "GOODS",
        "recoverablePercentOverride": 50
      }
    ]
  }'
FieldTypeRequiredDescription
transactionDirection"purchase"Yes, to enter purchase modePersisted verbatim. Also narrows which of your client_tax_codes rows can reverse-match this calculation's lines to direction purchase (or direction-less) rows only.
supplierobjectYes, for "purchase" (optional, entity-side, for "sale")The actual vendor on the purchase invoice — same shape as customer (name, taxId, billingAddress). taxId and billingAddress.country are the supplier's VAT ID and country; shippingAddress/b2bOverride/exemptionRef carry no meaning here and are ignored. ref resolves saved supplier master data (see GET/POST /suppliers), filling in any fields you omit here — an unresolvable ref returns 422 SUPPLIER_REF_NOT_FOUND. Unlike supplier.ref, customer.ref on a sale never resolves stored master data — it only auto-applies ECM exemption certificates.
lineItems[].supplyType"GOODS" | "SERVICES"No (but see note)Purchase-only; ignored for a sale line. A purchase line with no supplyType still returns a full computed treatment/rate, but is flagged lineItems[].outcome: { status: "UNKNOWN_TREATMENT", reasonCode: "missing_supply_type" } — see Response Reference.
lineItems[].recoverablePercentOverridenumber, 0–100NoPurchase-only; ignored for a sale line. Manual override of the recoverable share of this line's input tax (VAT Directive Art. 173–175 partial-exemption split) — a 0–100 percentage, not a 0–1 fraction. When supplied, wins over whatever recoverability your configured client tax code carries. Affects only the reported recoverable/blocked split, never the tax amount charged.
lineItems[].statedTaxAmountnumberNoPurchase-only; ignored for a sale line. The tax amount your supplier actually charged on their invoice, in decimal units. When supplied, Clearvo independently computes the legally-due tax and returns a verdict (MATCH/OVERCHARGED/UNDERCHARGED) rather than trusting the invoice at face value — see lineItems[].taxVerification in Response Reference. Skipped (no verdict computed, never defaulted to MATCH) for a genuine border-paid import line, where the real import-VAT base is customs value + duty + freight, not this line's invoice net.

Commodity & Tariff Classification

Two optional line-item fields — taxTreatmentOverride and commodityCode — sit above the ordinary rate-band resolution described in Tax Treatments. Neither changes any upstream B2B/reverse-charge/exemption decision, and neither changes how a line's taxCategory is classified — for rate-band purposes they only replace the tail, B2C-default rate-band step with a caller-supplied or tariff-derived one. commodityCode has one further effect outside rate-band resolution: on an IOSS-treated calculation it also drives the response's customsDuty item count, regardless of whether taxTreatmentOverride is also present on that line — see Response Reference.

ℹ
Precedence, highest first. (1) taxTreatmentOverride — a caller-forced band, honoured for the line's resolved jurisdiction (with one exception — see below). (2) commodityCode — consulted for rate-band purposes only when taxTreatmentOverride is absent, but always consulted for customsDuty item counting on an IOSS-treated line, regardless of taxTreatmentOverride. (3) The ordinary rate-band lookup described in Tax Treatments (explicit country/region rate research, then an EU/US/global scoped default, then the STANDARD floor).

Band-only, never a raw rate. taxTreatmentOverride takes one of STANDARD, MIDDLE, REDUCED, SUPER_REDUCED, SPECIAL, ZERO, or EXEMPT — the same closed set a naturally-resolved band can take. You are naming which band applies, not a percentage; Clearvo still resolves the actual rate for that band in the line's own jurisdiction. If no researched rate exists for that band in that jurisdiction, the line degrades the same way an unresolved band always does (see degraded / degradedReason in Response Reference) — this never invents a numeric rate.

ℹ
Not supported for US lines. STANDARD, MIDDLE, REDUCED, SUPER_REDUCED, and SPECIAL have no meaning for a line that resolves to a US jurisdiction — US sales tax is a single combined state/county/city/district percentage per address, not a VAT-style banded rate table. Sending one of these five values on a US line returns a 422 us_tax_treatment_override_unsupported error rather than silently applying the ordinary calculated rate. ZERO and EXEMPT are rate-independent and work identically for every jurisdiction, including the US.

Per-country code scoping. commodityCode keys into Clearvo's internal tariff-rate data the same way taxCategory keys into category rate data — that backing data is not itself a retrievable resource; there is no GET endpoint that exposes it. Codes are jurisdiction-scoped nomenclatures, never assumed shared across countries: GB is classified under the UK Trade Tariff, EU member states under CN/TARIC. There is no separate scheme field to set — HS, CN, and UK Trade Tariff codes all extend the same WCO Harmonized System numbering for their shared leading digits, so matching is driven entirely by the line's own resolved country/region, not by which nomenclature label the code happens to use. A code is looked up hierarchy-aware: its own digit precision is tried first, then progressively shorter standard prefixes (10 → 8 → 6 → 4 digits), each scoped to the line's own resolved country/region, taking the first level with a matching rate. A total miss at every level never invents a rate — the line re-enters the ordinary taxCategory/classification cascade exactly as if commodityCode had never been supplied.

The confidence signal is unaffected. classificationStatus and classificationConfidence on each response line item (see Response Reference) describe how Clearvo resolved the line's tax category — classification runs unconditionally, before jurisdiction and rate-band resolution. That is true even when taxTreatmentOverride or commodityCode later determined the line's actual rate band: a line can carry a high-confidence AI-classified category and still have its rate band overridden by a tariff code, or vice versa. Never infer anything about band provenance from classificationConfidence — check sourcingRationale and the applied rate/taxCode instead.

curl — commodity code override
curl https://api.clearvo.io/v1/tax/calculate \
  -X POST \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "GBP",
    "customer": {
      "billingAddress": { "country": "GB", "postalCode": "SW1A 1AA" }
    },
    "lineItems": [
      {
        "id": "line-1",
        "amount": 45.00,
        "quantity": 1,
        "productName": "Children'"'"'s cotton t-shirt",
        "commodityCode": "6109100010"
      },
      {
        "id": "line-2",
        "amount": 12.00,
        "quantity": 3,
        "productName": "Promotional sample — always zero-rated",
        "taxTreatmentOverride": "ZERO"
      }
    ]
  }'

Line 1 has no taxTreatmentOverride, so the UK Trade Tariff code resolves the rate band. Line 2's taxTreatmentOverride takes precedence over any commodityCode that line might also carry — it is always checked first.

Successful response

See the Quickstart for a full response example. For the complete field reference, see Response Reference.

Error responses

ℹ
422 vs 400. Clearvo uses 422 Unprocessable Entity for request bodies that are valid JSON but fail field-level validation (missing fields, wrong types, constraint violations). The error field in the response body identifies the failing field and reason — e.g. "customer: Required" or "merchantRef: String must contain at most 100 character(s)". Fix the field named in error to resolve it. 400 Bad Request is reserved for scope-level errors (e.g. using an account-scoped key on an entity-only endpoint).
HTTPWhen it occursHow to fix
422Missing required field (customer, lineItems, currency, or a line item id / amount)Check the error field — it names the failing path (e.g. "customer: Required"). Add the missing field.
422currency is not exactly 3 characters, or lineItems is an empty array or exceeds 100 itemsSupply a valid ISO 4217 currency code (e.g. "EUR") and between 1 and 100 line items. For larger carts, split into multiple calculate calls.
422merchantRef exceeds 100 characters, lineItems[].productCode exceeds 40, lineItems[].productName exceeds 200, or lineItems[].productDescription exceeds 500Truncate the field to its limit.
422customer.billingAddress.region (or shippingAddress.region) is missing when country is "US"Supply the state code (e.g. "CA", "TX"). No country-level US tax rate exists — state is mandatory for all US addresses.
400An account-scoped key was used without an X-Entity-Id headerAdd X-Entity-Id: <entityId> to identify the target entity, or use an entity-scoped key.
403The entity in X-Entity-Id does not belong to this accountCheck that the entity ID belongs to the account associated with this API key.
401The x-api-key header is missing or the key is unrecognisedInclude a valid x-api-key header on every request.
409idempotency_key_in_progress — a previous request with this idempotencyKey is still being processed (only reachable when the resolved commit is true)Retry shortly. There is no request-body comparison on replay: once the first request for a key completes, every later request with the same key replays its stored response verbatim, whatever body it sends. Use a fresh key for a genuinely different transaction.
422transactionType is "credit_note", the resolved commit is true (explicit or the default), but relatedCalculationId is absentSupply the calculationId of the original invoice in relatedCalculationId, or send "commit": false if you only meant to preview the credit note amount.
422relatedCalculationId is provided but the referenced calculation does not exist or belongs to a different entityVerify the ID — it must be a recorded calculation for this entity. Preview (uncommitted) calculations are not valid targets.
422The transaction resolves to an unsupported jurisdiction — error: "country_not_supported" with message "Tax calculations for {country} are not currently supported. Call GET /v1/tax/jurisdictions for the list of supported jurisdictions."Call GET /v1/tax/jurisdictions for live coverage. The response also includes a country field and a hint pointing to support@clearvo.io if you believe the country should be supported.
Tax Calculations

Credit Notes & Refunds

Clearvo supports two distinct patterns for reversing a committed transaction, chosen based on how your billing or ERP system models reversals.

Credit notes (B2B / ERP path)

ERP systems such as SAP, NetSuite, and Oracle issue a formal credit note document when a sale is reversed. Submit this as a new calculation with transactionType: "credit_note". Clearvo negates all line item amounts before the calculation pipeline runs — the same jurisdiction, treatment, and rate logic applies unchanged, but every amount comes out negative. This keeps the credit note fully audit-traceable as its own document with its own calculationId.

curl — credit note
curl https://api.clearvo.io/v1/tax/calculate \
  -X POST \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "EUR",
    "transactionType": "credit_note",
    "relatedCalculationId": "cl_calc_01j4...",
    "supplier": { "billingAddress": { "country": "IE" }, "taxId": "IE1234567T" },
    "customer": {
      "taxId": "DE987654321",
      "billingAddress": { "country": "DE" }
    },
    "lineItems": [
      {
        "id": "line-1",
        "amount": 500.00,
        "amountIncludesTax": false,
        "productName": "Enterprise software licence"
      }
    ]
  }'

The response is identical in shape to a regular calculation — all amounts are negative (e.g. taxableAmount: -500.00, taxAmount: -95.00). The response includes transactionType: "credit_note" and relatedCalculationId.

⚠
relatedCalculationId is required whenever a credit note is recorded. As of 2026-09-12 that includes a credit note where commit was simply omitted — the default is now true, so an omitted commit with no relatedCalculationId now fails with 422 where it previously succeeded as an unrecorded preview. Fix it either of two ways: supply relatedCalculationId (it must reference a previously recorded invoice calculation belonging to the same entity — a preview/uncommitted calculation cannot be referenced), or send "commit": false if you only meant to preview the credit note amount.

Refunds (B2C / billing system path)

B2C billing systems such as Stripe, Chargebee, and Recurly process a payment refund without issuing a separate credit note document. Use the refund endpoint to mark the original calculation as refunded.

Endpoint

POST https://api.clearvo.io/v1/tax/calculate/{id}/refund

curl — refund
curl https://api.clearvo.io/v1/tax/calculate/cl_calc_01j4.../refund \
  -X POST \
  -H "x-api-key: csk_live_..."
JSON Response
{
  "ok": true,
  "calculationId": "cl_calc_01j4...",
  "refundedAt": "2026-06-24T10:30:00.000Z"
}

Error responses

HTTPWhen it occursHow to fix
404The calculation does not exist or belongs to a different entityCheck that the ID is correct and the API key is for the entity that owns this calculation.
409The calculation has already been marked as refundedNo action needed — the refund was already processed. The response body includes the original refundedAt timestamp.
422The calculation is a sandbox calculationSandbox calculations cannot be marked as refunded — they never count toward usage or compliance thresholds in the first place.
422The calculation is a credit note (not an invoice)Credit notes cannot be refunded. If the credit note was issued in error, issue a correcting invoice instead.

Compliance threshold impact

Both a recorded credit note and a recorded original invoice are, themselves, recorded calculations — reversing one never deletes or un-records the other:

  • Credit notes — the negative amount nets out the original invoice's contribution to your Compliance Radar threshold total for that entity and country as soon as it's recorded.
  • Refunds — marking a calculation refunded does not durably reduce your Compliance Radar threshold total. It is applied immediately, but the total is periodically recalculated from every recorded calculation regardless of refund status, so a refunded amount counts toward the threshold again once that recalculation next runs. There is currently no way to permanently exclude a refunded calculation from your threshold total — a refund is the correct step for your own records, but do not rely on it to bring a threshold back below APPROACHING/BREACH.

Neither path deletes anything — there is no way to un-record a calculation once it has been persisted. If a calculation was recorded in error with no corresponding real sale, contact support@clearvo.io rather than attempting to work around it client-side.

Tax Calculations

Exemptions

Store tax exemption records for specific customers. Use these endpoints to log that a customer holds a valid exemption certificate for a jurisdiction and product category. To apply an exemption in a calculation, pass "taxCode": "E" on the relevant line items — the engine processes it as exempt. Automated certificate matching during calculation is on the roadmap.

Endpoints

MethodPathDescription
GET/v1/tax/exemptionsList all exemptions. Filter by country, customerTaxId, or taxCategorySlug.
POST/v1/tax/exemptionsCreate a new exemption.
GET/v1/tax/exemptions/{id}Retrieve a single exemption.
DELETE/v1/tax/exemptions/{id}Delete an exemption (e.g. when a certificate expires).
curl — create exemption
curl https://api.clearvo.io/v1/tax/exemptions \
  -X POST \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "country": "US",
    "region": "TX",
    "customerTaxId": "12-3456789",
    "taxCategorySlug": "software",
    "reason": "Non-profit exemption certificate",
    "certificateRef": "TX-NPO-2026-00123",
    "validFrom": "2026-01-01",
    "validTo": "2026-12-31"
  }'

Exemption fields

FieldTypeRequiredDescription
countrystringYesISO country code the exemption applies to.
regionstringNoState or province code. Leave null for a country-wide exemption.
customerTaxIdstringYesThe customer's tax ID that this exemption is issued to.
taxCategorySlugstringNoLimit the exemption to a specific product category. If null, the exemption applies to all categories in the jurisdiction.
reasonstringNoHuman-readable description for your records (e.g. "Non-profit exemption certificate").
certificateRefstringNoYour reference number for the exemption certificate. Stored for audit purposes.
validFromstringYesYYYY-MM-DD. Date from which the exemption is valid.
validTostringNoYYYY-MM-DD. Expiry date. If omitted, the exemption does not expire.
Tax Calculations

Tax Obligations

Track where your entity has a tax registration obligation — based on economic nexus thresholds, physical presence, or a manual override. The calculation engine uses obligation records to determine whether to apply seller-not-registered treatment.

Endpoints

MethodPathDescription
GET/v1/tax/obligationsList all obligations. Filter by country, registrationStatus, or obligationStatus.
GET/v1/tax/obligations/{id}Retrieve a single obligation.
PATCH/v1/tax/obligations/{id}Update registration status, registration number, or obligation status.
curl — update obligation
curl https://api.clearvo.io/v1/tax/obligations/oblig_01j4... \
  -X PATCH \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "registrationStatus": "REGISTERED",
    "registrationNumber": "DE987654321",
    "obligationStatus": "COMPLIANT"
  }'

Obligation fields

FieldTypeDescription
countrystringISO country code this obligation applies to.
regionstring | nullState or province. Null for country-level obligations.
registrationStatus"REGISTERED" | "PENDING" | "NOT_REGISTERED"Current registration status with the tax authority in this jurisdiction. Controls whether the engine applies local tax rates or seller-not-registered treatment.
registrationNumberstring | nullThe tax registration number issued by the authority. Set when registrationStatus is REGISTERED.
obligationStatus"COMPLIANT" | "MONITORING" | "ACTION_REQUIRED"Overall compliance posture. ACTION_REQUIRED surfaces in the dashboard as a priority item.
thresholdAmountnumber | nullThe economic nexus threshold for this jurisdiction (in local currency). Informational only — Clearvo does not automatically track sales against this threshold.
currentPeriodAmountnumber | nullYour current period sales amount in this jurisdiction. Update this via PATCH to keep the obligation status current.
estimatedExposurenumber | nullEstimated tax exposure if registered (computed field — not updateable directly).
Tax Calculations

Product Catalogue

/v1/tax/calculate never runs a live AI classification — see Product Classification for the deterministic lookup it uses instead. The Product Catalogue endpoints below are how you build that catalogue up front: classify products with AI assistance, inspect AI-assigned slugs, approve or correct them, and bulk-load classifications from an ERP export — so a real transaction resolves to a correct, reviewed category via productCode/productName lookup instead of falling through to your account default.

Endpoints

MethodPathDescription
GET/v1/tax/productsList classified products. Filter by status (APPROVED | NEEDS_REVIEW), entityId (account-scoped keys only), page, limit (max 100).
POST/v1/tax/productsClassify a single product. Providing taxCategory bypasses AI and saves immediately as APPROVED.
PATCH/v1/tax/products/{id}Approve or correct a classification. Accepts slug and status (APPROVED | NEEDS_REVIEW | MANUAL).
DELETE/v1/tax/products/{id}Soft-delete a product. Sets deleted_at and hides it from all reads — classification history is preserved. Use the restore endpoint to reactivate.
POST/v1/tax/products/{id}/restoreReactivate a soft-deleted product. Body: { "reason": "string" } (required — logged to audit trail).
POST/v1/tax/products/bulkClassify up to 500 products in one call. Accepts text/csv or application/json.
curl — classify a product
curl https://api.clearvo.io/v1/tax/products \
  -X POST \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "productCode": "PLAN-BIZ-ANNUAL",
    "productName": "Business plan — annual subscription",
    "productDescription": "Cloud software subscription, B2B"
  }'
curl — bulk classify (CSV)
curl https://api.clearvo.io/v1/tax/products/bulk \
  -X POST \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: text/csv" \
  --data-binary 'product_code,product_name,product_description,tax_category
PLAN-BIZ,Business plan,,saas_business
EBOOK-01,EU Tax Guide 2026,Comprehensive EU VAT reference,
CONSULT-HR,HR transformation workshop,Half-day onsite session,'

Classification fields

FieldTypeDescription
productCodestringYour SKU or ERP code. Used as the cache key — the same product is classified at most once per account.
productNamestringPlain-language product name, used for an exact-match catalogue lookup when productCode isn't supplied or doesn't match. Required unless productCode is supplied.
productDescriptionstringAdditional context for the classifier. Include when the product name alone is ambiguous.
taxCategorystringExplicit slug override (e.g. "saas_business", "ebooks"). Bypasses AI — confidence is set to 1.0 and status is APPROVED immediately.
classificationCodesarrayUp to 5 outside product codes, each { "system": "stripe" | "shopify" | "hs" | …, "code": "…" }. Tried in order; the first that maps to a tax category is used, the same way as on a calculation line. Ambiguous, unmapped or unknown codes are skipped.
statusAPPROVED | NEEDS_REVIEWAI confidence ≥ 0.85 → APPROVED automatically. Below 0.85 → NEEDS_REVIEW until approved via PATCH or the dashboard.
Tax Calculations

Tax Categories

The Tax Categories endpoint returns the complete, live taxonomy of product category slugs — the same set the engine uses to classify line items. Call this endpoint to discover valid values for taxCategory on calculate requests and defaultTaxCategorySlug in account settings.

ℹ
Slugs are an open string taxonomy — new categories are added non-breakingly as the engine is updated. Fetch this endpoint rather than hardcoding slug strings to ensure your integration handles new additions gracefully.

Endpoint

GET https://api.clearvo.io/v1/tax/categories

Accepts both entity-scoped and account-scoped API keys.

curl
curl https://api.clearvo.io/v1/tax/categories \
  -H "x-api-key: csk_live_..."
JSON Response
{
  "object": "list",
  "count": 48,
  "categories": [
    {
      "slug": "api_data_services",
      "name": "API Access / Data Feeds / Integrations",
      "defaultTaxCode": "S",
      "eligibleSchemes": ["STANDARD", "OSS_UNION", "OSS_NON_UNION", "SIMPLIFIED"]
    },
    {
      "slug": "ebooks",
      "name": "eBooks / Digital Publications",
      "defaultTaxCode": "AA",
      "eligibleSchemes": ["STANDARD", "OSS_UNION", "OSS_NON_UNION", "SIMPLIFIED"]
    },
    {
      "slug": "saas_business",
      "name": "SaaS / Cloud Software (Business Use)",
      "defaultTaxCode": "S",
      "eligibleSchemes": ["STANDARD", "OSS_UNION", "OSS_NON_UNION", "SIMPLIFIED"]
    },
    "..."
  ]
}

Response fields

FieldTypeDescription
slugstringThe category identifier to use in taxCategory on line items, or as defaultTaxCategorySlug in account settings.
namestringHuman-readable description of the category.
defaultTaxCodestringThe EN16931 tax code most commonly applied to this category (S = standard, AA = reduced, E = exempt). The actual code used in a calculation may differ depending on country-specific rules.
eligibleSchemesstring[]OSS/IOSS schemes this category qualifies for: STANDARD, OSS_UNION, OSS_NON_UNION, IOSS, SIMPLIFIED (e.g. Norway VOEC). Physical goods include IOSS; location-specific services include STANDARD only.
Tax Calculations

Supported Jurisdictions

GET /v1/tax/jurisdictions returns every jurisdiction the engine can calculate tax for, plus any announced jurisdictions that are not yet live. Use it to discover coverage programmatically — for example, to decide which countries to route through Clearvo, or to self-heal after a country_not_supported error. This endpoint returns coverage only — it never returns rates or rate bands.

curl
curl https://api.clearvo.io/v1/tax/jurisdictions \
  -H "x-api-key: csk_live_..."
JSON Response
{
  "object": "list",
  "jurisdictions": [
    { "country": "AE", "name": "United Arab Emirates", "comingSoon": false },
    { "country": "AL", "name": "Albania", "comingSoon": false },
    { "country": "AR", "name": "Argentina", "comingSoon": false }
  ],
  "count": 76
}

Truncated for readability — the live response lists all 76 jurisdictions in the jurisdictions array (the full set appears in the tables below).

An announced jurisdiction — one with a published go-live date that is not yet live — appears in the same array with comingSoon: true and an availableFrom date. There are none announced today, so the entry below is illustrative (how Qatar would appear if it enacted its VAT law):

JSON — illustrative announced entry
{ "country": "QA", "name": "Qatar", "comingSoon": true, "availableFrom": "2027-01-01" }

Response fields

FieldTypeDescription
objectstringAlways "list".
jurisdictions[]arrayOne entry per jurisdiction, sorted alphabetically by ISO country code. Unsupported jurisdictions are omitted entirely — absence from this list means POST /v1/tax/calculate returns 422 country_not_supported for that country.
jurisdictions[].countrystringISO 3166-1 alpha-2 country code (uppercase), e.g. "DE".
jurisdictions[].namestringEnglish display name of the jurisdiction, e.g. "Germany".
jurisdictions[].comingSoonbooleanfalse — live now: calculate accepts this jurisdiction. true — announced but not yet live: calculate still returns 422 country_not_supported until the availableFrom date, at which point the jurisdiction normally goes live automatically. If a go-live is postponed, the entry simply remains comingSoon: true — treat this field, not the date, as authoritative for whether calculate accepts the jurisdiction.
jurisdictions[].availableFromstringPresent only when comingSoon is true — the announced go-live date (YYYY-MM-DD). Omitted entirely on live jurisdictions.
countintegerTotal number of entries in jurisdictions (live plus coming soon).

Freshness and caching

This endpoint reads coverage live on every request, but the calculation pipeline caches coverage for up to 30 minutes. Two consequences:

  • A jurisdiction newly appearing in this list can take up to 30 minutes to be accepted by POST /v1/tax/calculate.
  • A comingSoon jurisdiction whose availableFrom date arrives normally goes live in calculate immediately — the go-live date is evaluated per call, not cached. If the go-live is postponed, this list keeps reporting comingSoon: true and calculate keeps returning 422 — the two never disagree.

Recommended client pattern: cache this list on your side and refresh it every 30–60 minutes, and re-fetch it whenever you receive a 422 country_not_supported — the error means your cached view of coverage is stale or the country is genuinely unsupported.

Unsupported jurisdiction error

When a calculation resolves to a jurisdiction not in this list, POST /v1/tax/calculate returns 422 rather than guessing a rate:

JSON Response — 422
{
  "error": "country_not_supported",
  "country": "QA",
  "message": "Tax calculations for QA are not currently supported. Call GET /v1/tax/jurisdictions for the list of supported jurisdictions.",
  "hint": "If you believe this country should be supported, contact support@clearvo.io."
}

Supported countries

The 76 jurisdictions live today, grouped by region. These tables are generated from the same coverage data the endpoint serves — the endpoint remains the live source of truth between site deploys. Qatar is deliberately not supported: its 2016 VAT framework law has never been enacted, so there is no live Qatar VAT to calculate.

European Union (27)

CodeCountryCodeCountryCodeCountry
ATAustriaFIFinlandLVLatvia
BEBelgiumFRFranceMTMalta
BGBulgariaGRGreeceNLNetherlands
CYCyprusHRCroatiaPLPoland
CZCzechiaHUHungaryPTPortugal
DEGermanyIEIrelandRORomania
DKDenmarkITItalySESweden
EEEstoniaLTLithuaniaSISlovenia
ESSpainLULuxembourgSKSlovakia

Rest of Europe (11)

CodeCountryCodeCountryCodeCountry
ALAlbaniaISIcelandRSSerbia
BABosnia & HerzegovinaMEMontenegroTRTürkiye
CHSwitzerlandMKNorth MacedoniaUAUkraine
GBUnited KingdomNONorway

Americas (10)

CodeCountryCodeCountryCodeCountry
ARArgentinaCOColombiaUSUnited States
BRBrazilECEcuadorUYUruguay
CACanadaMXMexico
CLChilePEPeru

Asia-Pacific (15)

CodeCountryCodeCountryCodeCountry
AUAustraliaINIndiaPHPhilippines
BDBangladeshJPJapanSGSingapore
CNChinaKRSouth KoreaTHThailand
HKHong Kong SAR ChinaMYMalaysiaTWTaiwan
IDIndonesiaNZNew ZealandVNVietnam

Middle East (7)

CodeCountryCodeCountryCodeCountry
AEUnited Arab EmiratesJOJordanSASaudi Arabia
BHBahrainKWKuwait
ILIsraelOMOman

Africa (6)

CodeCountryCodeCountryCodeCountry
EGEgyptKEKenyaNGNigeria
GHGhanaMAMoroccoZASouth Africa
Tax Calculations

Registrations

Registrations tell the calculation engine where your entity is authorised to collect tax. A jurisdiction with no registration record defaults to REGISTERED (tax collected). Set a jurisdiction to NOT_REGISTERED and the engine returns tax code O (outside scope, 0%) for all transactions there. Adding an IOSS number enables IOSS treatment for qualifying EU B2C shipments ≤ €150.

Endpoints

MethodPathDescription
GET/v1/tax/registrationsList registrations for the entity. Account-scoped keys must supply ?entityId=. Returns collectionStatus, collectFromDate, and canCollectTax on each registration when Tax Calculations is enabled.
POST/v1/tax/registrationsCreate a registration. Account-scoped keys must include X-Entity-Id.
PATCH/v1/tax/registrations/{id}Set the collection start date for a registration. Pass collectFromDate: null to collect immediately, or an ISO date string to defer. Returns the updated collectionStatus.
DELETE/v1/tax/registrations/{id}Delete a tax number registration. Account-scoped keys must include X-Entity-Id.

Collection status

When Tax Calculations is enabled for your account, each registration carries a collectionStatus that controls whether Clearvo applies tax for that jurisdiction.

StatusMeaning
COLLECTINGTax is being collected — collectFromDate is set to today or a past date.
DEFERREDCollection starts on a future collectFromDate. No tax collected until that date.
SETUP_NEEDEDTax Calculations is enabled but no collectFromDate has been set. Set one via PATCH /v1/tax/registrations/{id}.
nullTax Calculations is not enabled for this account.

The list response also includes two top-level flags: taxCalcEnabled (boolean — whether the product is active) and taxCalcSetupRequired (boolean — true when any registration is in SETUP_NEEDED state).

curl — add a VAT registration
curl https://api.clearvo.io/v1/tax/registrations \
  -X POST \
  -H "x-api-key: csk_live_ent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "VAT",
    "country": "DE",
    "taxNumber": "DE123456789"
  }'
curl — set collection date
# Start collecting immediately
curl https://api.clearvo.io/v1/tax/registrations/<id> \
  -X PATCH \
  -H "x-api-key: csk_live_ent_..." \
  -H "Content-Type: application/json" \
  -d '{ "collectFromDate": null }'

# Defer to a future date
curl https://api.clearvo.io/v1/tax/registrations/<id> \
  -X PATCH \
  -H "x-api-key: csk_live_ent_..." \
  -H "Content-Type: application/json" \
  -d '{ "collectFromDate": "2026-07-01" }'

Registration types

TypeDescriptionEngine effect
VATStandard national VAT registrationSets obligation to REGISTERED; tax collected at local rate
IOSSEU Import One-Stop Shop numberWrites registration_number on a tax_registrations row with scheme=IOSS; enables IOSS treatment for eligible B2C goods ≤ €150 shipped into the EU
UNION_OSSEU One-Stop Shop (Union scheme)Sets obligation to REGISTERED with OSS scheme flag
NON_UNION_OSSEU One-Stop Shop (Non-Union scheme)Sets obligation to REGISTERED with OSS scheme flag
Tax Calculations

Account Settings

Account-level defaults that apply to every calculation made with your API key. All three settings can be overridden per-request — the cascade is: request field → product cache → account setting → hardcoded default.

Endpoints

MethodPathDescription
GET/v1/tax/settingsRead current account settings. Returns defaults if no settings row has been written yet.
PATCH/v1/tax/settingsUpdate one or more settings. Partial updates — omitted fields are unchanged.

New accounts default to defaultPriceIncludesTax: true for every jurisdiction other than the US, matching common non-US B2C convention — a US-resolved line always stays exclusive regardless of this setting. The example below explicitly opts a non-US account out of that default (tax-exclusive prices, added on top).

curl — update settings
curl https://api.clearvo.io/v1/tax/settings \
  -X PATCH \
  -H "x-api-key: csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "vatValidationMode": "format",
    "vatUnverifiableTreatment": "consumer",
    "defaultPriceIncludesTax": false,
    "defaultTaxCategorySlug": "saas_business"
  }'

Settings fields

FieldTypeDefaultDescription
vatValidationMode"full" | "format" | "none""full"Account-level default for VAT ID validation. Overridden per-request by the vatValidation request field. "full" — live VIES lookup (default). "format" — format check only, no VIES call. "none" — no validation; all VAT IDs trusted.
vatUnverifiableTreatment"consumer" | "business""consumer"How to treat B2B transactions when the customer's VAT ID cannot be verified (only applies when vatValidationMode is "full"). "consumer" charges tax at B2C rates (conservative). "business" applies reverse charge and sets degraded: true.
defaultPriceIncludesTaxbooleantrue (non-US) / n/a for USWhether line item amounts already include tax by default, for every jurisdiction other than the US — defaults to true (inclusive), matching common non-US B2C convention. Always ignored for a US-resolved line, which is always treated as exclusive at this account-wide tier regardless of this value — US retail convention adds tax at checkout rather than folding it into the sticker price. A line item's own amountIncludesTax field is honoured for every jurisdiction including the US (e.g. a fixed-price digital subscription or a marketplace absorbing tax).
defaultTaxCategorySlugstring | nullnullFallback product category slug when no productCode/productName catalogue match is found. If null, the engine applies the standard physical-goods category.
usAddressPrecision"rooftop" | "zip""rooftop"How US sales tax jurisdiction is resolved. See Jurisdiction Resolution below.
placeOfBusinessAddressobject | nullnullWhere your business is physically established — distinct from the per-transaction seller address sent to POST /v1/tax/calculate. Changing this can change which jurisdiction's rate applies to your intrastate US sales: Texas, California, and Illinois source some sales to the seller's own location rather than the buyer's. Review recent tax calculations after changing this field. Sub-fields: line1, line2, city, region, postalCode, country (2 or 3 letter code) — each independently nullable to clear just that field, or pass null for the whole object to clear the entire address.
isMarketplaceFacilitatorbooleanfalseWhether the account is a marketplace facilitator (a platform that collects and remits tax on behalf of third-party sellers) rather than a direct seller. Currently only affects sourcing determination for Illinois — no other jurisdiction is affected by this flag today.
availableTaxCategoriesarray—Read-only. Full list of active category slugs and names available in your account (GET only).
Tax Calculations

Response Reference

Full field reference for the TaxCalculateResponse object returned by the calculation endpoint.

Top-level fields

FieldTypeDescription
calculationIdstringUnique ID for this calculation (cl_calc_* prefix, cl_calc_test_* in sandbox). Present whether the calculation was recorded or previewed.
entityIdstringThe entity (fiscal identity) that processed this calculation. Confirms which entity-scoped API key was used — useful for Merchant of Record setups where multiple entities share an account.
apiVersionstringAlways "v1" today. The X-Clearvo-Api-Version request header, if you sent one, is echoed back as a response header (not in this body field) — no request behaviour currently branches on it.
resolvedAtstringISO 8601 timestamp of when this calculation actually ran. For a recorded calculation, this — not the request's own optional date field — is what windows it into a Compliance Radar threshold period.
sandboxbooleanTrue when the request was made with a csk_test_* key. A sandbox calculation never counts toward usage or Compliance Radar totals, whatever committed below is.
committedbooleanWhether the resolved commit was true for this calculation — true for a recorded calculation, false for a preview. Since commit defaults to true (2026-09-12), this is the field to check if you need to confirm synchronously whether a given call was actually persisted, without inferring it from whether you happened to send commit yourself.
merchantRefstringEchoes the request's merchantRef, when supplied.
paymentMethodstringEchoes the request's paymentMethod, when supplied.
incotermsstringEchoes the request's incoterms, when supplied.
taxTreatmentstringThe tax treatment applied to the transaction as a whole. See Tax Treatments for all possible values and their meanings. The effective rate and rate band are returned per line item (lineItems[].rate below), not here — a single calculation can span multiple bands when its lines fall into different categories.
taxCodestringEN16931 tax code for the transaction as a whole: S, AA, AE, E, K, G, Z, or O.
transactionType"invoice" | "credit_note"Echoed from the request (defaults to "invoice"). Credit note responses have negative amounts on all line items and totals.
relatedCalculationIdstring | undefinedThe calculationId of the original invoice this credit note reverses. Only present when transactionType is "credit_note" and the field was provided in the request.
refundedAtstring | undefinedISO 8601 timestamp. Set when POST /v1/tax/calculate/{id}/refund has been called. Absent for non-refunded calculations.
totalTaxnumber | undefinedTop-level convenience alias for summary.totalTax below.
degradedboolean | undefinedTrue when the engine encountered a partial failure (e.g. rate service timeout, VIES unreachable). The calculation still succeeds but a fallback rate may have been used.
degradedReasonstring | undefinedHuman-readable explanation of why the result is degraded. Only present when degraded: true.
staleCacheReasonstring | undefinedPresent only when degraded was caused specifically by a rate-service outage papered over with a static backup rate rather than left at 0% — a machine-readable sub-cause of degradedReason (e.g. "cch_unavailable_using_state_fallback").
reportingCurrencyAmountsobject | undefinedPresent only when the request supplied reportingCurrency: { currency, totalTax, totalAmountWithTax, fxRate, fxRateAsOf } — the same totals converted into that currency.
jurisdictionBreakdownarray | undefinedPresent only when the resolved jurisdiction has a multi-level tax structure (e.g. a US state + county + city + district stack). Each entry: { name, level, rate, taxableAmount, taxAmount }.
pendingCertificatesarray | undefinedPresent only when an inline lineItems[].exempt claim had no matching active ECM certificate. Each entry: { certId, certRef, ecmUrl } — see Exemptions.
lineResultsarrayAlias for lineItems below — identical data, both fields are always present together.

jurisdiction

FieldTypeDescription
jurisdiction.countrystringISO country code of the resolved tax jurisdiction.
jurisdiction.regionstring | undefinedState or province of the resolved jurisdiction. Present for US/Canadian transactions where sub-national rates apply.
jurisdiction.resolutionMethodstringHow the jurisdiction was determined: SHIPPING_ADDRESS, BILLING_ADDRESS, IP_GEOLOCATION, BIN_COUNTRY, VAT_ID, or SELLER_ADDRESS (B2C general services sourced to the seller's own country — see the Changelog).
jurisdiction.resolutionPrecisionstringConfidence in the resolved jurisdiction: EXACT, POSTAL, REGION, or COUNTRY.
jurisdiction.addressPrecisionstring | undefinedUS transactions only. The address resolution tier used to look up the rate: ROOFTOP (street address resolved to a zip+4 code), ZIP (postal code only), or STATE (region only).
jurisdiction.sourcingMethod"ORIGIN" | "DESTINATION" | undefinedUS TX/CA/IL only. Which address local tax was sourced to. Absent for every other state/country, which always destination-source.
jurisdiction.sourcingAddressstring | undefinedCompanion to sourcingMethod — which address it resolved to (SELLER_PLACE_OF_BUSINESS or BUYER_ADDRESS).
jurisdiction.sourcingCitationstring | undefinedStatute/regulation citation backing sourcingMethod.
jurisdiction.localityOverrideobject | undefinedUS only — present only when a self-administered ("home-rule") locality's own rate row (not the state default) was applied, e.g. Boulder, CO: { locality, region, taxAuthorityId, reason }.
jurisdiction.conflictsarrayNon-empty when multiple jurisdiction signals disagree (e.g. billing address says DE, IP geolocation says FR). Each entry describes the conflicting signal and its country. The winning signal is indicated by resolutionMethod.
ℹ
Rate format — decimal fractions throughout. Every rate, standardRate, reducedRate, and slugRate field across all Tax Calculation endpoints uses a decimal fraction: 0.19 means 19%, 0.07 means 7%, 0 means zero-rated. Never multiply or divide rates received from the API — use them directly in tax calculations (taxAmount = taxableAmount × rate).

supplier / customer / entityRole (response)

Present on every calculation — distinct from the request's own supplier/customer input objects. entityRole ("supplier" for a "sale", "customer" for a "purchase") tells you which of the two blocks below is your own entity; the other is the counterparty. Only the counterparty block carries the B2B/B2C validation fields.

FieldTypeDescription
entityRole"supplier" | "customer"Which party block is your own entity.
supplier.countrystring | undefinedThis party's resolved country.
supplier.regionstring | undefinedState/region code, when resolved (e.g. US).
supplier.postalCodestring | undefined—
supplier.taxIdstring | undefinedThis party's tax/VAT ID, when known.
supplier.isEntitybooleantrue when this block is your own entity; false when it is the counterparty.
supplier.b2bOverrideboolean | undefinedCounterparty block only (a purchase). Echoes the request's supplier.b2bOverride.
supplier.taxIdValidatedboolean | undefinedCounterparty block only (a purchase). Whether the supplied supplier.taxId was successfully verified.
supplier.taxIdValidationStatusstring | undefinedCounterparty block only (a purchase). More detail on the validation outcome.
customer.countrystring | undefinedThis party's resolved country.
customer.regionstring | undefinedState/region code, when resolved (e.g. US).
customer.postalCodestring | undefined—
customer.taxIdstring | undefinedThis party's tax/VAT ID, when known.
customer.isEntitybooleantrue when this block is your own entity; false when it is the counterparty.
customer.b2bOverrideboolean | undefinedCounterparty block only (a sale). Echoes the request's customer.b2bOverride.
customer.taxIdValidatedboolean | undefinedCounterparty block only (a sale). Whether the supplied customer.taxId was successfully verified (VIES, or format-only under vatValidation: "format").
customer.taxIdValidationStatusstring | undefinedCounterparty block only (a sale). More detail on the validation outcome (e.g. why an ID could not be verified).

sellerRegistration

Whether and how your entity is registered to collect tax in the resolved jurisdiction. Also present per-line as lineItems[].sellerRegistration for a request spanning categories with different registration coverage.

FieldTypeDescription
sellerRegistration.status"REGISTERED" | "PENDING" | "NOT_REGISTERED" | "MONITORING"Registration state for this jurisdiction.
sellerRegistration.canCollectTaxbooleanWhether tax was actually collected on this calculation.
sellerRegistration.pricingModel"TAX_EXCLUSIVE" | "TAX_INCLUSIVE" | "TAX_INCLUSIVE_PENDING"Your configured pricing model for this registration.
sellerRegistration.registrationNumberstring | undefinedThe registration/VAT number on file, when one exists.
sellerRegistration.schemestring | undefinedThe registration scheme matched, e.g. "OSS_UNION", "SIMPLIFIED".
sellerRegistration.matchLevel"country" | "region" | "locality" | undefinedSpecificity of the registration row that matched (relevant for CA federal/provincial splits).
sellerRegistration.reasonstring | undefinedFree-form, customer-safe explanation of why canCollectTax is false despite a registration existing. Never machine-parseable — use reasonCode instead.
sellerRegistration.reasonCode"program-covered" | "registration-needed" | "not-obligated" | undefinedClosed, machine-readable classification, present only on a NOT_REGISTERED/$0 outcome.
sellerRegistration.requiresRegistrationToCollectbooleanWhether the destination jurisdiction itself requires seller registration to collect tax at all — a fact about the destination, independent of your own registration status there.

ioss (when applicable)

FieldTypeDescription
ioss.numberstringThe IOSS registration number used for this transaction.
ioss.registrationCountrystringThe EU member state where the IOSS number is registered.
ioss.totalGoodsValuenumberNominal total value of goods in the transaction currency (used to determine the ≤€150 threshold eligibility).
ioss.currencystringCurrency of the totalGoodsValue.

customsDuty (when applicable)

Present only on an IOSS-treated calculation for a low-value consignment, for the EU's temporary (2026-07-01 to 2028-06-30) per-item customs duty. This is an estimate of the duty that will be assessed when the consignment is declared for import — owed by the IOSS holder or their indirect customs representative, not VAT, not collected from the buyer, and never included in totalTax/totalAmountWithTax. It is counted per distinct tariff classification (6-digit level), assuming the order ships as a single consignment; the actual figure may differ if the carrier lodges a more detailed declaration, splits the order into several parcels, or the declaration is accepted outside the measure's window. It is not refunded when goods are returned — a credit note instead carries customsDutyNote: "NOT_REVERSED_ON_CREDIT_NOTE" and omits customsDuty. If you choose to recharge this amount to the buyer at the time of sale, that recharge becomes part of the VAT-taxable amount of the sale itself — the recharge is VAT-able, this field is not. When you also request duties (duties.include), the same €3 appears in the consignment's duty (source fee_rule): customsDuty is that one fee and consignments[] is the whole landed-cost estimate, so never add the two together. See Customs duties and landed cost.

FieldTypeDescription
customsDuty.feeCodestringAlways "EU_LOW_VALUE_CONSIGNMENT_CUSTOMS_DUTY" today.
customsDuty.currencystringAlways "EUR" — the duty is never converted into the calculation's own currency.
customsDuty.amountnumberTotal duty owed this calculation — perItemAmount × itemCount.
customsDuty.perItemAmountnumberThe per-distinct-classification rate the rule charges (currently €3.00).
customsDuty.itemCountintegerDistinct qualifying classification groups counted this calculation.
customsDuty.classificationDigitsintegerTariff-code digit-length used to group lines (6 = HS6/H7).
customsDuty.itemsarrayOne entry per distinct classification code, plus one per missing/malformed-code line. Each entry: { classificationCode, lineIds }.
customsDuty.linesMissingCommodityCodestring[] | undefinedlineIds of qualifying lines whose commodityCode was missing or unusable — each still counts as its own item, never silently dropped.
customsDuty.includedInTotalsfalseAlways false — never folded into totalTax/totalAmountWithTax.
customsDuty.payableBy"DECLARANT"Always "DECLARANT" — the IOSS holder or their indirect customs representative, never the buyer.
customsDuty.estimatetrueAlways true — customs assesses the actual debt on release; this is a forecast.
customsDuty.effectiveFromstringISO date — the resolved fee rule's own effective-from date.
customsDutyNote"NOT_REVERSED_ON_CREDIT_NOTE" | undefinedPresent instead of customsDuty on a credit note whose original invoice would otherwise have owed one — the duty is never reversed on a return.

Three IOSS goods lines, two sharing one tariff classification and one with no usable commodityCode — two duty items, €6.00 total:

JSON Response — IOSS with customsDuty
{
  "calculationId": "cl_calc_01j4...",
  /* … full TaxCalculateResponse … */
  "ioss": {
    "number": "IM2760000123",
    "registrationCountry": "IE",
    "totalGoodsValue": 90.00,
    "currency": "EUR"
  },
  "customsDuty": {
    "feeCode": "EU_LOW_VALUE_CONSIGNMENT_CUSTOMS_DUTY",
    "currency": "EUR",
    "amount": 6.00,
    "perItemAmount": 3.00,
    "itemCount": 2,
    "classificationDigits": 6,
    "items": [
      { "classificationCode": "610910", "lineIds": ["line-1", "line-2"] },
      { "classificationCode": null, "lineIds": ["line-3"] }
    ],
    "linesMissingCommodityCode": ["line-3"],
    "includedInTotals": false,
    "payableBy": "DECLARANT",
    "estimate": true,
    "effectiveFrom": "2026-07-01"
  },
  "totalTax": 17.10,
  /* customsDuty.amount (€6.00) is never folded into totalTax/totalAmountWithTax */
  "summary": { "totalAmount": 90.00, "totalDiscount": 0.00, "totalTax": 17.10, "totalAmountWithTax": 107.10 }
}

The same invoice, credited — the duty is not reversed:

JSON Response — credit note
{
  "calculationId": "cl_calc_01j5...",
  "transactionType": "credit_note",
  /* … full TaxCalculateResponse … */
  "customsDutyNote": "NOT_REVERSED_ON_CREDIT_NOTE"
  /* no "customsDuty" key on a credit note — the €6.00 already assessed on the original invoice is not refunded */
}

summary

FieldTypeDescription
summary.totalAmountnumberSum of all taxableAmount values across line items (tax-exclusive).
summary.totalDiscountnumberSum of all discounts applied across line items.
summary.totalTaxnumberSum of all taxAmount values across line items. Also aliased at the top level as totalTax. Never includes customsDuty.amount — the EU per-item customs duty estimate, when present, is not included in this total.
summary.totalAmountWithTaxnumberSum of all totalAmount values across line items (totalAmount + totalTax). Never includes customsDuty.amount — see customsDuty above.
summary.retailDeliveryFeesarray | undefinedPresent only when a state retail-delivery fee applies (CO, MN) — flat per-delivery fees already included in totalTax/totalAmountWithTax. Each entry: { state, name, amount }.

lineItems[]

FieldTypeDescription
idstringYour line item ID, echoed from the request.
taxCodestringEN16931 tax code for this line item.
ratenumberEffective tax rate for this line item as a decimal fraction (0.19 = 19%, 0 = zero-rated).
taxableAmountnumberAmount on which tax is calculated (always tax-exclusive, regardless of the seller's pricing model).
taxableBasisnumber | undefinedPresent (< 1) only when a statute taxes just a fixed share of the charge (e.g. Texas information services, 20% exempt). rate is already the effective rate, so taxAmount = taxableAmount × rate still holds.
taxAmountnumberTax amount for this line item.
totalAmountnumberTotal including tax (taxableAmount + taxAmount).
taxCategorystringProduct category slug resolved for this line item (e.g. digital_services).
classificationStatus"APPROVED" | "NEEDS_REVIEW" | "MANUAL" | "PENDING" | undefinedAPPROVED — used with confidence; NEEDS_REVIEW — low confidence, recommend manual override; MANUAL — explicit taxCategory supplied; PENDING — classification failed, fallback used.
classificationConfidencenumber | undefinedConfidence score (0–1) carried over from when this catalogue entry was originally classified. 1.0 when an explicit taxCategory was provided on this call.
classificationFallbacktrue | undefinedPresent and true only when the category was resolved via the account default (no productCode/productName catalogue match found). Omitted otherwise.
classificationSource"EXPLICIT" | "CODE_MAP" | "CACHED" | "CACHED_BY_NAME" | "DEFAULT" | undefinedHOW this line's tax category was resolved this call — see Product Classification. No live AI classification ever occurs on this endpoint. Always populated alongside the other classification* fields, even when taxTreatmentOverride/commodityCode later determined the actual rate band.
classificationSystemstring | undefinedPresent only when classificationSource is "CODE_MAP": which code system decided this line (stripe, shopify, hs, …).
classificationMatchedCodestring | undefinedPresent only when classificationSource is "CODE_MAP": the code in the map that actually decided. It can be a parent of the code you sent, for example a Shopify category that fell back to its nearest mapped parent.
sourcingRationale.rateBandstringThis line's resolved rate band (STANDARD/MIDDLE/REDUCED/SUPER_REDUCED/SPECIAL/ZERO/EXEMPT/etc.). Always present.
sourcingRationale.bandTierstringWhich resolution tier decided rateBand above — OVERRIDE confirms taxTreatmentOverride was honoured, COMMODITY_CODE confirms commodityCode matched a row; any other value means the band came from the ordinary taxCategory-driven chain instead.
jurisdictionobject | undefinedThis line's own resolved jurisdiction — same shape as the top-level jurisdiction object above. Present on every non-degraded line.
placeOfSupplyRule"GOODS" | "DIGITAL_SERVICE" | "GENERAL_SERVICE" | "UNSUPPORTED" | undefinedThis line's classified place-of-supply regime.
movement"local" | "intra_community" | "export" | "distance_sale" | "import" | "own_goods_movement" | undefinedThis line's EU-VAT-Directive movement classification. Present whenever the line reached full derivation, for both sale and purchase.
exemptionobject | undefinedPresent when an exemption certificate was applied: { certificateId, certificateRef, certificateType, effectiveTo }.
clientTaxCodestring | undefinedYour own ERP tax code for this line — either an echo of a resolved input (lineItems[].clientTaxCode/header clientTaxCode), or a reverse lookup handing back your own code for this line's resolved treatment when no input was supplied. Absent when no client tax code matches, or more than one does (an allowed but ambiguous duplicate).
outcomeobject | undefinedThe headline per-line resolution verdict — check this first. { status, reasonCode?, label?, nextStep? }. status is "RESOLVED" (complete, the default), "NO_TAX_CODE" (treatment fully determined but no client tax code matched — informational only, not an error), or "UNKNOWN_TREATMENT" (a required input signal is missing, e.g. purchase-mode missing_supply_type — always carries a reasonCode, label, and nextStep). Never causes a request-level 4xx.
recoverabilityobject | undefinedPurchase mode only — absent on every sale line. { recoverablePercent, recoverableAmount, blockedAmount, recoverabilityRate, allocationSource, status, selfAssessedOutputTax? } — the input-tax recoverable/blocked split for this purchase line. See Purchase Mode.
taxVerificationobject | undefinedPurchase mode only — present only when you supplied lineItems[].statedTaxAmount for this line and it isn't a border-paid import. { statedTaxAmount, calculatedTaxAmount, delta, verdict, recoverableBase }, where verdict is "MATCH", "OVERCHARGED", or "UNDERCHARGED". See Purchase Mode.
Tax Calculations

Tax Treatments

Clearvo selects a tax treatment for each transaction using a priority-ordered decision tree. The treatment determines the EN16931 tax code and effective rate applied to line items.

TreatmenttaxCodeRateWhen applied
STANDARD S Country standard or reduced rate Default B2C treatment. The buyer is in the seller's country, or the seller is registered in the buyer's country and no other rule takes priority. Correct rate band (STANDARD, REDUCED, etc.) is selected per product category.
IOSS S Destination country rate Non-EU seller with an IOSS number, selling physical goods to an EU B2C customer, where the total goods value is ≤€150. VAT is collected at the destination country's rate and declared via IOSS.
REVERSE_CHARGE AE (non-EU→EU B2B) or K (EU→EU B2B cross-border) 0% B2B transaction where the buyer has a verified VAT ID and the transaction crosses an EU border. Tax liability shifts to the buyer. No VAT is charged on the invoice.
ZERO_RATED Z 0% Product falls into a zero-rated category in the destination jurisdiction (e.g. children's clothing in the UK, basic foodstuffs in many EU countries).
EXEMPT E 0% An exemption certificate has been registered for this customer's tax ID in the relevant jurisdiction and product category. See Exemptions.
OUT_OF_SCOPE O 0% The transaction falls outside the scope of indirect tax (e.g. financial services, insurance, inter-group transactions where both parties are in the same VAT group).
SELLER_NOT_REGISTERED O 0% The seller's tax obligation record for the buyer's jurisdiction has registrationStatus: "NOT_REGISTERED". No tax is charged. The obligation status surfaces in the dashboard as MONITORING or ACTION_REQUIRED.
EXPORT G 0% EU seller, non-EU buyer — goods or services exported outside the EU VAT area. Zero-rated as an export.
ℹ
The rate on each response line maps directly to the taxRate field accepted by the Clearvo e-invoicing /v1/send endpoint (as a percentage — 0.19 becomes 19). No category lookup is required: /v1/send has no line taxCode input; it resolves the EN16931 category itself from the rate plus your clientTaxCode or taxTreatment.
Tax Calculations

Jurisdiction Resolution

Clearvo determines which tax authority's rules apply to a transaction using a rule-based resolution chain. The resolved jurisdiction, resolution method, and any conflicting signals are all returned in the response.

1
Physical goods — shipping address wins

For tangible goods, the shipping destination controls the jurisdiction. If a shippingAddress.country is provided, it takes priority over all other signals.

2
B2B — VAT ID prefix then billing address

For B2B transactions, Clearvo extracts the country prefix from the customer's taxId (e.g. DE from DE987654321) and uses it as the primary signal. If the VAT ID prefix is absent or unverifiable, the billing address country is used.

3
General (non-digital) B2C services — the seller's own country

Consulting, legal, accounting, insurance, and other general professional services sold to a consumer are sourced to where the seller is established, not the customer's location — the opposite of the digital-services rule below (EU VAT Directive Art. 45 — general B2C services; contrast with Art. 58's customer-location rule for electronically supplied services). A German consultancy invoicing a French consumer charges German VAT, not French VAT, and reports jurisdiction.resolutionMethod: "SELLER_ADDRESS" on that line. Clearvo applies this seller-country sourcing globally — for any seller/buyer country pair classified as a general service — not only within the EU; see the sourcing matrix below for the exact scope of every rule.

4
B2C digital services — 2-of-3 rule (EU)

Digital/electronically-supplied services are sourced to the customer's location for both B2B and B2C (EU VAT Directive Art. 58/44). When either the buyer or the seller is in the EU, sellers of B2C digital services must collect two non-contradictory pieces of evidence of the customer's location — the evidentiary procedure set out in Reg 282/2011 Art. 24f. Clearvo evaluates up to three signals: billing address country, IP geolocation country, and card BIN country. The majority wins. If all three agree, resolutionPrecision is EXACT; if only two agree, it is COUNTRY. Disagreements are listed in jurisdiction.conflicts. Outside that EU-linked scope, the same customer-location outcome is reached via a simpler billing → IP → seller-country cascade, with no formal evidence-conflict procedure.

5
Special postal territories

Some territories use the country code of their parent state but are outside the EU VAT area. Clearvo detects these automatically from the postal code:

  • Canary Islands (ES 35xxx–38xxx) — outside EU VAT
  • Ceuta & Melilla (ES 51xxx, 52xxx) — outside EU VAT
  • Åland Islands (FI 22xxx) — outside EU VAT
  • Madeira (PT 90xxx–93xxx) — inside EU VAT

For a general B2C service (step 3 above), this detection runs on the seller's own postal code rather than the customer's, since that line is sourced to the seller's country.

6
US address precision tiers

For US transactions, Clearvo looks up the applicable sales tax rate using one of three precision tiers, depending on which address fields are supplied and your account's usAddressPrecision setting:

  • ROOFTOP — a street address (line1) is present and your account setting is usAddressPrecision: "rooftop" (the default). Clearvo resolves the address to a zip+4 code and looks up the exact rate for that delivery-point range. "Rooftop" is the industry-standard term for address-level precision in US sales tax — the underlying rate table is keyed on zip+4 ranges, which cover approximately 10–20 delivery points.
  • ZIP — only a postal code is available, or your account setting is usAddressPrecision: "zip" (which skips street-address resolution even when line1 is present). Clearvo uses the zip5 code directly.
  • STATE — only a state code (region) is available with no postal code. Clearvo applies the state-level rate. Note that US region is always required — if it is missing, the API returns a 422 error.

The tier used is returned in jurisdiction.addressPrecision on every US response. You can switch your account between ROOFTOP and ZIP precision under Settings → Tax Calculations.

When the resolution chain cannot determine the jurisdiction from available signals, Clearvo falls back to the seller's country and marks the result as degraded: true with a degradedReason of "jurisdiction_unresolved".

Sourcing matrix

Every line item is classified into one of three place-of-supply regimes (placeOfSupplyRule in the response — see Product Classification) before jurisdiction and tax code are resolved. The tables below are the complete, current sourcing behaviour per regime, including which rules are EU-specific versus applied by Clearvo for any seller/buyer country. A category classified UNSUPPORTED (e.g. real estate, restaurant/catering, passenger transport, event admission) never reaches sourcing at all — the calculation is rejected with a 422 category_not_supported error.

GOODS — tangible products, destination-of-goods sourcing.

CustomerSeller → buyerSourced toTax codeLegal basis & geographic scope
B2CanyCustomer's shipping (or billing) addressStandard/reduced rate at destinationApplied globally.
B2Bsame countryDomestic — seller's countryS (standard rate)Applied globally.
B2BEU seller → EU buyer (cross-border)Buyer's country (self-accounted)K — intra-Community supply, Art. 138EU-only. K is reserved for goods.
B2BEU seller → non-EU buyerExport, zero-ratedGEU-only (requires an EU seller).
B2Bnon-EU seller → EU buyerBuyer's country (self-accounted)AE — reverse charge, Art. 44/196Applied globally for any non-EU seller shipping into the EU.
B2Bnon-EU seller → non-EU buyerBuyer's country (self-accounted)AE — reverse chargeApplied globally (Clearvo default outside the EU); no ZA/AE/SA local-tax carve-out for goods — see below.

DIGITAL_SERVICE — electronically supplied (TBE) services: SaaS, streaming, downloads, online courses, etc. Customer-location sourcing for both B2B and B2C.

CustomerSeller → buyerSourced toTax codeLegal basis & geographic scope
B2Cbuyer or seller in the EUCustomer location, 2-of-3 evidence (billing/IP/BIN)Standard/reduced rate at destinationEU-only evidence procedure (Reg 282/2011 Art. 24f); the underlying customer-location rule (Art. 58) is applied by Clearvo for any transaction.
B2Cneither in the EUCustomer location via billing → IP → seller cascadeStandard/reduced rate at destinationApplied globally — same customer-location outcome, without the formal EU evidence-conflict procedure.
B2Bsame countryDomestic — seller's countrySApplied globally.
B2BEU seller → EU buyer (cross-border)Buyer's country (self-accounted)AE — reverse charge, Art. 44/196EU-only. Previously emitted K (goods' code); services now correctly use AE — see the changelog.
B2BEU seller → non-EU buyerExport, zero-ratedGEU-only (requires an EU seller).
B2Bnon-EU seller → EU buyerBuyer's country (self-accounted)AE — reverse charge, Art. 44/196Applied globally for any non-EU seller.
B2Bnon-EU seller → non-EU buyer, destination ZA, AE, or SABuyer's country — seller charges local VAT/GSTDestination standard rateCarve-out limited to South Africa, UAE, and Saudi Arabia. These jurisdictions require a registered non-resident supplier of digital services to charge local VAT/GST even B2B. Digital services only — the same three destinations apply ordinary reverse charge for GOODS or GENERAL_SERVICE.
B2Bnon-EU seller → non-EU buyer, any other destinationBuyer's country (self-accounted)AE — reverse chargeApplied globally (Clearvo default).

GENERAL_SERVICE — non-digital services: consulting, legal, accounting, financial services, insurance, healthcare. B2B sourcing matches DIGITAL_SERVICE; B2C sourcing does not.

CustomerSeller → buyerSourced toTax codeLegal basis & geographic scope
B2CanySeller's own country — resolutionMethod: "SELLER_ADDRESS"Seller-country standard/reduced rateCitation is EU VAT Directive Art. 45. Clearvo applies this rule globally — for any seller/buyer country pair, not only where the seller or buyer is in the EU. This is the behaviour change covered in the changelog.
B2Bsame countryDomestic — seller's countrySApplied globally.
B2BEU seller → EU buyer (cross-border)Buyer's country (self-accounted)AE — reverse charge, Art. 44/196EU-only. Previously emitted K; services now correctly use AE — see the changelog.
B2BEU seller → non-EU buyerExport, zero-ratedGEU-only (requires an EU seller).
B2Bnon-EU seller → EU buyerBuyer's country (self-accounted)AE — reverse charge, Art. 44/196Applied globally for any non-EU seller.
B2Bnon-EU seller → non-EU buyerBuyer's country (self-accounted)AE — reverse chargeApplied globally. The ZA/AE/SA local-tax carve-out above is scoped to DIGITAL_SERVICE only and never applies to general services.

B2G (government) customers are treated identically to B2B throughout the tables above.

Tax Calculations

Product Classification

Clearvo classifies each line item into a product category to determine the correct VAT rate band — for example, whether a digital product is taxed at the standard rate or a reduced "cultural goods" rate. No HS codes are required.

Classification pipeline

/v1/tax/calculate never makes a live AI call — every line resolves through a deterministic, low-latency cascade so a large cart is never slowed down by an unrecognised product name. AI-assisted classification is available separately, for building your product catalogue in advance — see Product Catalogue below.

1
Explicit override (taxCategory)

If you pass a taxCategory slug on a line item, that category is used with confidence 1.0 and status: "APPROVED". Use this for a one-off line, or when you maintain your own classification and want deterministic results without relying on Clearvo's catalogue at all.

2
Classification code map (classificationCodes)

Most catalogues already carry a product code from somewhere else: a Stripe product tax code, a Shopify product category, or a customs commodity code. Send those codes with the line and Clearvo looks each one up in its classification code map, which links an outside code to the right tax category. This is the recommended path for catalogues that already use one of these systems, and it takes precedence over everything below.

Send up to 5 codes as { "system", "code" } pairs. The systems are:

  • stripe — a Stripe product tax code such as txcd_10103000. The code must match exactly.
  • shopify — a Shopify product taxonomy category id such as aa-1-13. If your specific category has no entry of its own, Clearvo uses its nearest mapped parent category (aa-1-13-5 falls back to aa-1-13, then aa-1).
  • hs — a customs commodity code. Clearvo uses the longest mapped prefix of the code you send.

Other lowercase system names are accepted, but a system Clearvo does not know is skipped. Codes are tried in the order you list them and the first one that maps wins. A code is skipped, and the next one tried, when it has no entry in the map, when the map marks it as ambiguous (one outside category covers products that are taxed differently), or when the map records that no Clearvo category fits it. If no code maps, the line carries on to the steps below: product code, product name, then your account default. A skipped code never changes the result silently.

When a code decides the line, the response sets classificationSource to "CODE_MAP" and adds classificationSystem and classificationMatchedCode. The matched code is the entry that actually decided, which may be a parent of the one you sent, so you can see exactly why the line was classified as it was.

3
Product code lookup

Clearvo checks whether a prior classification exists for this exact productCode in your catalogue. If found, it's used immediately with fromCache: true. Catalogue entries are per-account and persist across calls — see Product Catalogue for how entries get there.

4
Product name lookup

If no productCode was supplied (or it didn't match), Clearvo looks for an exact, case-insensitive match on productName against your catalogue. Only an unambiguous match is used — if two catalogue entries share the same name, it's treated as no match rather than guessed, and the line falls through to the default below.

5
Account default

If nothing above matched, the line resolves to your account's configured default category (defaultTaxCategorySlug), or the standard physical-goods category if you haven't set one. status: "NEEDS_REVIEW" signals this line wasn't recognised — add it to your catalogue (see below) so future calls resolve it directly.

Common category slugs

A representative sample — call GET /v1/tax/categories for the complete live list.

SlugTypical rate bandExamples
saas_businessSTANDARDSaaS subscriptions (B2B use)
saas_personalSTANDARDSaaS subscriptions (consumer / personal use)
api_data_servicesSTANDARDAPI access, data feeds, webhooks
streaming_videoSTANDARDVideo streaming subscriptions
ebooksREDUCED (most EU countries)Digital books, audiobooks, online newspapers
books_physicalREDUCED (most EU countries)Printed books, newspapers, maps
online_coursesEXEMPT (many jurisdictions)E-learning, training subscriptions
food_basicREDUCED or ZEROGroceries, basic unprocessed foods
food_restaurantREDUCED or STANDARDPrepared meals, catering
medical_devicesEXEMPT or REDUCEDMedical equipment, hearing aids, spectacles
pharmaceuticalsREDUCED or ZEROPrescription drugs, licensed medications
clothing_childrenREDUCED or ZEROChildren's apparel (zero-rated in UK and IE)
financial_servicesEXEMPTBanking fees, insurance premiums, credit services
professional_servicesSTANDARDConsulting, legal, accounting, advisory
physical_goods_generalSTANDARDCatch-all for physical goods with no specific slug
digital_generalSTANDARDCatch-all for digital goods with no specific slug
nontaxableZERO / EXEMPTItems not subject to indirect tax in any jurisdiction
✓
For production use, we recommend pre-classifying your product catalogue via the Product Catalogue endpoints and sending productCode on every line. This gives you a reviewed, deterministic result instead of falling through to your account default — reserve an inline taxCategory override for one-off lines or products you don't want in the catalogue at all.
Tax Calculations

E-Invoicing Integration

Tax Calculation output is designed to feed directly into Clearvo e-invoice submissions. The rate and taxAmount on each calculated line item become the taxRate (as a percentage) and taxAmount on the matching lines[] entry of POST /v1/send — no mapping layer is needed, and the invoice line never carries a taxCode.

Example: calculate then invoice

Step 1 — calculate
{
  // POST /v1/tax/calculate response (line items)
  "lineItems": [{
    "id": "line-1",
    "taxCode": "S",
    "rate": 0.19,
    "taxableAmount": 100.00,
    "taxAmount": 19.00,
    "totalAmount": 119.00
  }]
}
Step 2 — invoice (POST /v1/send)
{
  "invoiceNumber": "INV-2026-001",
  "issueDate": "2026-06-21",
  "currency": "EUR",
  "country": "DE",
  // supplier and customer fields …
  "lines": [{
    "description": "Annual SaaS subscription",
    "quantity": 1,
    "unitPrice": 100.00,
    "taxRate": 19,       // ← rate 0.19 from calculation response, as a percentage
    "taxAmount": 19.00  // ← from calculation response
  }]
}

EN16931 tax code reference

CodeNameWhen used
SStandard rateStandard or reduced VAT/GST applied to the transaction
AALower rateA lower rate than the standard rate applies (e.g. reduced band in EU)
AEVAT Reverse ChargeNon-EU seller to EU B2B buyer — buyer accounts for VAT
KVAT exempt (intra-community)EU-to-EU cross-border B2B — intra-community supply
GFree export item, tax not chargedEU seller, non-EU buyer — export outside the EU VAT area
EExempt from taxTransaction is exempt under local rules (financial services, health, education)
ZZero-rated goodsGoods or services taxable at 0% (e.g. children's clothing in UK)
OServices outside scope of taxTransaction falls outside the indirect tax scope, or seller is not registered in the jurisdiction
Tax Calculations

Sandbox

Use a csk_test_* API key to run calculations in sandbox mode. Sandbox calculations are identical to production in every respect except that they never count toward usage or Compliance Radar totals, and the sandbox field on the response is true.

What runs normally in sandbox

  • Jurisdiction resolution (all signals, postal territory detection, 2-of-3 rule)
  • Rate lookup (same rate tables as production)
  • Product classification (catalogue lookup and account default)
  • Tax treatment determination (all rules, including IOSS and reverse charge)
  • DB write on a recorded calculation (commit: true, or omitted since it now defaults to true) — written to the sandbox DB, not production
  • Idempotency key enforcement (scoped to the sandbox DB)

What is skipped in sandbox

  • VIES customer VAT ID verification — the customer's VAT ID is accepted at face value; no live VIES call is made
  • Usage counting and Compliance Radar threshold contribution — a sandbox calculation never counts toward either, whatever committed is
curl — sandbox
curl https://api.clearvo.io/v1/tax/calculate \
  -X POST \
  -H "x-api-key: csk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "EUR",
    "supplier": { "billingAddress": { "country": "IE" }, "taxId": "IE1234567T" },
    "customer": { "billingAddress": { "country": "FR" } },
    "lineItems": [{ "id": "l1", "amount": 99.00, "amountIncludesTax": false, "productName": "SaaS subscription" }]
  }'
JSON Response — sandbox
{
  "calculationId": "cl_calc_test_...",
  "sandbox": true,
  "committed": true,
  "degraded": false,
  "jurisdiction": { "country": "FR", "resolutionMethod": "BILLING_ADDRESS" },
  "taxTreatment": "STANDARD",
  "taxCode": "S",
  "lineItems": [{
    "id": "l1",
    "taxCode": "S",
    "rate": 0.20,
    "taxableAmount": 99.00,
    "taxAmount": 19.80,
    "totalAmount": 118.80
  }],
  "summary": { "totalAmount": 99.00, "totalDiscount": 0.00, "totalTax": 19.80, "totalAmountWithTax": 118.80 }
}
✓
The sandbox is the right place to validate your full calculate→invoice workflow end-to-end. Rates are real, jurisdiction resolution is real, and the rate/taxCode output is identical to production — the only difference is that the sandbox flag is set and no charge is incurred.