> ## Documentation Index
> Fetch the complete documentation index at: https://docs.numeral.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> What's new in the Numeral API

# API Changelog

Jump to a version: [2026-11-01](#version-2026-11-01) (latest) · [2026-03-01](#version-2026-03-01) · [2026-01-01](#version-2026-01-01) · [2025-05-12](https://docs.numeral.com/api-reference/v2025-05-12/introduction.md) · [2024-09-01](https://docs.numeral.com/api-reference/v2024-09-01/introduction.md)

## Version 2026-11-01

> [!NOTE]
>
> This version explains every tax result. Each tax jurisdiction row gets a `note`, and calculations get one `effective_tax_rate` for the order. It's additive only: requests, tax amounts and errors are the same as `2026-03-01`.

### New: `note` on every tax jurisdiction row

Every row in `line_items[].tax_jurisdictions` now has a `note` that says why it came out the way it did. It's an empty string when tax was calculated normally.

| `note` | Meaning |
| - | - |
| `""` | Tax was calculated normally. |
| `no_collection_exempt_customer` | Customer is tax exempt, for everything or through a certificate covering this item. US and Canada only. |
| `no_collection_no_state_nexus` | No tax registration set up for this US state or Canadian province. |
| `no_collection_no_vat_registration` | No VAT registration set up for this country. |
| `no_collection_no_gst_registration` | No GST registration set up for this country. |
| `collection_manually_deactivated` | You have a registration here but turned collection off. |
| `automatic_tax_disabled` | The request sent `automatic_tax: "disabled"`. |
| `b2b_reverse_charge` | Cross-border business-to-business sale; the buyer accounts for the tax. |
| `shipping_follows_highest_taxed_item` | Shipping is taxed at the highest VAT rate in the order (UK). |
| `canary_islands_zero_rate` | No VAT: the Canary Islands are outside the EU VAT area. |

Returned on:

* `POST /tax/calculations`
* `POST /tax/platform/calculations` (`line_items` and `fee_line_items`)
* `POST /tax/transactions`, `GET /tax/transactions/{transaction_id}`, `GET /tax/transactions?reference_order_id=`
* `POST /tax/refunds`, `GET /tax/transactions/{transaction_id}/refunds`, `POST /tax/refund_reversals`

Records created before this version was released may return an empty `note`.

> [!NOTE]
>
> Versions `2024-09-01` and `2025-05-12` also had a `note` field, with the internal calculation method (`RATE_FRAC`, `PROGRESSIVE_RATE`, …) on normal rows. In `2026-11-01`, normal rows return `""` instead, and the two older human-readable values are now `b2b_reverse_charge` and `shipping_follows_highest_taxed_item`.

### New: `effective_tax_rate`

One blended rate for the order: `total_tax_amount / total_amount_excluding_tax`, as a decimal fraction (`0.105` = 10.5%), up to 6 decimal places.

| Endpoint | Field |
| - | - |
| `POST /tax/calculations` | `effective_tax_rate` |
| `POST /tax/platform/calculations` | `totals.order.effective_tax_rate`, and `totals.fees.effective_tax_rate` when `fee_details` is sent |

* Includes flat fees, such as Minnesota's retail delivery fee.
* Accounts for rates that apply to only part of an amount, such as Tennessee's single article rules. Summing the rows' `tax_rate` values doesn't.
* `0` when no tax applies.
* Not returned on transactions or refunds.

```json theme={null}
{
  "id": "calc_1793039299123",
  "object": "tax.calculation",
  "total_tax_amount": 1155,
  "total_amount_excluding_tax": 11000,
  "total_amount_including_tax": 12155,
  "effective_tax_rate": 0.105
}
```

> **[Calculations](https://docs.numeral.com/v2026-11-01/objects/calculations.md)**
>
> Full field list, every `note` value with examples, and how `effective_tax_rate` is computed.

***

### Migration Guide

#### From 2026-03-01 to 2026-11-01

> [!NOTE]
>
> There are **no breaking changes** from 2026-03-01. Requests are unchanged, and no fields are removed or renamed.

1. **Update your `X-API-Version` header** to `2026-11-01`, or change your account's default version.
2. **Allow the two new fields** if your parser rejects unknown fields: `note` on tax jurisdiction rows and `effective_tax_rate`.
3. **Optionally use `note`** to explain zero-tax rows to your users, and `effective_tax_rate` instead of summing `tax_rate` values.

A calculation made on one version can be recorded as a transaction on another. The transaction comes back in the shape of the version you send it on.

***

## Version 2026-03-01

> [!NOTE]
>
> **Release Date:** March 1, 2026
>
> This version adds IP-based tax resolution to calculations and platform calculations, a headless ECM read API for managing exemption certificates and certificate requests, and a certificate upload API for submitting certificate documents.

### New: Headless ECM read API (`certificate-requests` + `certificates`)

> [!NOTE]
>
> The headless ECM surface lets a client embed exemption-certificate
> collection in their own product — list, retrieve, and cancel certificate
> requests, and list / retrieve certificates with per-jurisdiction validity
> and a short-lived download URL.
>
> These endpoints are **live-only** (`sk_test_*` keys return
> `TESTMODE_NOT_SUPPORTED`) and are scoped by your API key's account.

#### New Endpoints

> **Certificate Requests**
>
> Manage outstanding certificate collection requests.
>
> * `GET /tax/certificate-requests` — List
> * `GET /tax/certificate-requests/{request_id}` — Get
> * `DELETE /tax/certificate-requests/{request_id}` — Cancel (terminal, idempotent)

> **Certificates**
>
> Read submitted certificates and download the original document.
>
> * `GET /tax/certificates` — List with filters (customer, jurisdiction, status, type, dates)
> * `GET /tax/certificates/{certificate_id}` — Get with short-lived `download_url`

#### Public status vocabulary

The `status` field on both objects uses a deliberately narrow public
vocabulary — Numeral's review pipeline runs many internal states that are
not surfaced.

| Object | Statuses |
| - | - |
| `tax.certificate_request` | `pending`, `fulfilled`, `canceled`, `invalid` |
| `tax.exemption_certificate` | `processing`, `needs_info`, `active`, `expiring`, `expired`, `invalid` |

`expiring` and `expired` are kept distinct from `invalid` so a renewable
lapse is distinguishable from a rejected or revoked certificate.

#### New error types

| Type | HTTP Code | Description |
| - | - | - |
| `CERTIFICATE_NOT_FOUND` | 404 | Certificate not found |
| `CERTIFICATE_REQUEST_NOT_FOUND` | 404 | Certificate request not found |
| `REQUEST_ALREADY_FULFILLED` | 409 | Cannot cancel a fulfilled request |
| `REQUEST_INVALID` | 409 | Cannot cancel a request in a terminal invalid state |

> **[Error Codes Reference](https://docs.numeral.com/api-reference/v2026-11-01/error-codes.md)**
>
> See the full list of error types and their meanings.

#### Notes

* Cancel is **terminal and idempotent** — calling `DELETE` on an
  already-canceled request returns `200` with the same body.
* `download_url` on `GET /tax/certificates/{id}` is a pre-signed URL with
  roughly a 1-hour TTL. Re-fetch the certificate to mint a new URL. The URL
  is `null` (response is still `200`) when no document is attached yet.
* Webhooks for certificate state transitions (e.g.
  `certificate.processing_completed`) will ship under the Public Webhooks
  Platform. Polling these read endpoints is the supported interim pattern.

#### Additive response fields

Certificates and certificate requests now include two enrichments to the
customer/buyer link (backfill-safe, no breaking changes):

* **`customer.name`** — display name for the linked customer. Populated
  from `Customer.name` with a fallback to the linked master buyer's
  `legal_name` when the customer record has no name of its own.
* **`linked_buyer`** — the master buyer entity that owns the certificate
  or request. Distinct from `customer` (a `Customer` record) — populated
  whenever the object has a master-buyer reference. Use this field to
  render "who owns this certificate" without depending on a specific
  `Customer` record having been linked yet.

```json theme={null}
{
  "customer": {
    "id": "cust_6126acaf-7379-411a-8ada-00005bac0715",
    "reference_customer_id": "20506",
    "name": "Acme Co"
  },
  "linked_buyer": {
    "id": "12345",
    "legal_name": "Acme Co"
  }
}
```

***

### New: Certificate upload API (`POST /tax/certificates` + `certificate-uploads`)

> [!NOTE]
>
> The ECM surface now accepts writes. Submit a certificate document from a URL
> you control, poll the resulting upload, and read the certificates it
> produced. Pre-map the upload to a `reference_customer_id` and the exemption
> applies automatically on calculations for that customer.
>
> These endpoints are **live-only** (`sk_test_*` keys return
> `TESTMODE_NOT_SUPPORTED`) and require `X-API-Version: 2026-11-01` or later.

#### New Endpoints

> **[Submit Certificate](https://docs.numeral.com/api-reference/v2026-11-01/endpoint/submit_certificate)**
>
> Submit a document for asynchronous processing.
>
> * `POST /tax/certificates` — returns `201` with a `tax.certificate_upload`

> **[Certificate Uploads](https://docs.numeral.com/api-reference/v2026-11-01/endpoint/list_certificate_uploads)**
>
> Track processing jobs and retrieve the submitted document.
>
> * `GET /tax/certificate-uploads` — List with `status` and customer filters
> * `GET /tax/certificate-uploads/{upload_id}` — Get with short-lived `document_url`

#### The `tax.certificate_upload` object

An upload is a processing job; a certificate is its output. Uploads carry
their own status vocabulary, separate from certificate statuses:

| Object | Statuses |
| - | - |
| `tax.certificate_upload` | `processing`, `completed`, `failed`, `duplicate` |

`processing` is the only non-terminal state. `completed` populates
`certificate_ids`; `failed` populates `error_message`; `duplicate` is
assigned at submit time when the same document bytes and filename were
already submitted.

#### New error types

| Type | HTTP Code | Description |
| - | - | - |
| `INVALID_DOCUMENT_URL` | 400 | `document_url` is not a valid `https` URL, or points to a blocked, unresolvable, or private host |
| `DOCUMENT_FETCH_FAILED` | 400 | The document could not be downloaded |
| `DOCUMENT_URL_UNAUTHORIZED` | 422 | The document host returned 401/403 — usually an expired presigned URL |
| `UNSUPPORTED_FILE_TYPE` | 400 | The document is not a PDF, PNG, or JPEG |
| `FILE_TOO_LARGE` | 400 | The document exceeds 10 MB |
| `BUYER_IDENTITY_CONFLICT` | 409 | `customer.id` and `customer.reference_customer_id` resolve to different customers |
| `CERTIFICATE_UPLOAD_NOT_FOUND` | 404 | Certificate upload not found |

#### Notes

* Certificate type and jurisdiction are classified from the document.
  `POST /tax/certificates` does not accept a `certificate_type_id`.
* `document_url` on `GET /tax/certificate-uploads/{upload_id}` is a
  pre-signed URL with a 1-hour TTL and is omitted from list items by design.
* See the [Submit Certificates via API](https://docs.numeral.com/essentials/integration-guides/submit-certificates-via-api.md)
  guide for the end-to-end flow.

***

### New Feature: IP-Based Tax Resolution

> **IP Resolution**
>
> Pass a customer IP address instead of a full address for tax calculations.
>
> * Works with both `/tax/calculations` and `/tax/platform/calculations`
> * Supports IPv4 and IPv6 addresses

> **Resolution Modes**
>
> Control how insufficient IP data is handled.
>
> * `strict` - Error if insufficient detail
> * `zero` - Return zero-rate
> * `approximate` - Use most populous ZIP
> * `best_effort` - Try approximate, fall back to zero

***

#### The `customer.ip` Object

Instead of providing `customer.address`, you can now pass `customer.ip`:

```json theme={null}
{
  "customer": {
    "type": "CONSUMER",
    "ip": {
      "value": "217.217.113.167",
      "resolution": "strict"
    }
  }
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `value` | string | Yes | A valid IPv4 or IPv6 address |
| `resolution` | string | No | Resolution strategy (default: `"strict"`) |

> [!NOTE]
>
> At least one of `customer.address` or `customer.ip` must be provided. You can also provide both -- the address will be used first, with IP as a fallback.

#### Resolution Modes

| Mode | Behavior |
| - | - |
| `strict` (default) | Returns an error if IP cannot resolve to sufficient detail for tax calculation |
| `zero` | Returns a zero-rate response immediately if resolution is insufficient |
| `approximate` | Attempts to resolve the IP to an address using heuristics |
| `best_effort` | Tries approximate first, falls back to zero rates if that also fails |

> [!WARNING]
>
> Numeral strongly recommends using `strict` mode to stay as compliant as possible.

#### New Response Fields

Calculation responses now include these additional fields:

| Field | Type | Description |
| - | - | - |
| `location_source` | string | `"address"` or `"ip"` -- which input was used for tax determination |
| `resolution_precision` | string | Precision level: `STREET`, `POSTAL_PLUS`, `POSTAL`, `PROVINCE`, `COUNTRY`, or `APPROXIMATED` |
| `address_used` | object | The resolved address that was actually used for tax calculation |

#### Example: IP-Based Calculation

```bash theme={null}
curl -X POST https://api.numeralhq.com/tax/calculations \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "X-API-Version: 2026-11-01" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": {
      "type": "CONSUMER",
      "ip": {
        "value": "217.217.113.167",
        "resolution": "strict"
      }
    },
    "order_details": {
      "customer_currency_code": "USD",
      "tax_included_in_amount": false,
      "automatic_tax": "auto",
      "line_items": [
        {
          "product_category": "SAAS_GENERAL",
          "reference_line_item_id": "ip-test-001",
          "amount": 10000,
          "quantity": 1
        }
      ]
    }
  }'
```

**Response:**

```json theme={null}
{
  "testmode": true,
  "id": "calc_...",
  "object": "tax.calculation",
  "customer_currency_code": "USD",
  "line_items": [
    {
      "line_item_id": "li_...",
      "product": {
        "reference_line_item_id": "ip-test-001",
        "reference_product_id": "default-saas-general",
        "reference_product_name": "Default SAAS_GENERAL Product",
        "product_tax_code": "SAAS_GENERAL"
      },
      "tax_jurisdictions": [
        {
          "tax_rate": 0.06,
          "tax_due_decimal": 600,
          "fee_amount": 0,
          "rate_type": "GENERAL STATE SALES TAX",
          "tax_authority_name": "Pennsylvania",
          "tax_type": "SALES"
        },
        {
          "tax_rate": 0.01,
          "tax_due_decimal": 100,
          "fee_amount": 0,
          "rate_type": "GENERAL COUNTY LOCAL SALES TAX",
          "tax_authority_name": "ALLEGHENY",
          "tax_type": "SALES"
        }
      ],
      "tax_amount": 700,
      "amount_excluding_tax": 10000,
      "amount_including_tax": 10700,
      "quantity": 1
    }
  ],
  "total_tax_amount": 700,
  "tax_included_in_amount": false,
  "total_amount_excluding_tax": 10000,
  "total_amount_including_tax": 10700,
  "expires_at": 1771710514,
  "customer": { "type": "CONSUMER" },
  "automatic_tax": "auto",
  "address_resolution_status": "POSTAL_ONLY",
  "address_used": {
    "address_line_1": "",
    "address_line_2": "",
    "address_city": "",
    "address_province": "PA",
    "address_postal_code": "15212",
    "address_country": "US"
  },
  "location_source": "ip",
  "resolution_precision": "POSTAL"
}
```

#### New Error Types

| Type | HTTP Code | Description |
| - | - | - |
| `INVALID_IP_FORMAT` | 400 | `ip.value` is not a valid IPv4 or IPv6 address |
| `IP_RESOLUTION_FAILED` | 422 | IP could not be resolved to any country |
| `IP_RESOLUTION_INSUFFICIENT_US` | 422 | IP resolved to a US state but no postal code was found |
| `IP_RESOLUTION_INSUFFICIENT_CA` | 422 | IP resolved to CA but no province was found |

> **[Error Codes Reference](https://docs.numeral.com/api-reference/v2026-11-01/error-codes.md)**
>
> See the complete list of error types and their meanings.

***

### New Field: `transacted_at`

Calculations and platform calculations now accept an **optional** top-level `transacted_at` field — the time the transaction occurred, as a Unix timestamp in **seconds**. It determines which tax rates and rules apply at that point in time (for example, during a sales tax holiday). If omitted, the calculation uses the current time.

```json theme={null}
{
  "order_details": { /* ... */ },
  "transacted_at": 1772323200
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `transacted_at` | integer | No | Unix timestamp in **seconds** of when the transaction occurred. Defaults to the current time when omitted. |

***

### Migration Guide

#### From 2026-01-01 to 2026-03-01

> [!NOTE]
>
> There are **no breaking changes** from 2026-01-01. All existing requests continue to work as-is.

1. **Update your `X-API-Version` header** to `2026-03-01`
2. **Optionally use `customer.ip`** instead of or in addition to `customer.address`
3. **Handle new 422 error types** if using IP resolution with `strict` mode
4. **Check `location_source`** in responses to see whether address or IP was used

***

## Version 2026-01-01

> [!NOTE]
>
> **Release Date:** January 1, 2026
>
> This version introduces Merchant Management and Platform Calculations for marketplace and payment processor scenarios.

### New Endpoints

> **Merchant CRUD**
>
> Full create, read, update, and delete operations for merchants (sellers) on your platform.
>
> * `POST /tax/merchants` - Create merchant
> * `GET /tax/merchants` - List merchants
> * `GET /tax/merchants/:id` - Get merchant
> * `POST /tax/merchants/:id` - Update merchant
> * `DELETE /tax/merchants/:id` - Delete merchant

> **Platform Calculations**
>
> Tax calculations for marketplace transactions with merchant context.
>
> * `POST /tax/platform/calculations`

> **Platform Transactions**
>
> Convert platform calculations into recorded transactions for tax reporting.
>
> * `POST /tax/platform/transactions`

***

### Breaking Changes

> [!WARNING]
>
> **Amount is Per-Unit in Platform Calculations**
>
> The most significant change in this version is how `amount` is interpreted in the `/platform/calculations` endpoint.
>
> | Endpoint | Amount Behavior |
> | - | - |
> | `/tax/calculations` | Total line item value |
> | `/tax/platform/calculations` | **Per-unit price** |
>
> **Example:**
>
> ```json theme={null}
> // For 3 items at $25.00 each
>
> // Standard calculations (amount = total)
> {
>   "amount": 7500,  // $75.00 total
>   "quantity": 3
> }
>
> // Platform calculations (amount = per-unit)
> {
>   "amount": 2500,  // $25.00 per unit
>   "quantity": 3    // taxable base = $75.00
> }
> ```

***

### New Features

#### Merchant Management

Create and manage merchants (sellers) on your platform:

```bash theme={null}
curl -X POST https://api.numeralhq.com/tax/merchants \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "X-API-Version: 2026-01-01" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_merchant_id": "seller-123",
    "name": "Acme Seller",
    "default_address": {
      "address_line_1": "123 Main St",
      "address_city": "San Francisco",
      "address_province": "CA",
      "address_postal_code": "94105",
      "address_country": "US"
    }
  }'
```

Key features:

* Use your own IDs via `reference_merchant_id`
* Store merchant tax IDs for B2B transactions
* Default addresses used as origin in calculations
* Testmode isolation (test merchants separate from live)

#### Platform Calculations

Calculate taxes for marketplace/platform transactions:

```bash theme={null}
curl -X POST https://api.numeralhq.com/tax/platform/calculations \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "X-API-Version: 2026-01-01" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": {
      "address": { ... }
    },
    "merchant": {
      "merchant_id": "seller-123"
    },
    "order_details": {
      "customer_currency_code": "USD",
      "tax_included_in_amount": false,
      "line_items": [
        { "amount": 2500, "quantity": 3, "product_category": "GENERAL_MERCHANDISE" }
      ]
    },
    "roles": ["marketplace_provider"]
  }'
```

Key features:

* **Per-unit pricing** - `amount × quantity = taxable base`
* **Merchant context** - Link calculations to specific sellers
* **Platform roles** - Specify your role (marketplace, payment processor, merchant of record)
* **Fee calculations** - Separate tax calculation for platform fees
* **Enhanced response** - Includes `tax_authority_name`, `tax_authority_type`, `tax_type`

#### Platform Roles

Specify your role in the transaction:

| Role | Description |
| - | - |
| `marketplace_provider` | You operate a marketplace connecting buyers and sellers |
| `payment_processor` | You process payments for the transaction |
| `merchant_of_record` | You are the legal seller of record |

#### Platform Transactions

Convert platform calculations into recorded transactions for tax reporting and filing:

```bash theme={null}
curl -X POST https://api.numeralhq.com/tax/platform/transactions \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "X-API-Version: 2026-01-01" \
  -H "Content-Type: application/json" \
  -d '{
    "platform_calculation_id": "plat_calc_abc123",
    "reference_order_id": "order_12345",
    "reference_payment_id": "pay_67890",
    "transaction_processed_at": 1736300000
  }'
```

Key features:

* **Returns a list** - May return one or two transactions depending on your platform role
* **Payment processors** - Receive separate `order` and `fee` transactions
* **Marketplace providers / Merchants of record** - Receive a single `order` transaction
* **Transaction types** - `type: "order"` for sales, `type: "fee"` for platform fees

| Platform Role | Transactions Created |
| - | - |
| `payment_processor` | 2 (order + fee) |
| `marketplace_provider` | 1 (order only) |
| `merchant_of_record` | 1 (order only) |

#### Fee Calculations

Calculate taxes on platform fees separately from order items:

```json theme={null}
{
  "order_details": { ... },
  "fee_details": {
    "line_items": [
      { "amount": 299, "quantity": 1, "product_category": "ELECTRONIC_GOODS" }
    ]
  }
}
```

The response includes separate totals:

```json theme={null}
{
  "totals": {
    "order": { "tax_amount": 875 },
    "fees": { "tax_amount": 26 }
  }
}
```

***

### Enhanced Tax Response

Platform calculations include additional jurisdiction details:

```json theme={null}
{
  "tax_jurisdictions": [
    {
      "tax_rate": 0.0875,
      "rate_type": "SALES TAX",
      "jurisdiction_name": "California",
      "tax_authority_name": "California State",
      "tax_authority_type": "STATE",
      "tax_type": "SALES"
    }
  ]
}
```

| Field | Description |
| - | - |
| `tax_authority_name` | Human-readable authority name |
| `tax_authority_type` | `STATE`, `COUNTY`, `CITY`, or `DISTRICT` |
| `tax_type` | `SALES`, `USE`, `VAT`, or `GST` |

***

### New Error Format

> [!WARNING]
>
> **Breaking Change: Simplified Error Response**
>
> Error responses now use a flattened structure instead of the nested `error` object from previous versions.

**Previous format (2025-05-12 and earlier):**

```json theme={null}
{
  "error": {
    "code": "product_not_found",
    "message": "Product not found"
  }
}
```

**New format (2026-01-01+):**

```json theme={null}
{
  "code": 400,
  "type": "PRODUCT_NOT_FOUND",
  "message": "Product not found"
}
```

Key changes:

* **Flattened structure** - No more nested `error` object
* **HTTP status in response** - `code` field contains the HTTP status code
* **Uppercase error types** - Error types are now SCREAMING\_SNAKE\_CASE
* **Consistent typing** - Use the `type` field for programmatic error handling

> **[Error Codes Reference](https://docs.numeral.com/api-reference/v2026-11-01/error-codes.md)**
>
> See the complete list of error types and their meanings.

***

### Previous Versions

| Version | Key Features |
| - | - |
| 2026-01-01 | Merchant management, platform calculations, per-unit pricing, fee calculations |
| 2025-05-12 | Customer types (B2B/B2C), tax IDs, 32 currencies, automatic\_tax |
| 2024-09-01 | Base version with core tax calculation features |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.