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

# Purchase Orders

A **purchase order (PO)** is a document issued by a buyer to a vendor that specifies the items, quantities, and agreed-upon prices for goods or services. POs serve as the authoritative record of what was ordered and at what cost.

> **Info**
>
> **Source of truth:** Purchase orders originate in your source system (ERP). Ottimate syncs POs from your ERP and stores them for document matching — your ERP remains the authoritative source for purchase order data.

## How purchase orders work in Ottimate

When a PO is created in your source system, you sync it to Ottimate using the Purchase Orders API. Once in Ottimate, POs are used to match against invoices to validate that billed amounts align with what was ordered.

Each PO can be configured for one of two matching workflows:

* **Three-way matching** (default) — The PO is matched against both a receipt and an invoice. This ensures that what was ordered, received, and billed all agree before payment is released.
* **Two-way matching** — The PO is matched directly against an invoice, skipping the receipt step. Set `is_2_way: true` when creating the PO. This is common for services or other scenarios where delivery verification is not required.

## PO status values

Ottimate tracks the lifecycle of each PO with the following statuses:

| Status     | Description                                |
| ---------- | ------------------------------------------ |
| `pending`  | PO has been received but not yet processed |
| `open`     | PO is active and available for matching    |
| `received` | All items on the PO have been received     |
| `closed`   | PO has been fully matched and closed       |
| `flagged`  | PO requires attention or review            |
| `archived` | PO is no longer active                     |
| `deleted`  | PO has been removed                        |

## Key fields

| Field                  | Description                                                                                                                                                                                                                                                                                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `po_number`            | PO number from your source system. Required — Ottimate does not auto-generate PO numbers.                                                                                                                                                                                                                                                                                      |
| `external_id`          | Your system's unique identifier for this PO. On the [bulk endpoint](#bulk-operations), a PO with a matching `external_id` (together with `erp_vendor_id` and `ottimate_location_id`) is updated instead of duplicated. `POST /v1/purchase-orders` does not use `external_id` for this — it rejects the request if `po_number` already exists for the same vendor and location. |
| `erp_vendor_id`        | Vendor identifier from your ERP system                                                                                                                                                                                                                                                                                                                                         |
| `erp_vendor_name`      | Vendor display name from your ERP                                                                                                                                                                                                                                                                                                                                              |
| `ottimate_location_id` | The Ottimate location this PO belongs to                                                                                                                                                                                                                                                                                                                                       |
| `is_2_way`             | Set to `true` for two-way matching (no receipt required)                                                                                                                                                                                                                                                                                                                       |
| `custom_fields`        | Custom fields configured for this company's purchase orders                                                                                                                                                                                                                                                                                                                    |
| `items`                | Array of line items, each with `name`, `quantity`, `price`, and optional `sku`, `uom`, and `dimensions`                                                                                                                                                                                                                                                                        |

### Item-level dimensions

It is common for PO line items to carry dimension mappings (e.g., GL account, department, project) from the source ERP. When an invoice is matched to a PO, Ottimate can inherit these dimensions onto the invoice line items — so coding happens automatically based on what was already set on the purchase order.

Dimensions are passed as key-value pairs on each item, keyed by dimension type (e.g., `DEPARTMENT`, `CLASS`). Each value must be a **plain string** — the ERP dimension code or ID (e.g., `"erp_dim_dept123"`):

```json
{
  "items": [
    {
      "name": "Tomatoes",
      "quantity": 100,
      "price": 2.5,
      "dimensions": {
        "DEPARTMENT": "erp_dim_dept123",
        "CLASS": "erp_dim_class456"
      }
    }
  ]
}
```

> **Warn**
>
> **The `dimensions` format in POST/PATCH requests differs from GET responses.** GET responses return dimensions as rich objects (with `id`, `erp_dimension_id`, `name`, and `code` fields). Copying dimensions from a GET response and passing them directly to a POST or PATCH request will return a `PO4000` validation error: *"Dimension value for 'X' must be a string."* Always use the plain string value (the ERP code or ID) when writing dimensions.

> **Warn**
>
> **Dimensions must exist in Ottimate before they can be referenced on a PO.** Dimensions are typically synced from your ERP via an Ottimate-hosted integration, or created directly using the [Dimensions API](/api-reference/api-reference/dimensions/post-dimensions-root). If a PO request references a dimension that does not exist in Ottimate, the request will return a `400` error.

When creating or updating a PO, providing `dimensions` on a line item **replaces all existing dimensions** on that item — include every dimension you want to keep. Omit the field or pass `null` to leave dimensions unchanged.

## API endpoints

| Endpoint                                                                                                  | Description                                                                             |
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| [`GET /v1/purchase-orders`](/api-reference/api-reference/purchase-orders/get-purchase-orders-root)        | List purchase orders with filters for company, location, vendor, status, and date range |
| [`GET /v1/purchase-orders/{id}`](/api-reference/api-reference/purchase-orders/get-purchase-orders-id)     | Get a specific purchase order with all line items                                       |
| [`POST /v1/purchase-orders`](/api-reference/api-reference/purchase-orders/post-purchase-orders-root)      | Create a single purchase order                                                          |
| [`PATCH /v1/purchase-orders/{id}`](/api-reference/api-reference/purchase-orders/patch-purchase-orders-id) | Update an existing purchase order                                                       |
| [`POST /v1/purchase-orders/bulk`](/api-reference/api-reference/purchase-orders/post-purchase-orders-bulk) | Bulk create or update purchase orders                                                   |

### Expanding line items in list results

By default, `GET /v1/purchase-orders` list results do not include each PO's `items` array. Pass `expand=items` to include full line items inline in the list response. When `expand=items` is used, both `date_from` and `date_to` are required and must be within a 30-day window — omitting either or exceeding 30 days returns a `400` error.

## Bulk operations

The bulk upsert endpoint allows you to create or update multiple POs in a single request. This operation is processed asynchronously.

**Rate limits:**

* Maximum **100 purchase orders** per request
* Maximum **1,000 total line items** across all POs in a single request

The response returns a `batch_id` that you can use with the [Batch progress](/api-reference/api-reference/batch/get-batch-id-progress) and [Batch results](/api-reference/api-reference/batch/get-batch-id-results) endpoints to track the status of your bulk operation.

## Updating purchase orders

When updating a PO via `PATCH`, note the following restrictions:

* You **cannot** change `status` — including it in the request body returns a `400` error
* You **cannot** change `po_number`, `external_id`, `erp_vendor_id`, or `ottimate_location_id` — including one of these with a different value does **not** return an error; the request succeeds and the field is silently ignored, leaving the existing value unchanged
* To update existing line items, include the item `id` in the items array
* To add new line items, omit the `id` field — Ottimate will create them automatically