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

# Payments

The Payments API sends money to a vendor for a set of invoices, and reports where that payment has got to. It covers two objects:

* **Payments vendor** — the vendor record a payment is sent to, tied to one company and identified by your ERP's vendor ID.
* **Payment** — one disbursement to one vendor from one location, covering one or more invoices.

> **Info**
>
> **Ottimate chooses the payment rail, not you.** Whether a payment goes out as ACH, a check, or a virtual card is decided by the location's payment configuration and the vendor's enrollment status. There is no field to request a method, and the method is only visible after the fact, on the payment's `payment_method`.

## The payment flow

#### Make sure the vendor exists in Ottimate

Payments are matched to a vendor by your ERP's vendor identifier, `erp_vendor_id`, scoped to the paying location. If that vendor is not in Ottimate yet, create it with [`POST /v1/payments/vendor`](/api-reference/api-reference/payments/post-payments-vendor).

Creating a vendor requires `ottimate_company_id`, `name`, `erp_vendor_id`, `email`, `phone_number`, `address_line_1`, `city`, `state`, and `zipcode`. There is no upsert. Ottimate matches the request against existing vendors in the company on name, ZIP code and street address, and returns `400` naming the existing vendor when it treats one as a duplicate.

#### Create the payment

[`POST /v1/payments/payment`](/api-reference/api-reference/payments/post-payments-payment) takes the vendor, the paying location, when to send it, and the invoices it covers.

```json
{
  "erp_vendor_id": "INTERNAL VENDOR NUMBER",
  "location_id": 456,
  "scheduled_date": "2026-10-01T00:00:00.000000Z",
  "payment_number": "PAYMENT-0001",
  "memo": "October produce",
  "invoices": [
    { "invoice_number": "INV-0001", "total_amount": "1000.00", "date": "2026-09-15", "currency": "USD" },
    { "invoice_number": "INV-0002", "total_amount": "250.50", "date": "2026-09-18", "currency": "USD" }
  ]
}
```

The response carries only the new payment's ID:

```json
{ "payment": { "id": "1234" } }
```

#### Poll the payment until it settles

[`GET /v1/payments/payment/{id}`](/api-reference/api-reference/payments/get-payments-payment-id) returns the payment's current state. Follow `status_code` — see [Payment status](#payment-status).

#### Cancel it if plans change

