Skip to navigation

Invoice Approvals

Learn about how to programmatically manage approvals in Ottimate

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.

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

EndpointDescription
GET /v1/invoices/{id}/approversList every approver assigned to an invoice
POST /v1/invoices/{id}/approversAdd a manual approver
DELETE /v1/invoices/{id}/approversRemove an approver by approval_id
POST /v1/invoices/{id}/approveApprove an invoice
POST /v1/invoices/bulk-approveApprove up to 100 invoices asynchronously
POST /v1/invoices/{id}/approve-policyApprove an invoice at one specific policy tier
POST /v1/invoices/{id}/revoke-approvalRevoke an approval that has already been given
GET /v1/invoices/{id}/approval-summaryApproval state per policy tier, with approver counts
GET /v1/invoices/{id}/approval-chainThe ordered policy chain, including nested tiers
GET /v1/invoices/pending-approvalInvoices 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

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

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

CodeMeaning
200Approvers returned. An invoice with no approvers returns an empty approvers array, not a 404.
401Missing, invalid, or expired bearer token.
403Missing or invalid X-Api-Key, or no access to this invoice.
404No 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:

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

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

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

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

A duplicate approver returns:

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

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

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

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:

{
"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
{
"policy_id": "plcy_7d27b39042ab11f09213577c0b92a500"
}

policy_id is required — omitting it returns a 400. Use GET /v1/invoices/{id}/approval-chain to find the tier you want to approve.

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

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

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

Poll GET /v1/batch/{id}/progress for status and GET /v1/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.

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.