> This page is for version V1 (default).
> For other versions, use one of these documentation indexes:
> - V1 (default): https://docs.ottimate.com/v-1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.ottimate.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ottimate.com/_mcp/server.

# Catalog Entries

A catalog entry represents an item from your inventory management system stored in Ottimate. Each catalog entry is associated with a specific vendor and company, enabling accurate invoice matching and price variance detection.

When catalog entries are successfully synchronized, Ottimate automatically matches invoice line items to catalog items and displays match suggestions, including pricing variances, in the item validation interface.

## Common Workflows

This guide covers four primary catalog management workflows that can be enabled via API.

1. [**Syncing Catalog Data**](#syncing-catalog-data) - Push inventory items from your system to Ottimate
2. [**Fetching Purchased Items**](#fetching-purchased-items-for-a-catalog-entry) - Retrieve all invoice items matched to a catalog entry
3. [**Fetching Matched Catalog Entries**](#fetching-matched-catalog-entries-for-purchased-items) - Find which catalog entry an invoice item is matched to
4. [**Location-based pricing**](#advanced-pricing-configurations) - Sync location-specific prices (requires Ottimate setup)

---

## Syncing Catalog Data

Syncing your inventory catalog to Ottimate enables automatic invoice matching and price variance detection. This workflow is designed for inventory management systems to maintain an up-to-date catalog in Ottimate.

### Single Item Sync

Create a single catalog entry using `POST /v1/catalog/entries`:

```json
POST /v1/catalog/entries
Content-Type: application/json

{
  "ottimate_company_id": 15196,
  "erp_vendor_id": "9999",
  "item_name": "HEINZ KC BBQ BAKED BEANS",
  "sku": "SKU-BBB-001",
  "upc": "1300001102",
  "size": "12.0/16 OZ",
  "uom": "Case (C)",
  "price": 19.500000,
  "unit_price": 1.620000,
  "reference_id": "1300001102"
}
```

**Response (201 Created):**

```json
{
  "id": "cte_12345",
  "ottimate_company_id": 15196,
  "reference_id": "1300001102",
  "item_name": "HEINZ KC BBQ BAKED BEANS",
  "description": "HEINZ KC BBQ BAKED BEANS",
  "sku": "SKU-BBB-001",
  "size": "12.0/16 OZ",
  "uom": "Case (C)",
  "price": 19.500000,
  "unit_price": 1.620000,
  "erp_vendor_id": "9999",
  "ottimate_vendor_name": "Sysco Foods",
  "display_name": "HEINZ KC BBQ BAKED BEANS - Sysco",
  "created_date": "2024-12-05T10:30:00Z",
  "last_modified_date": "2024-12-05T10:30:00Z"
}
```

### Required Fields

| Field                 | Type    | Description                                                                                                                                                                                                                                                |
| --------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ottimate_company_id` | Integer | The Ottimate-assigned company ID. Retrieve via `GET /v1/accounts/{id}/companies`                                                                                                                                                                           |
| `erp_vendor_id`       | String  | ERP-assigned vendor identifier. Must be consistent with the `erp_vendor_id` used to create an accounting vendor in Ottimate. Verify via `GET /v1/vendors`                                                                                                  |
| `item_name`           | String  | Item name. Populates "Item Name" in the UI and powers AI-based matching. Technically one of `item_name` or `description` must be present — the request is only rejected if both are missing — but `description` is deprecated, so always send `item_name`. |

### Recommended Fields

Including these fields significantly improves matching accuracy:

| Field          | Type    | Description                                                                                                                                                                                                                    |
| -------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `reference_id` | String  | Your system's unique identifier for this item.                                                                                                                                                                                 |
| `sku`          | String  | Stock-Keeping Unit from your inventory system                                                                                                                                                                                  |
| `upc`          | String  | 12-digit Universal Product Code. If Should always be provided if available to improve matching accuracy                                                                                                                        |
| `size`         | String  | Pack size using format: `{case size}/{unit size}` (e.g., "12.0/16 OZ" indicates 12 units in the case, and each unit is 16 fluid ounces)                                                                                        |
| `uom`          | String  | Unit of measure (e.g., "CASE", "EA", "LB")                                                                                                                                                                                     |
| `price`        | Decimal | Pack price in USD. Accepts decimals up to 5 digits. (e.g. 10.00000)                                                                                                                                                            |
| `unit_price`   | Decimal | Price per individual unit within the pack.                                                                                                                                                                                     |
| `prices`       | Array   | Prices for this entry (see [Location-based pricing](#advanced-pricing-configurations)). Optional, but omitting is deprecated — the entry is still created, and the response includes a `warnings` field flagging the omission. |
| `allowances`   | Array   | Allowances for this entry. Optional, with the same omission-warning behavior as `prices`.                                                                                                                                      |

For the full list of fields, see [POST /v1/catalog/entries](/api-reference/api-reference/catalog/post-catalog-entries).

### Bulk Sync

For syncing large catalogs (1,000+ items), use the bulk endpoint `POST /v1/catalog/entries/bulk`:

```json
POST /v1/catalog/entries/bulk
Content-Type: application/json

{
  "ottimate_company_id": 15196,
  "entries": [
    {
      "erp_vendor_id": "9999",
      "item_name": "HEINZ KC BBQ BAKED BEANS",
      "sku": null,
      "upc": "1300001102",
      "size": "12.0/16 OZ",
      "uom": "Case (C)",
      "price": 12.99,
      "reference_id": "1300001102"
    },
    {
      "erp_vendor_id": "9999",
      "item_name": "BACON BITS PER LB",
      "sku": null,
      "upc": "23585000000",
      "size": "1.0/LB",
      "uom": "Weight (W)",
      "price": 3.99,
      "catalog_unique_key": "1300001102"
    }
    // ... up to 1000 entries per request
  ]
}
```

**Response (202 Accepted):**

This endpoint is always asynchronous. It validates your request, queues the work, and immediately returns a batch handle — it never returns per-item results directly.

```json
{
  "version": "1.0.0",
  "status": "processing",
  "batch_id": "bch_abc123",
  "batch_url": "/v1/batch/bch_abc123/progress",
  "results_url": "/v1/batch/bch_abc123/results",
  "summary": {
    "count": 2
  }
}
```

Poll `batch_url` (`GET /v1/batch/{id}/progress`) until `status` is `completed`, then call `results_url` (`GET /v1/batch/{id}/results`) to get the per-entry outcomes (created/updated/error) for the batch. See the Batch API reference for the full polling contract.

### Understanding the upsert pattern for bulk operations

Bulk catalog synchronization uses an **upsert (update or insert) pattern** to prevent duplicates. Ottimate automatically derives a `catalog_unique_key` for every entry — exactly which fields it's built from depends on your company's catalog matching tier and ERP integration:

**Starter/default tier** always uses:

```
catalog_unique_key = erp_vendor_id + item_name + sku + pack_size
```

UPC has no effect on this formula for the Starter tier.

**Growth/Premium tier** (and some ERP integrations, e.g. Acumatica/Pyxis, regardless of tier) use UPC-based formulas instead:

```
catalog_unique_key = erp_vendor_id + upc + buying_format   # Growth/Premium
catalog_unique_key = erp_vendor_id + upc                   # Acumatica/Pyxis
```

For these UPC-based formulas specifically: if the entry being submitted has no `upc` value, the key falls back to the Starter-tier formula above (`erp_vendor_id + item_name + sku + pack_size`) for that entry.

In all cases, the key that's actually stored is always this auto-derived value — if you pass your own `catalog_unique_key` in the request (as shown below), it's used only to look up a matching row for the upsert; it does not become the entry's stored key.

When you POST to `/catalog/entries/bulk`:

* **If the `catalog_unique_key` doesn't exist** → Creates a new catalog entry
* **If the `catalog_unique_key` exists** → Updates the existing catalog entry

This lets you safely sync your entire catalog repeatedly without creating duplicates.

> **Warning**
>
> To avoid duplicating items through an upsert operation, keep `erp_vendor_id`, `item_name`, `size`, `uom`, `upc` consistent, or use `PATCH catalog/entries/{entry_id}` to update entries individually.

### Updating a Catalog Entry

Update an existing entry with `PATCH /v1/catalog/entries/{id}`. Only the fields you include are changed — omitted fields keep their current value:

```json
PATCH /v1/catalog/entries/cte_12345
Content-Type: application/json

{
  "price": 26.99,
  "size": "6 CT"
}
```

`catalog_unique_key` is accepted on this request but has no effect — the entry's key is always regenerated from your company's key format on save (see [Understanding the upsert pattern](#understanding-the-upsert-pattern-for-bulk-operations) above). A value you submit here does not rename or override the stored key.

`prices` and `allowances` cannot be changed through this endpoint. They can only be set when the entry is created — sending either field to `PATCH /v1/catalog/entries/{id}` returns a 400. To add prices or allowances after creation, use the standalone endpoints (`POST /v1/catalog/prices`, `POST /v1/catalog/allowances`); existing allowances can be updated with `PATCH /v1/catalog/allowances/{id}`.

### Retrieving Catalog Entries

Before syncing, you may want to check what's already in Ottimate:

**List all entries for a company:**

```http
GET /v1/catalog/entries?ottimate_company_id=15196&limit=100
```

`ottimate_company_id` is optional — if omitted, results are scoped to all companies the caller has access to.

**Search for specific items:**

```http
GET /v1/catalog/entries?ottimate_company_id=15196&search=tomato&erp_vendor_id=Sysco
```

`search` matches against item name, description, `sku`, and `upc`.

**Get full details for a specific entry:**

```http
GET /v1/catalog/entries/{id}
```

Prices and allowances are included whenever the entry has any — there is no `expand` parameter on this endpoint. If the entry has no prices or no allowances, the corresponding field is omitted from the response entirely rather than returned as an empty array.

On the list endpoint (above), `expand=prices` or `expand=allowances` returns full entry objects with **both** prices and allowances included; the two values can't be selected independently.

---

## Fetching Purchased Items for a Catalog Entry

Once catalog entries are matched to invoice items, you can retrieve all purchased items associated with a specific catalog entry. This is useful for:

* Analyzing purchase history for a specific catalog item
* Identifying price variances over time

### Endpoint

```http
GET /v1/catalog/variances/{catalog_entry_id}/invoice-items
```

### Example Request

```http
GET /v1/catalog/variances/cte_12345/invoice-items?ottimate_company_id=15196
```

### Response

```json
{
  "count": 150,
  "next": "https://api.ottimate.com/v1/catalog/variances/cte_12345/invoice-items?page=2",
  "previous": null,
  "results": [
          {
            "id": 821125897,
            "invoice_id": 139851296,
            "invoice_number": "I-1767901026-2",
            "ottimate_location_id": 51582,
            "item_name": "Sample Item 821125897",
            "price": 1.200000,
            "quantity": 28.000000,
            "invoice_date": "2026-01-11",
            "price_variance": 0.19999999999999996,
            "effective_price": 1.1928571428571428
        },
        {
            "id": 821125898,
            "invoice_id": 139851297,
            "invoice_number": "I-1767901026-2",
            "ottimate_location_id": 51582,
            "item_name": "Sample Item 821125897",
            "price": 1.000000,
            "quantity": 12.000000,
            "invoice_date": "2026-01-04",
            "price_variance": null,
            "effective_price": 1.0
        }
    // ... more purchased items
  ]
}
```

### Purchased Items

The result object includes purchased item objects with the following fields:

| Field             | Description                                                                                                                      |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | Ottimate-generated ID for the invoice line item.                                                                                 |
| `invoice_id`      | Ottimate-generated identifier for the invoice. Can be used to fetch invoice details through `GET /v1/invoices/{invoice_id}`      |
| `invoice_number`  | Vendor-issued Invoice Number extracted from the original invoice document                                                        |
| `item_name`       | Item name extracted from the invoice                                                                                             |
| `price`           | Actual item price on the invoice                                                                                                 |
| `effective_price` | Actual item price - any applicable allowances and discounts                                                                      |
| `price_variance`  | Overbilling/underbilling depending on the difference between item price on the invoice, catalog price, adjusting for allowances. |

---

## Fetching Matched Catalog Entries for Purchased Items

When reviewing invoices, you may need to see which catalog entries are matched to invoice line items.

### Invoice List with Expansion

Retrieve invoices with catalog matching information using the `expand=items` parameter. To set the `expand` parameter, you must also limit the date range to 30 days.

Note that only invoice items matched to a catalog entry will return a catalog entry object.

```http
GET /v1/invoices?date_from=2025-12-15&date_to=2026-01-14&expand=items
```

### Example Response

```json
{
            "id": "124425480",
            "invoice_number": "202601050949",
            "type": "invoice",
            "ottimate_vendor_name": "PETE-MA-01",
            "ottimate_vendor_id": "833906",
            "erp_vendor_id": "PETE-MA-01",
            "ottimate_location_name": "Sample Location",
            "ottimate_location_id": "48764",
            "created_at": "2026-01-05T06:49:24.609646-08:00",
            "invoice_date": "2026-01-05",
            "due_date": "2026-01-30",
            "total_amount": 155.00,
            "total_tax": 0.00,
            "status": "ready-for-export",
            "payment_status": "unpaid",
            "error": null,
            "currency": "USD",
            "outstanding_balance": 155.00,
            "purchase_order": "",
            "has_unmatched_items": null,
            "approved_date": "2026-01-06T11:40:07.536994-08:00",
            "exported_date": null,
            "on_hold": {
                "is_held": true,
                "reason": ""
            },
            "posting_date": null,
            "items": [
                {
                    "line": "735920103",
                    "ottimate_item_id": "34551621",
                    "name": "t-shirt xl",
                    "display_name": "t-shirt xl",
                    "sku": "",
                    "type": "item",
                    "quantity": 1.0,
                    "unit": "Each",
                    "unit_price": 155.00,
                    "line_total": 155.00,
                    "charge_type": "",
                    "is_taxed": false,
                    "pack_size": null,
                    "uom": null,
                    "catalog_entry": {
                        "id": "cte_TQoHfgJ9S8mT",
                        "ottimate_company_id": 15365,
                        "reference_id": "pbwteelogoxl",
                        "catalog_unique_key": "petema01-pbwteelogoxl-10each-petesbwshoptshirtxl",
                        "item_name": "Pete's B&W Shop T-Shirt - XL",
                        "description": "Pete's B&W Shop T-Shirt - XL",
                        "original_description": "Pete's B&W Shop T-Shirt - XL",
                        "sku": "PBW-TEE-LOGO-XL",
                        "upc": "",
                        "size": "1.0/Each",
                        "uom": "Each",
                        "price": 11.000000,
                        "unit_price": 11.000000,
                        "last_purchased_price": null,
                        "erp_vendor_id": "PETE-MA-01",
                        "ottimate_vendor_name": "",
                        "is_split_case": false,
                        "authorized_vendor": true,
                        "last_purchased_date": "",
                        "created_date": "2025-12-23T08:11:30.213448-08:00",
                        "last_modified_date": "2025-12-23T08:11:30.213432-08:00",
                        "dimensions": "",
                        "properties": "",
                        "display_name": "pete's b&w shop t-shirt - xl (PBW-TEE-LOGO-XL)",
                        "prices": "",
                        "allowances": ""
                    },
                    "custom_fields": {},
                    "dimensions": {}
                }
            ]
        }
}
```

---

## Advanced Pricing Configurations

> **Warning**
>
> **Setup Required**: Location-specific pricing and allowances require additional configuration during client onboarding. Contact your Ottimate implementation team before using these features.

### Location-Specific Prices

An Ottimate implementation may include setting up multiple locations. By default, catalog prices apply to all locations within a company. For clients with store-based pricing, you can set different prices for different Ottimate locations or location groups.

#### Configuration Requirements

Before implementing location-specific prices:

1. Confirm the client company is configured for location-based catalog pricing
2. Obtain the `ottimate_location_id` or `ottimate_group_id` values via `GET /v1/accounts/{account_id}/locations`

#### Creating Location-Specific Prices

Include the `prices` array when creating catalog entries in place of the `price` field:

```json
POST /v1/catalog/entries
Content-Type: application/json

{
    "ottimate_company_id": 123456,
    "erp_vendor_id": "PETE-MA-01",
    "item_name": "Curry Paste Namya 1 kg",
    "sku": "6340260",
    "reference_id": "1341219125",
    "upc": "1341219125",
    "unit_price": 1.00,
    "size":"12.0/1  kg",
    "uom":"Case (C)",
    "prices":[
        {
            "price":60.00000,
            "price_type":"location",
            "ottimate_location_id":123,
            "start_date":"2025-12-23"
        },
        {
            "price":61.00000,
            "price_type":"location",
            "ottimate_location_id":456,
            "start_date":"2026-01-01"
        }
    ]
}
```

#### Price Object Fields

| Field                  | Type     | Required | Description                                       |
| ---------------------- | -------- | -------- | ------------------------------------------------- |
| `price`                | Decimal  | Yes      | Pack price in USD for this location               |
| `unit_price`           | Decimal  | No       | Optional unit price applicable to this location   |
| `ottimate_location_id` | Integer  | No       | Specific location ID (use OR `ottimate_group_id`) |
| `ottimate_group_id`    | Integer  | No       | Location group ID (use OR `ottimate_location_id`) |
| `start_date`           | DateTime | No       | Date when price becomes effective                 |
| `end_date`             | DateTime | No       | Date when price expires (null = no expiration)    |

> **Note**
>
> Location-based prices can be created either directly on `POST /v1/catalog/entries` (as shown above, via `price_type: "location"` in the `prices` array) or through the bulk upsert endpoint `POST /v1/catalog/entries/bulk` — both accept the same `prices` shape.

### Managing Prices Separately

Prices can also be listed and created independently of their catalog entries.

**List prices for a catalog entry:**

```http
GET /v1/catalog/prices?ottimate_company_id=15196&catalog_entry_id=cte_12345
```

Filter by location, effective date, or price type:

```http
GET /v1/catalog/prices?ottimate_company_id=15196&ottimate_location_id=456&effective_date=2026-01-01&price_type=location
```

`ottimate_company_id` is optional — if omitted, results are scoped to all companies the caller has access to.

**Create a price:**

```http
POST /v1/catalog/prices
Content-Type: application/json

{
  "catalog_entry_id": "cte_12345",
  "ottimate_company_id": 15196,
  "price": 25.99,
  "price_type": "location",
  "ottimate_location_id": 456,
  "start_date": "2024-01-01T12:00:00Z"
}
```

`ottimate_location_id` is required when `price_type` is `location`, and `ottimate_group_id` is required when `price_type` is `location_group` — omitting the matching ID returns a 400. Supplying either ID when `price_type` is `company` is accepted but silently ignored. Supplying both `ottimate_location_id` and `ottimate_group_id` together always returns a 400 ("Cannot specify both..."), regardless of `price_type`.

If `end_date` is omitted, it defaults to 36,500 days (\~100 years) after `start_date` rather than staying unset.

`start_date`/`end_date` are stored as the calendar date of the submitted timestamp (UTC).

A price's `reference_id` is always generated by Ottimate from the price's own identifying fields (catalog entry, effective date, price type, target and amount). It is read-only: `reference_id` is not accepted on price requests, and a value sent with one is ignored. Read it from the response rather than tracking a value of your own.

This endpoint has no update or delete counterpart — to change a price, create a new one with the desired `start_date`/`end_date`.

## Related Endpoints

* [**Vendors API**](/api-reference/api-reference/vendors) - Manage vendor relationships required for catalog entries
* [**Dimensions API**](/api-reference/api-reference/dimensions) - Configure accounting dimensions for catalog items
* [**Invoices API**](/api-reference/api-reference/invoices) - Work with invoice data and matched catalog entries