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

# Updating invoices

Once an invoice exists in Ottimate — whether it was extracted from an uploaded document or pushed in through the API — you can correct it with [`PATCH /v1/invoices/{id}`](/api-reference/api-reference/invoices/patch-invoices-id). A single request can update the header, create and update line items, and replace attached images atomically.

> **Warn**
>
> **You cannot change an invoice's `status` with this endpoint.** Invoice `status` is derived by Ottimate from the invoice's underlying state. To move an invoice through the lifecycle, use the approval endpoints, [`POST /v1/invoices/mark-exported`](/api-reference/api-reference/invoices/post-invoices-mark-exported), or [`POST /v1/invoices/{id}/flag`](/api-reference/api-reference/invoices/post-invoices-id-flag). See [Invoice lifecycle](/invoice-lifecycle).
>
> Sending `status` here is **accepted and then silently ignored**, which is easy to mistake for success:
>
> * `{"status": "archived"}` or `{"status": "verified"}` returns `200` and leaves the invoice completely unchanged
> * Any other value returns `400` — `"exported" is not a valid choice.`
>
> So the field is validated against the ingestion-only `archived`/`verified` enum but has no effect on an update. That enum is only meaningful on **creation** — see [Bulk invoice operations](/invoice-bulk-operations).

## Header fields

Every field is optional on `PATCH` — send only what you want to change.

| Field                  | Type    | Description                                                                                                             |
| ---------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
| `ottimate_location_id` | integer | The Ottimate location this invoice belongs to.                                                                          |
| `erp_vendor_id`        | string  | Vendor identifier from your ERP. If no matching vendor exists, Ottimate creates one — which requires `erp_vendor_name`. |
| `erp_vendor_name`      | string  | Vendor display name. Required only when `erp_vendor_id` refers to a vendor that does not yet exist. Max 200 characters. |
| `invoice_number`       | string  | Vendor's invoice number.                                                                                                |
| `invoice_date`         | date    | Invoice date (`YYYY-MM-DD`).                                                                                            |
| `due_date`             | date    | Payment due date.                                                                                                       |
| `posting_date`         | date    | GL posting date.                                                                                                        |
| `total_amount`         | decimal | Invoice total.                                                                                                          |
| `total_tax`            | decimal | Total tax on the invoice.                                                                                               |
| `discount_amount`      | decimal | Total discount applied.                                                                                                 |
| `purchase_order`       | string  | PO number to match against.                                                                                             |
| `notes`                | string  | Free-text notes. Max 1024 characters.                                                                                   |
| `currency`             | string  | 3-letter currency code.                                                                                                 |
| `type`                 | string  | `invoice` or `credit_memo`. Any other value returns a `400`.                                                            |
| `dimensions`           | object  | Header-level dimension mappings.                                                                                        |
| `custom_fields`        | object  | Header-level custom metadata. Requires account-level configuration.                                                     |
| `items`                | array   | Line items to create or update. See [Line Items](/line-items).                                                          |
| `image_urls`           | array   | Replace all invoice images with these HTTPS URLs.                                                                       |
| `raw_images`           | array   | Replace all invoice images with these base64-encoded files.                                                             |

## Dimensions replace at the header, merge on items

This is the most common source of surprise on this endpoint:

| Level               | Behaviour                                                                                                                                                       |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Header `dimensions` | **Replaces** the entire stored set. Include every dimension you want to keep. Pass `{}` to clear all header dimensions. Omit the field to leave them unchanged. |
| Item `dimensions`   | **Merges** with the stored set. Set a value to `null` to remove only that dimension. Pass `{}` or omit the field to leave them unchanged.                       |

Values must be plain strings — the `erp_dimension_id` from [`GET /v1/dimensions`](/api-reference/api-reference/dimensions/get-dimensions-root), not the rich dimension object returned on reads.

```json
{
  "invoice_date": "2024-01-15",
  "total_amount": 1500.00,
  "dimensions": {
    "DEPARTMENT": "erp_dim_dept123",
    "CLASS": "erp_dim_class456"
  },
  "items": [
    {
      "id": 456,
      "quantity": 10,
      "price": 50.00,
      "dimensions": { "DEPARTMENT": "erp_dim_dept789" }
    }
  ]
}
```

## Custom fields

Both header and item `custom_fields` **replace** the stored object — any key you omit is reset to its configured default value, not left alone. Custom fields must be configured for your account first; sending them with no configured spec, or sending an unrecognised key, returns a `400`.

## Replacing images

There are three ways to change an invoice's images, and they differ in transport:

| Approach                                                                                           | Transport             | Notes                                                                                                                  |
| -------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `image_urls` on `PATCH /v1/invoices/{id}`                                                          | JSON                  | Array of URLs. Every URL must use `https://` — plain HTTP returns a `400`.                                             |
| `raw_images` on `PATCH /v1/invoices/{id}`                                                          | JSON                  | Array of `{ "filename": "...", "data": "<base64>" }` objects. Both keys are required, and `data` must be valid base64. |
| [`PATCH /v1/invoices/{id}/images`](/api-reference/api-reference/invoices/patch-invoices-id-images) | `multipart/form-data` | One or more files under the `images` key. Use this when you have the file bytes rather than a URL.                     |