[`POST /v1/payments/payment/{id}/cancel`](/api-reference/api-reference/payments/post-payments-payment-id-cancel) cancels a payment that has not started moving yet, and marks its invoices as no longer paid. Both outcomes return `200`, so check the `cancelled` field. See [Cancelling a payment](#cancelling-a-payment).

## How the payment amount is determined

> **Warn**
>
> **There is no amount field on a payment.** The payment total is the sum of `invoices[].total_amount`. `invoices` is required and must not be empty — a payment always exists to settle specific invoices.

This has two consequences worth designing around:

* **Partial payments are not supported.** An invoice is paid for its full `total_amount` or it is not on the payment. To pay part of a balance, send an invoice whose `total_amount` is the amount you intend to pay.
* **To change the total, change the invoice list.** There is no way to override the sum.

### What the `invoices` entries mean

How each entry is resolved depends on the invoice-matching setting Ottimate holds for the location. Ask your Partner Manager which applies to yours — it is not readable through the API.

|                            | Invoice matching **off** (default)         | Invoice matching **on**                                                                |
| -------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------- |
| What happens to each entry | Ottimate creates an invoice from it        | Ottimate looks up an existing invoice by `invoice_number` for that vendor and location |
| `invoice_number`           | Required                                   | Required, and must already exist — otherwise `404`                                     |
| `total_amount`, `date`     | **Required** — a missing one returns `400` | Ignored; the stored invoice's values are used                                          |
| `currency`, `notes`        | Optional, stored on the new invoice        | Ignored                                                                                |

`currency` accepts only `USD` and `CAD`. `total_amount` is a decimal string with at most two decimal places.

## Payment status

A payment reports its state twice: `status_code` is the stable numeric value to branch on, and `status` is its display label.

| `status_code` | `status`         | Meaning                                                |
| ------------- | ---------------- | ------------------------------------------------------ |
| `0`           | Pending Approval | Created, waiting on approval                           |
| `1`           | Approved         | Approved, not yet scheduled                            |
| `2`           | Scheduled        | Queued to be sent on `scheduled_date`                  |
| `10`          | In Review        | Held for review                                        |
| `11`          | On Hold          | Held                                                   |
| `6`           | Initiated        | Funds movement has started — **no longer cancellable** |
| `4`           | Sent             | Sent to the vendor                                     |
| `5`           | Paid             | Paid                                                   |
| `8`           | Delivered        | Confirmed delivered                                    |
| `9`           | Swiped           | Virtual card was charged by the vendor                 |
| `3`           | Failed           | Processing failed                                      |
| `7`           | Archived         | Archived                                               |
| `-1`          | Canceled         | Cancelled                                              |

Branch on the codes you handle and treat anything unrecognized as a non-terminal state rather than an error, falling back to the `status` string for display.

## Reading a payment

`GET /v1/payments/payment/{id}` wraps the payment in a `payment` object:

```json
{
  "payment": {
    "id": "1234",
    "payment_method": "2",
    "status": "Scheduled",
    "status_code": 2,
    "last_error": null,
    "erp_vendor_id": "INTERNAL VENDOR NUMBER",
    "company_id": 1234,
    "scheduled_date": "2026-10-01T00:00:00.000000Z",
    "memo": "October produce",
    "payment_number": "PAYMENT-0001",
    "expected_delivery_date": null,
    "object_version": 1
  }
}
```

> **Warn**
>
> **`invoices` is absent unless you ask for it.** Pass `expand=invoices` to include the invoices the payment covers. Without it, the `invoices` key is not in the response at all — it is not returned as an empty array, so do not read an empty list as "this payment covers no invoices".
>
> ```bash
> GET /v1/payments/payment/1234?expand=invoices
> ```

`expand=invoices` adds:

```json
"invoices": [
  {
    "id": 1234,
    "invoice_number": "INV-0001",
    "date": "2026-09-15",
    "total_amount": "1000.00000000",
    "currency": "USD",
    "notes": null
  }
]
```

### Fields worth a second look

| Field                                                        | Notes                                                                                                                                                                                                   |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `payment_method`                                             | The payment provider's **numeric code in a JSON string** — `"2"`, not `"ACH"`. See [Payment methods](#payment-methods).                                                                                 |
| `company_id`                                                 | An integer, and named `company_id` — not `ottimate_company_id`. See [Naming differences](#naming-differences).                                                                                          |
| `object_version`                                             | Revision counter, starting at `1` and incrementing on every change. A higher value is a strictly newer read, which is useful for discarding a stale poll response.                                      |
| `last_error`                                                 | Reserved — always `null` in the current release. Detect failures from `status_code` `3` (Failed), not from this field.                                                                                  |
| `expected_delivery_date`                                     | Reserved — always `null` in the current release.                                                                                                                                                        |
| `invoices[].total_amount`                                    | Nullable, and returned with **8 decimal places** — `"1000.00000000"`. Requests accept 2, so a payment created with `"1000.00"` reads back as `"1000.00000000"`. Compare these as decimals, not strings. |
| `invoices[].notes`, `invoices[].date`, `invoices[].currency` | Nullable.                                                                                                                                                                                               |

## Payment methods

`payment_method` is the provider that carries the payment, serialized as its numeric code inside a string:

| `payment_method` | Provider               | Rail          |
| ---------------- | ---------------------- | ------------- |
| `"-1"`           | Offline payment        | Offline       |
| `"1"`            | Lob                    | Check         |
| `"2"`            | Dwolla                 | ACH           |
| `"4"`            | OnPay                  | Check         |
| `"5"`            | PostGrid               | Check         |
| `"6"`            | Direct integration     | ACH           |
| `"9"`            | ConnexPay              | Virtual card  |
| `"10"`           | OCW                    | Check         |
| `"11"`           | ConnexPay card on file | Virtual card  |
| `"12"`           | Corpay                 | International |
| `"13"`           | Stripe                 | Virtual card  |
| `"14"`           | Stripe                 | ACH           |

Treat any code you do not recognise as opaque. If no rail is available for the vendor at that location, `POST /v1/payments/payment` returns `400` rather than creating an unsendable payment.

## Cancelling a payment

`POST /v1/payments/payment/{id}/cancel` returns the payment's resulting state whether or not the cancellation took effect. **Both outcomes reach you as a `200` — branch on the `cancelled` field, not on the status code.**

| Outcome            | `cancelled` | `reason`                         |
| ------------------ | ----------- | -------------------------------- |
| Cancelled          | `true`      | `Payment cancelled successfully` |
| Too late to cancel | `false`     | `Payment not cancellable`        |

> **Warn**
>
> **A failed cancellation is not an error status.** The service produces a `409` internally when a payment has already started processing, but that status never reaches you: the gateway forwards only `400`, `401`, `403`, `404`, `422` and `500` alongside an endpoint's success status, so the `409` is delivered as a `200` carrying `cancelled: false`. Code that branches on the status code will read a failed cancellation as a successful one.

A payment can be cancelled right up until the money starts moving — that is, until `status_code` reaches `6` (Initiated), `4` (Sent), `5` (Paid), `8` (Delivered), or `9` (Swiped). From any of those states nothing changes and `cancelled` comes back `false`.

Cancelling is safe to repeat: an already-cancelled payment reports `cancelled: true` again and is left as it is. A successful cancellation also marks the payment's invoices as no longer paid.

Both outcomes include the full payment under `payment`, so you can read the resulting `status_code` without a follow-up `GET`.

## Naming differences

The company and location identifiers are the same values used everywhere else in the API, but the Payments API names them differently from the other endpoints. This is the most common source of a silently-ignored field, because both request serializers accept unknown keys without complaint.

| Value                                     | Payments API                                 | Elsewhere              |
| ----------------------------------------- | -------------------------------------------- | ---------------------- |
| Company, on payment endpoints             | `company_id` (integer)                       | `ottimate_company_id`  |
| Company, on payments **vendor** endpoints | `ottimate_company_id` (**string** of digits) | `ottimate_company_id`  |
| Location                                  | `location_id` (integer)                      | `ottimate_location_id` |

> **Warn**
>
> Note the two shapes of company ID within the Payments API itself: the payments vendor endpoints take and return `ottimate_company_id` as a **string** (`"1234"`), while the payment endpoints use an integer `company_id` (`1234`). Sending `ottimate_company_id` to `POST /v1/payments/payment` is silently ignored, not rejected — the payment is created with the company derived from `location_id` instead.

Both IDs come from the Accounts API — see [Identifier naming across endpoints](/account-structure/overview#identifier-naming-across-endpoints).

## Retrying safely

Both create endpoints are non-idempotent by default: a retried `POST` after a timeout can produce a second vendor or a second payment. Send an [`Idempotency-Key`](/idempotency) header on every `POST` to the Payments API so a retry replays the original response instead of creating a duplicate.

## API endpoints

| Endpoint                                                                                                         | Description                                                     |
| ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| [`POST /v1/payments/vendor`](/api-reference/api-reference/payments/post-payments-vendor)                         | Create a payments vendor                                        |
| [`GET /v1/payments/vendor/{id}`](/api-reference/api-reference/payments/get-payments-vendor-id)                   | Retrieve a payments vendor, including its ACH enrollment status |
| [`POST /v1/payments/payment`](/api-reference/api-reference/payments/post-payments-payment)                       | Create a payment for a set of invoices                          |
| [`GET /v1/payments/payment/{id}`](/api-reference/api-reference/payments/get-payments-payment-id)                 | Retrieve a payment, optionally with `expand=invoices`           |
| [`POST /v1/payments/payment/{id}/cancel`](/api-reference/api-reference/payments/post-payments-payment-id-cancel) | Cancel a payment that has not started processing                |

There is no endpoint that lists payments or vendors, and no way to update either after creation — hold onto the `id` from the create response.

## Errors

| Status | When                                                                                                                                                                                                                                                                                                             |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Validation failed; `erp_vendor_id` matched more than one vendor; an `invoices` entry is missing `total_amount` or `date` while invoice matching is off; the vendor has no Ottimate record and automatic vendor creation is disabled for the location; or no payment rail is available for the vendor or location |
| `401`  | Authentication failed, or your API user cannot reach the requested company                                                                                                                                                                                                                                       |
| `404`  | No location matched `location_id`, no vendor matched `erp_vendor_id`, an `invoice_number` did not match with invoice matching on, or the payment or vendor ID does not exist for your API user                                                                                                                   |
| `409`  | Produced internally when a payment has already started processing, but **not delivered to callers** — it arrives as a `200` with `cancelled: false`. See [Cancelling a payment](#cancelling-a-payment).                                                                                                          |