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: trueand a zero rate - Recorded by default, preview on request — every calculation is persisted to your audit trail unless you send
"commit": falsefor a quote/preview that shouldn't count toward usage or thresholds (see Calculate)
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.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.
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 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"
}
]
}'
{
"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.
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.
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.
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.
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": trueso 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/BREACHtransition or a new mandate-obligation alert if you call/calculatemore than once per real sale (e.g. a "quote" pattern) without passingcommit: falsefor the quote calls. - A
transactionType: "credit_note"request that omits bothcommitandrelatedCalculationIdnow fails with a 422 instead of succeeding as an ephemeral preview — see the reworded validation message in Credit Notes & Refunds. - A supplied
idempotencyKeyis now honoured (previously silently ignored whencommitwas omitted): the first request for a given key wins the claim, a concurrent duplicate gets 409idempotency_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.
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 everybuyer*-prefixed field (buyerType,notifyBuyer,buyerName,buyerTaxId,buyerNotification,notifyBuyerByDefault, and the country-specificcountrySpecific.*.buyer*fields) acrossPOST /v1/send,GET /v1/invoices,GET /v1/export,GET /v1/mandates, exemptions, transactions, and NEEDS_INFO payloads. - ~24 error codes containing
BUYERrenamed to theirCUSTOMERequivalent, 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, andINVALID_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_idrenamed to theircustomer_*equivalents. - Webhook keys: payload fields
buyer*/buyerNotificationrenamed tocustomer*/customerNotification, andinvoice.rejected'srejectedBy: 'buyer'is nowrejectedBy: '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.
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 send | Resolved to | Notes |
|---|---|---|
"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 send | Resolved 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 send | Resolved to | Notes |
|---|---|---|
"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.
| Scenario | What to send |
|---|---|
| B2C consumer sale | Omit taxId and b2bOverride — B2C treatment is the default |
| B2B sale, VAT number known | Supply customer.taxId — Clearvo verifies it and applies reverse charge or zero-rating automatically |
| B2B sale, VAT number not available | Set 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 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 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
Calculate
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": truein 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/BREACHtransition. - A
transactionType: "credit_note"request with norelatedCalculationIdnow fails with 422 instead of previewing. - A supplied
idempotencyKeyis 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).
"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 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 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):
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | Yes | ISO 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.) |
| reportingCurrency | string | No | ISO 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). |
| date | string | No | ISO 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. |
| commit | boolean | No | Default: 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. |
| idempotencyKey | string | No | A 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" | No | Default: "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" | No | Controls 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. |
| merchantRef | string | No | Merchant-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" | No | Informational only — echoed on the response, does not affect the calculation. |
| isMarketplaceFacilitatedSale | boolean | No | Set 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" | No | Default: "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. |
| relatedCalculationId | string | Conditionally required | The 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" | No | Default: "sale". Set to "purchase" for an incoming, input-tax transaction — see Purchase Mode below. |
| clientTaxCode | string | No | Your 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
| Field | Type | Required | Description |
|---|---|---|---|
| customer | object | Yes, 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. |
| supplier | object | Yes, 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.country | string | Yes, if supplier is sent | ISO 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.taxId | string | No | On 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.name | string | No | Free-text display name, informational only. |
| supplier.ref | string | No | Purchase 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.country | string | Yes | ISO 3166-1 alpha-2 country code of the customer's billing address — the primary jurisdiction signal for B2B and most B2C transactions. |
| customer.billingAddress.region | string | Yes (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.postalCode | string | No | Used 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.shippingAddress | object | No | Same shape as billingAddress. Takes precedence over billingAddress for physical goods when both are supplied. |
| customer.taxId | string | No | Customer'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.b2bOverride | boolean | No | When 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.exemptionRef | string | No | Reference (certificate_ref) to an exemption certificate stored in Clearvo ECM. Applies the exemption to eligible line items when an active matching certificate exists. |
| customer.ref | string | No | Your 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. |
| seller | object | No — deprecated | Deprecated 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". |
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
| Field | Type | Required | Description |
|---|---|---|---|
| shipFrom.country | string | No | Physical 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. |
| shipTo | object | No | Physical 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. |
| incoterms | string | No | Incoterms® 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" | No | Explicit 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" | No | Read 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. |
| insurance | object | No | { amount, currency? }. Consignment insurance, a customs-value component under CIF. Read only when duties are requested. |
| duties | object | No | { 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. |
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
| Field | Type | Required | Description |
|---|---|---|---|
| evidence.ipAddress | string | No | Customer'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.binCountry | string | No | ISO 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
| Field | Type | Required | Description |
|---|---|---|---|
| lineItems[].id | string | Yes | Your identifier for this line item. Echoed back in the response. |
| lineItems[].amount | number | Yes | Line 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[].quantity | number | No | Default: 1. Informational only — amount should already be the total line amount, not a per-unit price. |
| lineItems[].productName | string | No | Plain-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[].productDescription | string | No | Additional descriptive text, max 500 characters, stored for your records only. |
| lineItems[].productCode | string | No | Your own product/SKU code, max 40 characters, stored for your records and used to key the per-account classification cache. |
| lineItems[].amountIncludesTax | boolean | No | Explicitly 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[].taxCategory | string | No | Explicit product category slug, skipping catalogue lookup entirely (e.g. "digital-services", "books"). See Product Classification for available slugs. |
| lineItems[].classificationCodes | array | No | Up 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[].taxCode | string | No | EN16931 escape hatch — directly forces the output taxCode for this line. Rare; prefer taxTreatmentOverride or clientTaxCode. |
| lineItems[].discount | number | No | Reduces the taxable base: taxableAmount = amount − discount. |
| lineItems[].discountAmount | number | No | Audit-trail/display only — stored verbatim, never affects taxableAmount/taxAmount. Distinct from discount above, which does reduce the taxable base. |
| lineItems[].exempt | boolean | No | Claims 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[].taxTreatmentOverride | string | No | Caller-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[].commodityCode | string | No | Tariff/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" | No | Scheme 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[].countryOfOrigin | string | No | ISO 3166-1 alpha-2 country the goods were made in, read only when duties are requested. Never inferred from shipFrom. |
| lineItems[].weight | object | No | { value, unit } (kg, g, lb, oz), per unit; the line weight is value × quantity. Read only when duties are requested. |
| lineItems[].clientTaxCode | string | No | Your own ERP tax code for this line only — wins over the invoice-header clientTaxCode for this line specifically. |
| lineItems[].shippingCarrier | "common" | "seller_vehicle" | No | US 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[].chargeAvoidable | boolean | No | US shipping_handling lines only. Whether the customer could have avoided this shipping charge (e.g. in-store pickup). Omitted resolves conservatively to false. |
| lineItems[].actualCostOfShipment | boolean | No | US 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. |
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 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
}
]
}'
| Field | Type | Required | Description |
|---|---|---|---|
| transactionDirection | "purchase" | Yes, to enter purchase mode | Persisted 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. |
| supplier | object | Yes, 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[].recoverablePercentOverride | number, 0–100 | No | Purchase-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[].statedTaxAmount | number | No | Purchase-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.
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.
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 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 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).| HTTP | When it occurs | How to fix |
|---|---|---|
| 422 | Missing 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. |
| 422 | currency is not exactly 3 characters, or lineItems is an empty array or exceeds 100 items | Supply a valid ISO 4217 currency code (e.g. "EUR") and between 1 and 100 line items. For larger carts, split into multiple calculate calls. |
| 422 | merchantRef exceeds 100 characters, lineItems[].productCode exceeds 40, lineItems[].productName exceeds 200, or lineItems[].productDescription exceeds 500 | Truncate the field to its limit. |
| 422 | customer.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. |
| 400 | An account-scoped key was used without an X-Entity-Id header | Add X-Entity-Id: <entityId> to identify the target entity, or use an entity-scoped key. |
| 403 | The entity in X-Entity-Id does not belong to this account | Check that the entity ID belongs to the account associated with this API key. |
| 401 | The x-api-key header is missing or the key is unrecognised | Include a valid x-api-key header on every request. |
| 409 | idempotency_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. |
| 422 | transactionType is "credit_note", the resolved commit is true (explicit or the default), but relatedCalculationId is absent | Supply the calculationId of the original invoice in relatedCalculationId, or send "commit": false if you only meant to preview the credit note amount. |
| 422 | relatedCalculationId is provided but the referenced calculation does not exist or belongs to a different entity | Verify the ID — it must be a recorded calculation for this entity. Preview (uncommitted) calculations are not valid targets. |
| 422 | The 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. |
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 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 https://api.clearvo.io/v1/tax/calculate/cl_calc_01j4.../refund \
-X POST \
-H "x-api-key: csk_live_..."
{
"ok": true,
"calculationId": "cl_calc_01j4...",
"refundedAt": "2026-06-24T10:30:00.000Z"
}
Error responses
| HTTP | When it occurs | How to fix |
|---|---|---|
| 404 | The calculation does not exist or belongs to a different entity | Check that the ID is correct and the API key is for the entity that owns this calculation. |
| 409 | The calculation has already been marked as refunded | No action needed — the refund was already processed. The response body includes the original refundedAt timestamp. |
| 422 | The calculation is a sandbox calculation | Sandbox calculations cannot be marked as refunded — they never count toward usage or compliance thresholds in the first place. |
| 422 | The 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.
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
| Method | Path | Description |
|---|---|---|
| GET | /v1/tax/exemptions | List all exemptions. Filter by country, customerTaxId, or taxCategorySlug. |
| POST | /v1/tax/exemptions | Create 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 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
| Field | Type | Required | Description |
|---|---|---|---|
| country | string | Yes | ISO country code the exemption applies to. |
| region | string | No | State or province code. Leave null for a country-wide exemption. |
| customerTaxId | string | Yes | The customer's tax ID that this exemption is issued to. |
| taxCategorySlug | string | No | Limit the exemption to a specific product category. If null, the exemption applies to all categories in the jurisdiction. |
| reason | string | No | Human-readable description for your records (e.g. "Non-profit exemption certificate"). |
| certificateRef | string | No | Your reference number for the exemption certificate. Stored for audit purposes. |
| validFrom | string | Yes | YYYY-MM-DD. Date from which the exemption is valid. |
| validTo | string | No | YYYY-MM-DD. Expiry date. If omitted, the exemption does not expire. |
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
| Method | Path | Description |
|---|---|---|
| GET | /v1/tax/obligations | List 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 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
| Field | Type | Description |
|---|---|---|
| country | string | ISO country code this obligation applies to. |
| region | string | null | State 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. |
| registrationNumber | string | null | The 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. |
| thresholdAmount | number | null | The economic nexus threshold for this jurisdiction (in local currency). Informational only — Clearvo does not automatically track sales against this threshold. |
| currentPeriodAmount | number | null | Your current period sales amount in this jurisdiction. Update this via PATCH to keep the obligation status current. |
| estimatedExposure | number | null | Estimated tax exposure if registered (computed field — not updateable directly). |
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
| Method | Path | Description |
|---|---|---|
| GET | /v1/tax/products | List classified products. Filter by status (APPROVED | NEEDS_REVIEW), entityId (account-scoped keys only), page, limit (max 100). |
| POST | /v1/tax/products | Classify 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}/restore | Reactivate a soft-deleted product. Body: { "reason": "string" } (required — logged to audit trail). |
| POST | /v1/tax/products/bulk | Classify up to 500 products in one call. Accepts text/csv or application/json. |
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 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
| Field | Type | Description |
|---|---|---|
| productCode | string | Your SKU or ERP code. Used as the cache key — the same product is classified at most once per account. |
| productName | string | Plain-language product name, used for an exact-match catalogue lookup when productCode isn't supplied or doesn't match. Required unless productCode is supplied. |
| productDescription | string | Additional context for the classifier. Include when the product name alone is ambiguous. |
| taxCategory | string | Explicit slug override (e.g. "saas_business", "ebooks"). Bypasses AI — confidence is set to 1.0 and status is APPROVED immediately. |
| classificationCodes | array | Up 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. |
| status | APPROVED | NEEDS_REVIEW | AI confidence ≥ 0.85 → APPROVED automatically. Below 0.85 → NEEDS_REVIEW until approved via PATCH or the dashboard. |
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.
Endpoint
GET https://api.clearvo.io/v1/tax/categories
Accepts both entity-scoped and account-scoped API keys.
curl https://api.clearvo.io/v1/tax/categories \
-H "x-api-key: csk_live_..."
{
"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
| Field | Type | Description |
|---|---|---|
| slug | string | The category identifier to use in taxCategory on line items, or as defaultTaxCategorySlug in account settings. |
| name | string | Human-readable description of the category. |
| defaultTaxCode | string | The 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. |
| eligibleSchemes | string[] | 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. |
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 https://api.clearvo.io/v1/tax/jurisdictions \
-H "x-api-key: csk_live_..."
{
"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):
{ "country": "QA", "name": "Qatar", "comingSoon": true, "availableFrom": "2027-01-01" }
Response fields
| Field | Type | Description |
|---|---|---|
| object | string | Always "list". |
| jurisdictions[] | array | One 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[].country | string | ISO 3166-1 alpha-2 country code (uppercase), e.g. "DE". |
| jurisdictions[].name | string | English display name of the jurisdiction, e.g. "Germany". |
| jurisdictions[].comingSoon | boolean | false — 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[].availableFrom | string | Present only when comingSoon is true — the announced go-live date (YYYY-MM-DD). Omitted entirely on live jurisdictions. |
| count | integer | Total 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
comingSoonjurisdiction whoseavailableFromdate 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 reportingcomingSoon: trueand calculate keeps returning422— 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:
{
"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)
| Code | Country | Code | Country | Code | Country |
|---|---|---|---|---|---|
| AT | Austria | FI | Finland | LV | Latvia |
| BE | Belgium | FR | France | MT | Malta |
| BG | Bulgaria | GR | Greece | NL | Netherlands |
| CY | Cyprus | HR | Croatia | PL | Poland |
| CZ | Czechia | HU | Hungary | PT | Portugal |
| DE | Germany | IE | Ireland | RO | Romania |
| DK | Denmark | IT | Italy | SE | Sweden |
| EE | Estonia | LT | Lithuania | SI | Slovenia |
| ES | Spain | LU | Luxembourg | SK | Slovakia |
Rest of Europe (11)
| Code | Country | Code | Country | Code | Country |
|---|---|---|---|---|---|
| AL | Albania | IS | Iceland | RS | Serbia |
| BA | Bosnia & Herzegovina | ME | Montenegro | TR | Türkiye |
| CH | Switzerland | MK | North Macedonia | UA | Ukraine |
| GB | United Kingdom | NO | Norway |
Americas (10)
| Code | Country | Code | Country | Code | Country |
|---|---|---|---|---|---|
| AR | Argentina | CO | Colombia | US | United States |
| BR | Brazil | EC | Ecuador | UY | Uruguay |
| CA | Canada | MX | Mexico | ||
| CL | Chile | PE | Peru |
Asia-Pacific (15)
| Code | Country | Code | Country | Code | Country |
|---|---|---|---|---|---|
| AU | Australia | IN | India | PH | Philippines |
| BD | Bangladesh | JP | Japan | SG | Singapore |
| CN | China | KR | South Korea | TH | Thailand |
| HK | Hong Kong SAR China | MY | Malaysia | TW | Taiwan |
| ID | Indonesia | NZ | New Zealand | VN | Vietnam |
Middle East (7)
| Code | Country | Code | Country | Code | Country |
|---|---|---|---|---|---|
| AE | United Arab Emirates | JO | Jordan | SA | Saudi Arabia |
| BH | Bahrain | KW | Kuwait | ||
| IL | Israel | OM | Oman |
Africa (6)
| Code | Country | Code | Country | Code | Country |
|---|---|---|---|---|---|
| EG | Egypt | KE | Kenya | NG | Nigeria |
| GH | Ghana | MA | Morocco | ZA | South Africa |
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
| Method | Path | Description |
|---|---|---|
| GET | /v1/tax/registrations | List 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/registrations | Create 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.
| Status | Meaning |
|---|---|
COLLECTING | Tax is being collected — collectFromDate is set to today or a past date. |
DEFERRED | Collection starts on a future collectFromDate. No tax collected until that date. |
SETUP_NEEDED | Tax Calculations is enabled but no collectFromDate has been set. Set one via PATCH /v1/tax/registrations/{id}. |
null | Tax 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 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"
}'
# 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
| Type | Description | Engine effect |
|---|---|---|
| VAT | Standard national VAT registration | Sets obligation to REGISTERED; tax collected at local rate |
| IOSS | EU Import One-Stop Shop number | Writes registration_number on a tax_registrations row with scheme=IOSS; enables IOSS treatment for eligible B2C goods ≤ €150 shipped into the EU |
| UNION_OSS | EU One-Stop Shop (Union scheme) | Sets obligation to REGISTERED with OSS scheme flag |
| NON_UNION_OSS | EU One-Stop Shop (Non-Union scheme) | Sets obligation to REGISTERED with OSS scheme flag |
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
| Method | Path | Description |
|---|---|---|
| GET | /v1/tax/settings | Read current account settings. Returns defaults if no settings row has been written yet. |
| PATCH | /v1/tax/settings | Update 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 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
| Field | Type | Default | Description |
|---|---|---|---|
| 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. |
| defaultPriceIncludesTax | boolean | true (non-US) / n/a for US | Whether 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). |
| defaultTaxCategorySlug | string | null | null | Fallback 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. |
| placeOfBusinessAddress | object | null | null | Where 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. |
| isMarketplaceFacilitator | boolean | false | Whether 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. |
| availableTaxCategories | array | — | Read-only. Full list of active category slugs and names available in your account (GET only). |
Response Reference
Full field reference for the TaxCalculateResponse object returned by the calculation endpoint.
Top-level fields
| Field | Type | Description |
|---|---|---|
| calculationId | string | Unique ID for this calculation (cl_calc_* prefix, cl_calc_test_* in sandbox). Present whether the calculation was recorded or previewed. |
| entityId | string | The 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. |
| apiVersion | string | Always "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. |
| resolvedAt | string | ISO 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. |
| sandbox | boolean | True 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. |
| committed | boolean | Whether 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. |
| merchantRef | string | Echoes the request's merchantRef, when supplied. |
| paymentMethod | string | Echoes the request's paymentMethod, when supplied. |
| incoterms | string | Echoes the request's incoterms, when supplied. |
| taxTreatment | string | The 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. |
| taxCode | string | EN16931 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. |
| relatedCalculationId | string | undefined | The calculationId of the original invoice this credit note reverses. Only present when transactionType is "credit_note" and the field was provided in the request. |
| refundedAt | string | undefined | ISO 8601 timestamp. Set when POST /v1/tax/calculate/{id}/refund has been called. Absent for non-refunded calculations. |
| totalTax | number | undefined | Top-level convenience alias for summary.totalTax below. |
| degraded | boolean | undefined | True 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. |
| degradedReason | string | undefined | Human-readable explanation of why the result is degraded. Only present when degraded: true. |
| staleCacheReason | string | undefined | Present 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"). |
| reportingCurrencyAmounts | object | undefined | Present only when the request supplied reportingCurrency: { currency, totalTax, totalAmountWithTax, fxRate, fxRateAsOf } — the same totals converted into that currency. |
| jurisdictionBreakdown | array | undefined | Present 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 }. |
| pendingCertificates | array | undefined | Present only when an inline lineItems[].exempt claim had no matching active ECM certificate. Each entry: { certId, certRef, ecmUrl } — see Exemptions. |
| lineResults | array | Alias for lineItems below — identical data, both fields are always present together. |
jurisdiction
| Field | Type | Description |
|---|---|---|
| jurisdiction.country | string | ISO country code of the resolved tax jurisdiction. |
| jurisdiction.region | string | undefined | State or province of the resolved jurisdiction. Present for US/Canadian transactions where sub-national rates apply. |
| jurisdiction.resolutionMethod | string | How 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.resolutionPrecision | string | Confidence in the resolved jurisdiction: EXACT, POSTAL, REGION, or COUNTRY. |
| jurisdiction.addressPrecision | string | undefined | US 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" | undefined | US TX/CA/IL only. Which address local tax was sourced to. Absent for every other state/country, which always destination-source. |
| jurisdiction.sourcingAddress | string | undefined | Companion to sourcingMethod — which address it resolved to (SELLER_PLACE_OF_BUSINESS or BUYER_ADDRESS). |
| jurisdiction.sourcingCitation | string | undefined | Statute/regulation citation backing sourcingMethod. |
| jurisdiction.localityOverride | object | undefined | US 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.conflicts | array | Non-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, 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.
| Field | Type | Description |
|---|---|---|
| entityRole | "supplier" | "customer" | Which party block is your own entity. |
| supplier.country | string | undefined | This party's resolved country. |
| supplier.region | string | undefined | State/region code, when resolved (e.g. US). |
| supplier.postalCode | string | undefined | — |
| supplier.taxId | string | undefined | This party's tax/VAT ID, when known. |
| supplier.isEntity | boolean | true when this block is your own entity; false when it is the counterparty. |
| supplier.b2bOverride | boolean | undefined | Counterparty block only (a purchase). Echoes the request's supplier.b2bOverride. |
| supplier.taxIdValidated | boolean | undefined | Counterparty block only (a purchase). Whether the supplied supplier.taxId was successfully verified. |
| supplier.taxIdValidationStatus | string | undefined | Counterparty block only (a purchase). More detail on the validation outcome. |
| customer.country | string | undefined | This party's resolved country. |
| customer.region | string | undefined | State/region code, when resolved (e.g. US). |
| customer.postalCode | string | undefined | — |
| customer.taxId | string | undefined | This party's tax/VAT ID, when known. |
| customer.isEntity | boolean | true when this block is your own entity; false when it is the counterparty. |
| customer.b2bOverride | boolean | undefined | Counterparty block only (a sale). Echoes the request's customer.b2bOverride. |
| customer.taxIdValidated | boolean | undefined | Counterparty block only (a sale). Whether the supplied customer.taxId was successfully verified (VIES, or format-only under vatValidation: "format"). |
| customer.taxIdValidationStatus | string | undefined | Counterparty 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.
| Field | Type | Description |
|---|---|---|
| sellerRegistration.status | "REGISTERED" | "PENDING" | "NOT_REGISTERED" | "MONITORING" | Registration state for this jurisdiction. |
| sellerRegistration.canCollectTax | boolean | Whether tax was actually collected on this calculation. |
| sellerRegistration.pricingModel | "TAX_EXCLUSIVE" | "TAX_INCLUSIVE" | "TAX_INCLUSIVE_PENDING" | Your configured pricing model for this registration. |
| sellerRegistration.registrationNumber | string | undefined | The registration/VAT number on file, when one exists. |
| sellerRegistration.scheme | string | undefined | The registration scheme matched, e.g. "OSS_UNION", "SIMPLIFIED". |
| sellerRegistration.matchLevel | "country" | "region" | "locality" | undefined | Specificity of the registration row that matched (relevant for CA federal/provincial splits). |
| sellerRegistration.reason | string | undefined | Free-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" | undefined | Closed, machine-readable classification, present only on a NOT_REGISTERED/$0 outcome. |
| sellerRegistration.requiresRegistrationToCollect | boolean | Whether 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)
| Field | Type | Description |
|---|---|---|
| ioss.number | string | The IOSS registration number used for this transaction. |
| ioss.registrationCountry | string | The EU member state where the IOSS number is registered. |
| ioss.totalGoodsValue | number | Nominal total value of goods in the transaction currency (used to determine the ≤€150 threshold eligibility). |
| ioss.currency | string | Currency 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.
| Field | Type | Description |
|---|---|---|
| customsDuty.feeCode | string | Always "EU_LOW_VALUE_CONSIGNMENT_CUSTOMS_DUTY" today. |
| customsDuty.currency | string | Always "EUR" — the duty is never converted into the calculation's own currency. |
| customsDuty.amount | number | Total duty owed this calculation — perItemAmount × itemCount. |
| customsDuty.perItemAmount | number | The per-distinct-classification rate the rule charges (currently €3.00). |
| customsDuty.itemCount | integer | Distinct qualifying classification groups counted this calculation. |
| customsDuty.classificationDigits | integer | Tariff-code digit-length used to group lines (6 = HS6/H7). |
| customsDuty.items | array | One entry per distinct classification code, plus one per missing/malformed-code line. Each entry: { classificationCode, lineIds }. |
| customsDuty.linesMissingCommodityCode | string[] | undefined | lineIds of qualifying lines whose commodityCode was missing or unusable — each still counts as its own item, never silently dropped. |
| customsDuty.includedInTotals | false | Always false — never folded into totalTax/totalAmountWithTax. |
| customsDuty.payableBy | "DECLARANT" | Always "DECLARANT" — the IOSS holder or their indirect customs representative, never the buyer. |
| customsDuty.estimate | true | Always true — customs assesses the actual debt on release; this is a forecast. |
| customsDuty.effectiveFrom | string | ISO date — the resolved fee rule's own effective-from date. |
| customsDutyNote | "NOT_REVERSED_ON_CREDIT_NOTE" | undefined | Present 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:
{
"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:
{
"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
| Field | Type | Description |
|---|---|---|
| summary.totalAmount | number | Sum of all taxableAmount values across line items (tax-exclusive). |
| summary.totalDiscount | number | Sum of all discounts applied across line items. |
| summary.totalTax | number | Sum 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.totalAmountWithTax | number | Sum of all totalAmount values across line items (totalAmount + totalTax). Never includes customsDuty.amount — see customsDuty above. |
| summary.retailDeliveryFees | array | undefined | Present 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[]
| Field | Type | Description |
|---|---|---|
| id | string | Your line item ID, echoed from the request. |
| taxCode | string | EN16931 tax code for this line item. |
| rate | number | Effective tax rate for this line item as a decimal fraction (0.19 = 19%, 0 = zero-rated). |
| taxableAmount | number | Amount on which tax is calculated (always tax-exclusive, regardless of the seller's pricing model). |
| taxableBasis | number | undefined | Present (< 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. |
| taxAmount | number | Tax amount for this line item. |
| totalAmount | number | Total including tax (taxableAmount + taxAmount). |
| taxCategory | string | Product category slug resolved for this line item (e.g. digital_services). |
| classificationStatus | "APPROVED" | "NEEDS_REVIEW" | "MANUAL" | "PENDING" | undefined | APPROVED — used with confidence; NEEDS_REVIEW — low confidence, recommend manual override; MANUAL — explicit taxCategory supplied; PENDING — classification failed, fallback used. |
| classificationConfidence | number | undefined | Confidence score (0–1) carried over from when this catalogue entry was originally classified. 1.0 when an explicit taxCategory was provided on this call. |
| classificationFallback | true | undefined | Present 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" | undefined | HOW 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. |
| classificationSystem | string | undefined | Present only when classificationSource is "CODE_MAP": which code system decided this line (stripe, shopify, hs, …). |
| classificationMatchedCode | string | undefined | Present 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.rateBand | string | This line's resolved rate band (STANDARD/MIDDLE/REDUCED/SUPER_REDUCED/SPECIAL/ZERO/EXEMPT/etc.). Always present. |
| sourcingRationale.bandTier | string | Which 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. |
| jurisdiction | object | undefined | This 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" | undefined | This line's classified place-of-supply regime. |
| movement | "local" | "intra_community" | "export" | "distance_sale" | "import" | "own_goods_movement" | undefined | This line's EU-VAT-Directive movement classification. Present whenever the line reached full derivation, for both sale and purchase. |
| exemption | object | undefined | Present when an exemption certificate was applied: { certificateId, certificateRef, certificateType, effectiveTo }. |
| clientTaxCode | string | undefined | Your 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). |
| outcome | object | undefined | The 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. |
| recoverability | object | undefined | Purchase 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. |
| taxVerification | object | undefined | Purchase 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 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.
| Treatment | taxCode | Rate | When 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. |
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.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.
For tangible goods, the shipping destination controls the jurisdiction. If a shippingAddress.country is provided, it takes priority over all other signals.
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.
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.
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.
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.
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 isusAddressPrecision: "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 whenline1is 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 USregionis 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.
| Customer | Seller → buyer | Sourced to | Tax code | Legal basis & geographic scope |
|---|---|---|---|---|
| B2C | any | Customer's shipping (or billing) address | Standard/reduced rate at destination | Applied globally. |
| B2B | same country | Domestic — seller's country | S (standard rate) | Applied globally. |
| B2B | EU seller → EU buyer (cross-border) | Buyer's country (self-accounted) | K — intra-Community supply, Art. 138 | EU-only. K is reserved for goods. |
| B2B | EU seller → non-EU buyer | Export, zero-rated | G | EU-only (requires an EU seller). |
| B2B | non-EU seller → EU buyer | Buyer's country (self-accounted) | AE — reverse charge, Art. 44/196 | Applied globally for any non-EU seller shipping into the EU. |
| B2B | non-EU seller → non-EU buyer | Buyer's country (self-accounted) | AE — reverse charge | Applied 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.
| Customer | Seller → buyer | Sourced to | Tax code | Legal basis & geographic scope |
|---|---|---|---|---|
| B2C | buyer or seller in the EU | Customer location, 2-of-3 evidence (billing/IP/BIN) | Standard/reduced rate at destination | EU-only evidence procedure (Reg 282/2011 Art. 24f); the underlying customer-location rule (Art. 58) is applied by Clearvo for any transaction. |
| B2C | neither in the EU | Customer location via billing → IP → seller cascade | Standard/reduced rate at destination | Applied globally — same customer-location outcome, without the formal EU evidence-conflict procedure. |
| B2B | same country | Domestic — seller's country | S | Applied globally. |
| B2B | EU seller → EU buyer (cross-border) | Buyer's country (self-accounted) | AE — reverse charge, Art. 44/196 | EU-only. Previously emitted K (goods' code); services now correctly use AE — see the changelog. |
| B2B | EU seller → non-EU buyer | Export, zero-rated | G | EU-only (requires an EU seller). |
| B2B | non-EU seller → EU buyer | Buyer's country (self-accounted) | AE — reverse charge, Art. 44/196 | Applied globally for any non-EU seller. |
| B2B | non-EU seller → non-EU buyer, destination ZA, AE, or SA | Buyer's country — seller charges local VAT/GST | Destination standard rate | Carve-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. |
| B2B | non-EU seller → non-EU buyer, any other destination | Buyer's country (self-accounted) | AE — reverse charge | Applied globally (Clearvo default). |
GENERAL_SERVICE — non-digital services: consulting, legal, accounting, financial services, insurance, healthcare. B2B sourcing matches DIGITAL_SERVICE; B2C sourcing does not.
| Customer | Seller → buyer | Sourced to | Tax code | Legal basis & geographic scope |
|---|---|---|---|---|
| B2C | any | Seller's own country — resolutionMethod: "SELLER_ADDRESS" | Seller-country standard/reduced rate | Citation 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. |
| B2B | same country | Domestic — seller's country | S | Applied globally. |
| B2B | EU seller → EU buyer (cross-border) | Buyer's country (self-accounted) | AE — reverse charge, Art. 44/196 | EU-only. Previously emitted K; services now correctly use AE — see the changelog. |
| B2B | EU seller → non-EU buyer | Export, zero-rated | G | EU-only (requires an EU seller). |
| B2B | non-EU seller → EU buyer | Buyer's country (self-accounted) | AE — reverse charge, Art. 44/196 | Applied globally for any non-EU seller. |
| B2B | non-EU seller → non-EU buyer | Buyer's country (self-accounted) | AE — reverse charge | Applied 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.
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.
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.
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 astxcd_10103000. The code must match exactly.shopify— a Shopify product taxonomy category id such asaa-1-13. If your specific category has no entry of its own, Clearvo uses its nearest mapped parent category (aa-1-13-5falls back toaa-1-13, thenaa-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.
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.
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.
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.
| Slug | Typical rate band | Examples |
|---|---|---|
saas_business | STANDARD | SaaS subscriptions (B2B use) |
saas_personal | STANDARD | SaaS subscriptions (consumer / personal use) |
api_data_services | STANDARD | API access, data feeds, webhooks |
streaming_video | STANDARD | Video streaming subscriptions |
ebooks | REDUCED (most EU countries) | Digital books, audiobooks, online newspapers |
books_physical | REDUCED (most EU countries) | Printed books, newspapers, maps |
online_courses | EXEMPT (many jurisdictions) | E-learning, training subscriptions |
food_basic | REDUCED or ZERO | Groceries, basic unprocessed foods |
food_restaurant | REDUCED or STANDARD | Prepared meals, catering |
medical_devices | EXEMPT or REDUCED | Medical equipment, hearing aids, spectacles |
pharmaceuticals | REDUCED or ZERO | Prescription drugs, licensed medications |
clothing_children | REDUCED or ZERO | Children's apparel (zero-rated in UK and IE) |
financial_services | EXEMPT | Banking fees, insurance premiums, credit services |
professional_services | STANDARD | Consulting, legal, accounting, advisory |
physical_goods_general | STANDARD | Catch-all for physical goods with no specific slug |
digital_general | STANDARD | Catch-all for digital goods with no specific slug |
nontaxable | ZERO / EXEMPT | Items not subject to indirect tax in any jurisdiction |
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.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
{
// 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
}]
}
{
"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
| Code | Name | When used |
|---|---|---|
S | Standard rate | Standard or reduced VAT/GST applied to the transaction |
AA | Lower rate | A lower rate than the standard rate applies (e.g. reduced band in EU) |
AE | VAT Reverse Charge | Non-EU seller to EU B2B buyer — buyer accounts for VAT |
K | VAT exempt (intra-community) | EU-to-EU cross-border B2B — intra-community supply |
G | Free export item, tax not charged | EU seller, non-EU buyer — export outside the EU VAT area |
E | Exempt from tax | Transaction is exempt under local rules (financial services, health, education) |
Z | Zero-rated goods | Goods or services taxable at 0% (e.g. children's clothing in UK) |
O | Services outside scope of tax | Transaction falls outside the indirect tax scope, or seller is not registered in the jurisdiction |
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
committedis
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" }]
}'
{
"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 }
}
rate/taxCode output is identical to production — the only difference is that the sandbox flag is set and no charge is incurred.