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

# Invoice Approvals

## Overview

Ottimate's approval system allows you to manage invoice approvers programmatically through the API. While approval policies are configured in the Ottimate UI, the API enables you to:

* View all approvers assigned to an invoice, and inspect the full policy chain
* Add manual approvers to invoices
* Remove approvers from the approval chain
* Approve an invoice, in bulk or at a specific policy tier
* Revoke an approval that has already been given
* List the invoices awaiting your own approval

For information on how to configure approval policies in Ottimate, see [Approval Policy Configuration](https://support.ottimate.com/support/solutions/14000074747).

> **Info**
>
> **Note:** The Approval API manages approvers and approval actions only. Approval policies themselves must be configured through the Ottimate user interface.

## How Invoice Approvals Work

When an invoice enters the approval workflow, it receives a `pending-approval` status. The invoice must be approved by all assigned approvers before it can proceed to export. Approvers can be assigned either:

* **Policy-based approvers**: Automatically assigned based on approval policies configured in Ottimate
* **Manual approvers**: Added individually via the API or UI for ad-hoc review requirements

## API endpoints

| Endpoint                                                                                                           | Description                                          |
| ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
| [`GET /v1/invoices/{id}/approvers`](/api-reference/api-reference/invoices/get-invoices-id-approvers)               | List every approver assigned to an invoice           |
| [`POST /v1/invoices/{id}/approvers`](/api-reference/api-reference/invoices/post-invoices-id-approvers)             | Add a manual approver                                |
| [`DELETE /v1/invoices/{id}/approvers`](/api-reference/api-reference/invoices/delete-invoices-id-approvers)         | Remove an approver by `approval_id`                  |
| [`POST /v1/invoices/{id}/approve`](/api-reference/api-reference/invoices/post-invoices-id-approve)                 | Approve an invoice                                   |
| [`POST /v1/invoices/bulk-approve`](/api-reference/api-reference/invoices/post-invoices-bulk-approve)               | Approve up to 100 invoices asynchronously            |
| [`POST /v1/invoices/{id}/approve-policy`](/api-reference/api-reference/invoices/post-invoices-id-approve-policy)   | Approve an invoice at one specific policy tier       |
| [`POST /v1/invoices/{id}/revoke-approval`](/api-reference/api-reference/invoices/post-invoices-id-revoke-approval) | Revoke an approval that has already been given       |
| [`GET /v1/invoices/{id}/approval-summary`](/api-reference/api-reference/invoices/get-invoices-id-approval-summary) | Approval state per policy tier, with approver counts |
| [`GET /v1/invoices/{id}/approval-chain`](/api-reference/api-reference/invoices/get-invoices-id-approval-chain)     | The ordered policy chain, including nested tiers     |
| [`GET /v1/invoices/pending-approval`](/api-reference/api-reference/invoices/get-invoices-pending-approval)         | Invoices awaiting the authenticated user's approval  |

## List Invoice Approvers

To view all approvers assigned to an invoice, including their approval status:

```
GET /v1/invoices/{id}/approvers
```

Where `{id}` is the Ottimate-generated invoice ID.

### Response Details

The response includes an array of approvers, where each approver object contains:

* **`id`**: The approval record ID (e.g., `pbjappr_xxx`) - use this ID when removing an approver via the DELETE endpoint
* **`approver`**: User details including ID, email, and display name
* **`policy`**: The associated approval policy. Policy-based approvers carry the policy's `id` and `name`; manual approvers carry an object with **both fields as empty strings**
* **`created_date`**: When this approver was added to the invoice

### Example Response

```json
{
  "version": "1.0.0",
  "approvers": [
    {
      "id": "pbjappr_e1e7eb2cf7cf11f080d40eff1c63a64d",
      "approver": {
        "id": "138872",
        "email": "api_user@example.com",
        "display_name": "Api User"
      },
      "policy": {
        "id": "plcy_e00205b8f7cf11f0b0300eff1c63a64d",
        "name": "Minimum Approvals"
      },
      "created_date": "2026-01-22T12:21:06.883965-08:00"
    },
    {
      "id": "pbjappr_c1f04ec4f7a011f0ac5f0eff1c63a64d",
      "approver": {
        "id": "136597",
        "email": "peter@example.com",
        "display_name": "Peter"
      },
      "policy": {
        "id": "",
        "name": ""
      },
      "created_date": "2026-01-22T06:43:46.905413-08:00"
    }
  ]
}
```

> **Info**
>
> **Identifying Manual vs. Policy-Based Approvers:** `policy` is always an object — never `null`. Manual approvers have `policy.id` and `policy.name` as **empty strings**; policy-based approvers have both populated. Test `policy.id` for emptiness rather than checking whether `policy` itself is null.

### Status codes

| Code  | Meaning                                                                                           |
| ----- | ------------------------------------------------------------------------------------------------- |
| `200` | Approvers returned. An invoice with no approvers returns an empty `approvers` array, not a `404`. |
| `401` | Missing, invalid, or expired bearer token.                                                        |
| `403` | Missing or invalid `X-Api-Key`, or no access to this invoice.                                     |
| `404` | No invoice with this ID is visible to your account.                                               |

## Add a Manual Approver

Add an ad-hoc approver to an invoice using:

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

### Request body

The body is required and takes a single field:

```json
{
  "approver": 12345
}
```

Where `approver` is the **integer user ID** of the Ottimate user to add. This is the `approver.id` value returned by `GET /v1/invoices/{id}/approvers`, or a user ID from your account's user list.

### Response

```json
{
  "version": "1.0.0",
  "success": true,
  "message": "Approver added successfully"
}
```

### Important Behavior

When you add a manual approver:

* The approver must be an authorized user of the location with permission to access the invoice.
* The approver receives a pending-approval email notification.
* Approval policies are re-applied to the invoice.

> **Warn**
>
> **Adding an approver to an already-approved invoice resets it to `pending-approval`.** If the invoice had an `approved_date`, that date and the recorded approving user are cleared, so an invoice sitting at `ready-for-export` drops back into the approval queue. Do not use this endpoint to attach a reviewer for audit purposes on an approved invoice — it will pull the invoice out of the export path.

### Status codes

| Code  | Meaning                                                                            |
| ----- | ---------------------------------------------------------------------------------- |
| `200` | Approver added.                                                                    |
| `400` | Invalid or missing `approver`, or the user is already an approver on this invoice. |
| `401` | Missing, invalid, or expired bearer token.                                         |
| `403` | Missing or invalid `X-Api-Key`, or the invoice is protected and cannot be edited.  |
| `404` | No invoice with this ID is visible to your account.                                |

A duplicate approver returns:

```json
{
  "message": {
    "err": "Approver is already added to the Invoice."
  },
  "code": "INV4000",
  "request_id": "abcd1234efgh5678",
  "timestamp": "15/Jan/2024:10:30:00 +0000"
}
```

Omitting `approver` returns:

```json
{
  "message": {
    "approver": ["This field is required."]
  },
  "code": "INV4000",
  "request_id": "591fa421-ff70-4450-8024-0f22700b0aa1",
  "timestamp": "02/Sep/2026:06:10:32 +0000"
}
```

> **Info**
>
> **`message` is not always a string.** Ottimate's error envelope carries the underlying error in `message`, which may be a string or a nested object depending on the failure. Parse it defensively rather than assuming a string.

## Remove an Approver

Remove a specific approver from an invoice:

```
DELETE /v1/invoices/{id}/approvers?approval_id={approval_id}
```

### Parameters

* `id`: The invoice ID
* `approval_id`: The approval record ID to remove (e.g., `pbjappr_xxx`) - this is the `id` field from the approver object returned by `GET /v1/invoices/{id}/approvers`

> **Warn**
>
> **Omitting `approval_id` removes every approver on the invoice.** The parameter is documented as required, but a request without it deletes the invoice's entire approver list rather than returning an error. Always send `approval_id`.

## Approve an Invoice

Approve an invoice to mark it ready for export and payment:

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

### Features

* Marks the invoice with an `approved_date`
* Automatically verifies the invoice if not already verified
* Updates invoice status to `ready-for-export` when all approvals are complete

### Force Approval

For authorized users, you can bypass the normal approval chain:

```json
{
  "force_approve": true
}
```

## Approve at a Specific Policy Tier

When an invoice is routed through Advanced Approval Policies, you can approve it at one specific tier rather than approving the whole chain:

```
POST /v1/invoices/{id}/approve-policy
```

```json
{
  "policy_id": "plcy_7d27b39042ab11f09213577c0b92a500"
}
```

`policy_id` is required — omitting it returns a `400`. Use [`GET /v1/invoices/{id}/approval-chain`](/api-reference/api-reference/invoices/get-invoices-id-approval-chain) to find the tier you want to approve.

> **Warn**
>
> **`force_approve` is not supported here.** Sending `force_approve: true` to this endpoint returns a `400` error. Use `POST /v1/invoices/{id}/approve` if you need to bypass the approval chain.

## Revoke an Approval

Undo an approval step that has already been given:

```
POST /v1/invoices/{id}/revoke-approval
```

```json
{
  "approval_id": "pbjappr_550e8400e29b41d4a716446655440000"
}
```

`approval_id` is required and is the `id` of the approval record from `GET /v1/invoices/{id}/approvers`. Omitting it returns a `400`:

```json
{
  "message": "Missing required approval_id",
  "code": "INV4000",
  "request_id": "abcd1234efgh5678",
  "timestamp": "15/Jan/2024:10:30:00 +0000"
}
```

## Bulk Approve

Approve many invoices in one asynchronous request:

```
POST /v1/invoices/bulk-approve
```

```json
{
  "invoice_ids": ["139727692", "139727693"],
  "force_approve": false
}
```

* `invoice_ids` is required and must be a **non-empty JSON array** — a comma-separated string is rejected
* Maximum **100 invoices** per request
* `force_approve` is optional and defaults to `false`

The endpoint returns `202 Accepted` with a `batch` path you can poll:

```json
{
  "version": "1.0.0",
  "batch_id": "btch_9f3c1e2a7d5b11f0a1c30eff1c63a64d",
  "batch": "/v1/batch/btch_9f3c1e2a7d5b11f0a1c30eff1c63a64d/progress"
}
```

Poll [`GET /v1/batch/{id}/progress`](/api-reference/api-reference/batch/get-batch-id-progress) for status and [`GET /v1/batch/{id}/results`](/api-reference/api-reference/batch/get-batch-id-results) for per-invoice outcomes.

## Inspecting the Approval Chain

Two read endpoints expose the approval state in different shapes:

### Approval summary

```
GET /v1/invoices/{id}/approval-summary
```

Returns `manual_user_approvers`, a `policies` array with per-tier `min_users_for_approval` / `num_approved` / `num_pending` counts, and a `users` map keyed by user ID. Use this to answer "how many more approvals does this tier need?".

### Approval chain

```
GET /v1/invoices/{id}/approval-chain
```

Returns the ordered policy chain as an array, where each tier carries `status`, `min_approver`, `approved`, its `approvers[]`, and a `children` array for nested tiers. Use this to render the approval hierarchy.

Pass `sync_approval=true` to re-apply approval policies before returning the chain. It has no effect on an invoice that has already been exported.

## Invoices Awaiting Your Approval

```
GET /v1/invoices/pending-approval
```

Returns the invoices that the **authenticated API user** is themselves an approver on and has not yet approved. Supports `page`, `limit`, `ottimate_location_ids`, `ottimate_vendor_id`, `date_from`, and `date_to`.

> **Info**
>
> This is scoped to the authenticated user, not to your account. An account-scoped integration whose API user is not an assigned approver on anything will get an empty list — use `GET /v1/invoices?status=pending-approval` to see every invoice awaiting approval across the account.