All three **replace every existing image** on the invoice. Passing an empty array to `image_urls` or `raw_images` deletes all images; omitting the field entirely leaves them unchanged.

Supported formats are PDF, JPG, JPEG, and PNG.

> **Info**
>
> **The multipart route is capped at 10 MiB per request** (10,485,760 bytes for the whole body, across all files). That limit is enforced at the API gateway and returns a plain-text `413`, not JSON. `image_urls` avoids it entirely, since only the URLs travel in the request body. See [Extraction Overview](/extraction/overview) for details.

### Per-file failures do not fail the request

Image uploads are validated per file. A file that is rejected — unsupported format, oversized, or an S3 error — is reported in `image_errors` rather than failing the whole request.

> **Warn**
>
> **Always check `image_errors` — the status code will not tell you.** Both endpoints return `200` whether every file was attached or some were rejected. A partial failure is **not** signalled as `207 Multi-Status`. And because all three approaches replace the existing set first, an upload where every file fails validation leaves the invoice with **no images at all**, still under a `200`.

A rejected file looks like this — note the response was `HTTP 200`:

```json
{
  "images": [
    { "id": 149531010, "url": "https://doc-images.plateiq.com/8PrsU4-U_35-...png" }
  ],
  "image_errors": [
    {
      "source": "bad.bmp",
      "error": "Unsupported file format 'bmp'. Allowed formats: pdf, jpg, jpeg, png"
    }
  ]
}
```

> **Info**
>
> **`image_errors` is not always an array.** On `PATCH /v1/invoices/{id}`, the field is only populated when the request actually carried `image_urls` or `raw_images`. When it did not, the field comes back as an **empty string** (`"image_errors": ""`) rather than an empty array. Treat a non-array value as "no image work was requested".

## Line item results

When your request includes `items`, the response carries an `items_processing_result` object keyed by the position of each item in your request array. A `200` is returned even when every item failed, so inspect `items_processing_result.status`. See [Line Items](/line-items) for the full breakdown.

## Flagging an invoice for review

To send an invoice back for manual review rather than correcting it yourself:

```
POST /v1/invoices/{id}/flag
```

```json
{
  "flagging_reason": "Vendor total does not match the PO"
}
```

`flagging_reason` is optional and accepts `@`-mentions of Ottimate users; an unrecognised user ID returns a `400`.

Flagging sets the invoice's flag and records a `FLAGGED` entry in its history. A flagged invoice that has not been exported is returned as `needs-attention` and is held out of the export path, because `ready-for-export` requires the invoice to be unflagged.

> **Info**
>
> **Flagging is idempotent.** Re-flagging an already-flagged invoice is a no-op — it returns `200` but records no new history entry and does not overwrite the original `flagging_reason`. Clear the existing flag before applying a new reason.

Flagging an invoice you do not have permission to edit returns a `403` with the same generic `INV4030` envelope as any other permission failure.

## Status codes

| Code  | Meaning                                                                                                                                                                                          |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `200` | Invoice updated. Check `items_processing_result` and `image_errors` for partial failures.                                                                                                        |
| `400` | Validation error on the header, an item, a dimension, or a custom field.                                                                                                                         |
| `403` | Missing or invalid `X-Api-Key`, no access to the invoice's location, or the invoice is protected because it has already been approved or exported and your user lacks the permission to edit it. |
| `404` | No invoice with this ID is visible to your account.                                                                                                                                              |
| `500` | Unexpected server error.                                                                                                                                                                         |

A validation error returns the top-level message and an `INV4000` code:

```json
{
  "message": "Invalid invoice data",
  "code": "INV4000",
  "request_id": "f115db5a-fe4f-43fb-b12b-d1e1f5170d03",
  "timestamp": "02/Sep/2026:06:11:56 +0000"
}
```

> **Warn**
>
> **The response does not say which field failed.** Sending `image_urls` over plain HTTP, an unrecognised custom field, and a dimension value that is not a string all return the same `"Invalid invoice data"` body. Validate the request on your side, and change one thing at a time when narrowing down a `400`.

> **Info**
>
> **Approved and exported invoices are protected.** Editing one requires the "edit approved invoices" or "edit exported invoices" permission on your API user. Without it the request returns a `403` carrying the generic `INV4030` envelope — the specific reason (approved vs. exported) is not surfaced to API clients:
>
> ```json
> {
>   "message": "You do not have sufficient permissions.",
>   "code": "INV4030",
>   "request_id": "abcd1234efgh5678",
>   "timestamp": "15/Jan/2024:10:30:00 +0000"
> }
> ```
>
> Check the invoice's `status` to tell the two cases apart: `ready-for-export` means it is approved, `exported` means it has been exported.