Payments

Pay vendors from Ottimate and track each payment through to delivery

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.

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

1

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.

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.

2

Create the payment

POST /v1/payments/payment takes the vendor, the paying location, when to send it, and the invoices it covers.

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

{ "payment": { "id": "1234" } }
3

Poll the payment until it settles

GET /v1/payments/payment/{id} returns the payment’s current state. Follow status_code — see Payment status.

4

Cancel it if plans change

POST /v1/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.

How the payment amount is determined

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 entryOttimate creates an invoice from itOttimate looks up an existing invoice by invoice_number for that vendor and location
invoice_numberRequiredRequired, and must already exist — otherwise 404
total_amount, dateRequired — a missing one returns 400Ignored; the stored invoice’s values are used
currency, notesOptional, stored on the new invoiceIgnored

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_codestatusMeaning
0Pending ApprovalCreated, waiting on approval
1ApprovedApproved, not yet scheduled
2ScheduledQueued to be sent on scheduled_date
10In ReviewHeld for review
11On HoldHeld
6InitiatedFunds movement has started — no longer cancellable
4SentSent to the vendor
5PaidPaid
8DeliveredConfirmed delivered
9SwipedVirtual card was charged by the vendor
3FailedProcessing failed
7ArchivedArchived
-1CanceledCancelled

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:

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

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

GET /v1/payments/payment/1234?expand=invoices

expand=invoices adds:

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

Fields worth a second look

FieldNotes
payment_methodThe payment provider’s numeric code in a JSON string"2", not "ACH". See Payment methods.
company_idAn integer, and named company_id — not ottimate_company_id. See Naming differences.
object_versionRevision 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_errorReserved — always null in the current release. Detect failures from status_code 3 (Failed), not from this field.
expected_delivery_dateReserved — always null in the current release.
invoices[].total_amountNullable, 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[].currencyNullable.

Payment methods

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

payment_methodProviderRail
"-1"Offline paymentOffline
"1"LobCheck
"2"DwollaACH
"4"OnPayCheck
"5"PostGridCheck
"6"Direct integrationACH
"9"ConnexPayVirtual card
"10"OCWCheck
"11"ConnexPay card on fileVirtual card
"12"CorpayInternational
"13"StripeVirtual card
"14"StripeACH

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.

Outcomecancelledreason
CancelledtruePayment cancelled successfully
Too late to cancelfalsePayment not cancellable

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.

ValuePayments APIElsewhere
Company, on payment endpointscompany_id (integer)ottimate_company_id
Company, on payments vendor endpointsottimate_company_id (string of digits)ottimate_company_id
Locationlocation_id (integer)ottimate_location_id

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.

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 header on every POST to the Payments API so a retry replays the original response instead of creating a duplicate.

API endpoints

EndpointDescription
POST /v1/payments/vendorCreate a payments vendor
GET /v1/payments/vendor/{id}Retrieve a payments vendor, including its ACH enrollment status
POST /v1/payments/paymentCreate a payment for a set of invoices
GET /v1/payments/payment/{id}Retrieve a payment, optionally with expand=invoices
POST /v1/payments/payment/{id}/cancelCancel 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

StatusWhen
400Validation 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
401Authentication failed, or your API user cannot reach the requested company
404No 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
409Produced 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.