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.
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.
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 takes the vendor, the paying location, when to send it, and the invoices it covers.
The response carries only the new payment’s ID:
Poll the payment until it settles
GET /v1/payments/payment/{id} returns the payment’s current state. Follow status_code — see Payment status.
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_amountor it is not on the payment. To pay part of a balance, send an invoice whosetotal_amountis 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.
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.
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:
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”.
expand=invoices adds:
Fields worth a second look
Payment methods
payment_method is the provider that carries the payment, serialized as its numeric code inside a string:
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.
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.
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
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.

