Bulk invoice operations

Import invoices in bulk and export catalog-matched receiver data

Two endpoints handle invoices in bulk rather than one at a time: POST /v1/invoices/import for pushing many invoices into Ottimate, and GET /v1/invoices/receiver-export for pulling catalog-matched line data back out. Bulk approval is covered in Invoice approvals.

Importing invoices in bulk

POST /v1/invoices/import creates many invoices asynchronously. The request body is a bare JSON array of invoice objects — not an object wrapping an array:

[
{
"ottimate_location_id": 456,
"upload_id": "my-system-invoice-0001",
"erp_vendor_id": "SYS-001",
"invoice_number": "INV-2024-001",
"invoice_date": "2024-01-15",
"total_amount": 1500.00
},
{
"ottimate_location_id": 457,
"upload_id": "my-system-invoice-0002",
"erp_vendor_id": "SYS-001",
"invoice_number": "INV-2024-002",
"invoice_date": "2024-01-16",
"total_amount": 980.50
}
]

Each object takes the same fields as POST /v1/invoices, and each carries its own ottimate_location_id — a single request can span several locations.

  • Maximum 100 invoices per request
  • Every invoice requires ottimate_location_id and a unique upload_id
  • An empty array, or a body that is not an array, returns a 400
  • raw_images is not supported in bulk mode — use image_urls instead

Validation is all-or-nothing; creation is not. Every invoice in the array is validated up front, and one invalid entry rejects the whole request with a 400 — nothing is created. Once validation passes the batch is queued, and individual invoices can still fail during processing. Poll the batch results to confirm what was actually created.

Response

The endpoint returns 202 Accepted immediately:

{
"status": "processing",
"batch_id": "batch_550e8400e29b41d4a716446655440000",
"batch_url": "/v1/batch/batch_550e8400e29b41d4a716446655440000/progress",
"results_url": "/v1/batch/batch_550e8400e29b41d4a716446655440000/results",
"summary": {
"invoice_count": 2
}
}

Poll GET /v1/batch/{id}/progress for status and GET /v1/batch/{id}/results for per-invoice outcomes.

Access errors are per-item

If your API user cannot access one of the locations in the array, the whole request is rejected with a 400. The body carries one entry per invoice you sent, positionally, so an empty object means that invoice passed validation:

{
"message": [
{},
{ "ottimate_location_id": ["Access denied to location 999"] }
],
"code": "INV4000",
"request_id": "e7234e86-fead-48f1-bb73-cd17a71bef58",
"timestamp": "02/Sep/2026:10:13:01 +0000"
}

Here the second invoice in the array referenced a location the API user cannot reach. Nothing was created.

Marking already-processed invoices on ingestion

Both POST /v1/invoices and POST /v1/invoices/import accept an optional, write-only status field. It exists for backfilling invoices that were already approved and paid — or already approved and exported — in your own system, so they do not re-enter Ottimate’s approval workflow.

ValueEffect
archivedThe invoice is treated as already paid. It is filed as historical and returned as status: "inactive" with sub_status: "archived", and is excluded from billpay and autopay.
verifiedThe invoice is treated as approved and exported but not yet paid. It stays an active invoice and remains payable through billpay and autopay.

Omit the field entirely for normal invoices — they will route through extraction, coding, and approval as usual.

Sending verified for an invoice you have already paid risks a double payment. verified leaves the invoice active and eligible for billpay and autopay, so Ottimate may pay it again. Use archived for anything already paid, and verified only for invoices that are approved in your system but still genuinely outstanding. If you are unsure which applies, omit the field.

This is not the same status you read back. The write-only ingestion status accepts only archived or verified. The status returned on an invoice is the derived lifecycle status described in Invoice lifecycle, and it cannot be set through the API.

Receiver export

GET /v1/invoices/receiver-export returns exported invoices together with their catalog-matched line items, in the shape a receiving or inventory system expects.

This endpoint is in beta.

The result set is deliberately narrow. An invoice appears only if:

  • It is in the exported state
  • Its vendor has item validation enabled
  • It has at least one qualifying line after filtering — invoices left with zero lines are omitted entirely

Fringe catalog entries and lines that did not match the catalog are excluded from receiver_lines.

Filters

ParameterDescription
ottimate_company_idRestrict to one company
ottimate_location_idRestrict to one location
vendor_idRestrict to one vendor
exported_after / exported_beforeExport-date window in the location’s local time
exported_after_utc / exported_before_utcExport-date window in UTC
page / limitPagination

Response

{
"version": "1.0.0",
"count": 1,
"page": 1,
"limit": 20,
"results": [
{
"ottimate_invoice_id": "124426270",
"ottimate_company_id": 15196,
"ottimate_location_id": 456,
"invoice_number": "INV-2024-001",
"vendor_name": "Sysco Foods Corp",
"vendor_id": "SYS-001",
"invoice_date": "2024-01-15",
"exported_at": "2024-01-16T01:15:00-08:00",
"exported_at_utc": "2024-01-16T09:15:00.000000Z",
"receiver_status": "success",
"receiver_lines": [
{
"catalog_unique_key": "SKU-001",
"upc": "001234567890a",
"description": "Canola Oil 1gal",
"uom": "CS",
"case_pack": 6,
"quantity_received": 2,
"unit_price": 45.00,
"extended_price": 90.00,
"pack_size": "6/1GAL",
"ottimate_catalog_id": "12345678"
}
]
}
]
}

An empty result set does not mean nothing was exported. Because unmatched lines and non-item-validated vendors are filtered out, an invoice can be exported and still never appear here. Use GET /v1/invoices?status=exported to see everything that was exported.