> ## 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

## Version 2026-03-01

<Info>
  **Release Date:** March 1, 2026

  This version adds IP-based tax resolution to calculations and platform calculations, plus a headless ECM read API for managing exemption certificates and certificate requests.
</Info>

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

<Info>
  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.
</Info>

#### New Endpoints

<CardGroup cols={2}>
  <Card title="Certificate Requests" icon="envelope-open-text">
    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)
  </Card>

  <Card title="Certificates" icon="file-certificate">
    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`
  </Card>
</CardGroup>

#### 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 |

<Card title="Error Codes Reference" icon="triangle-exclamation" href="/api-reference/v2026-03-01/error-codes">
  See the full list of error types and their meanings.
</Card>

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

***

### New Feature: IP-Based Tax Resolution

<CardGroup cols={2}>
  <Card title="IP Resolution" icon="globe">
    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
  </Card>

  <Card title="Resolution Modes" icon="sliders">
    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
  </Card>
</CardGroup>

***

#### 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.
</Note>

#### 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.
</Warning>

#### 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-03-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            |

<Card title="Error Codes Reference" icon="triangle-exclamation" href="/api-reference/v2026-03-01/error-codes">
  See the complete list of error types and their meanings.
</Card>

***

### 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

<Info>
  There are **no breaking changes** from 2026-01-01. All existing requests continue to work as-is.
</Info>

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

<Info>
  **Release Date:** January 1, 2026

  This version introduces Merchant Management and Platform Calculations for marketplace and payment processor scenarios.
</Info>

### New Endpoints

<CardGroup cols={2}>
  <Card title="Merchant CRUD" icon="store">
    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
  </Card>

  <Card title="Platform Calculations" icon="calculator">
    Tax calculations for marketplace transactions with merchant context.

    * `POST /tax/platform/calculations`
  </Card>

  <Card title="Platform Transactions" icon="receipt">
    Convert platform calculations into recorded transactions for tax reporting.

    * `POST /tax/platform/transactions`
  </Card>
</CardGroup>

***

### 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
  }
  ```
</Warning>

***

### 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.
</Warning>

**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

<Card title="Error Codes Reference" icon="triangle-exclamation" href="/api-reference/v2026-03-01/error-codes">
  See the complete list of error types and their meanings.
</Card>

***

### 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                                |
