Updating invoices

Correct invoice header fields, line items, and attached images after ingestion

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}. A single request can update the header, create and update line items, and replace attached images atomically.

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, or POST /v1/invoices/{id}/flag. See 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.

Header fields

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

FieldTypeDescription
ottimate_location_idintegerThe Ottimate location this invoice belongs to.
erp_vendor_idstringVendor identifier from your ERP. If no matching vendor exists, Ottimate creates one — which requires erp_vendor_name.
erp_vendor_namestringVendor display name. Required only when erp_vendor_id refers to a vendor that does not yet exist. Max 200 characters.
invoice_numberstringVendor’s invoice number.
invoice_datedateInvoice date (YYYY-MM-DD).
due_datedatePayment due date.
posting_datedateGL posting date.
total_amountdecimalInvoice total.
total_taxdecimalTotal tax on the invoice.
discount_amountdecimalTotal discount applied.
purchase_orderstringPO number to match against.
notesstringFree-text notes. Max 1024 characters.
currencystring3-letter currency code.
typestringinvoice or credit_memo. Any other value returns a 400.
dimensionsobjectHeader-level dimension mappings.
custom_fieldsobjectHeader-level custom metadata. Requires account-level configuration.
itemsarrayLine items to create or update. See Line Items.
image_urlsarrayReplace all invoice images with these HTTPS URLs.
raw_imagesarrayReplace 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:

LevelBehaviour
Header dimensionsReplaces 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 dimensionsMerges 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, not the rich dimension object returned on reads.

{
"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:

ApproachTransportNotes
image_urls on PATCH /v1/invoices/{id}JSONArray of URLs. Every URL must use https:// — plain HTTP returns a 400.
raw_images on PATCH /v1/invoices/{id}JSONArray of { "filename": "...", "data": "<base64>" } objects. Both keys are required, and data must be valid base64.
PATCH /v1/invoices/{id}/imagesmultipart/form-dataOne 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.

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

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:

{
"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"
}
]
}

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

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

CodeMeaning
200Invoice updated. Check items_processing_result and image_errors for partial failures.
400Validation error on the header, an item, a dimension, or a custom field.
403Missing 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.
404No invoice with this ID is visible to your account.
500Unexpected server error.

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

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

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.

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:

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