Skip to content

Commitments

Commitments represent expected vendor costs — purchase orders, standing contracts, rental agreements, or any expected payable. They exist separately from bills: a commitment is a control record that tracks what you expect to owe, while a bill is the actual liability.

The typical flow:

  1. Push commitments from your ERP or operations system
  2. Invoices arrive in the inbox (PDF upload or email)
  3. Match invoices to commitments (automatic candidates + manual approval)
  4. Create bills from approved allocations
StatusMeaning
openNo invoices matched yet
partially_matchedSome invoices allocated, remaining balance > 0
fully_matchedAllocated amount equals expected total
overmatchedAllocated amount exceeds expected total
canceledCommitment voided — no further matching allowed

Statuses are derived from allocation totals, not set manually. The only exception is canceled, which is an explicit action.


GET /api/books/commitments

Scope: read

ParameterTypeDescription
statusstringFilter: open, partially_matched, fully_matched, overmatched, canceled
contact_idintegerFilter by vendor contact
fromdatePeriod start from (inclusive)
todatePeriod end to (inclusive)
has_remainingbooleanOnly commitments with unmatched balance
{
"commitments": [
{
"id": 1,
"number": "PO-2026-001",
"contact_id": 42,
"contact_name": "Acme Materials",
"external_source": "quarry",
"external_id": "ORD-8834",
"service_address": "123 Job Site Rd, Springfield IL",
"period_start": "2026-03-01",
"period_end": "2026-03-31",
"currency": "USD",
"expected_total_cents": 109500,
"matched_total_cents": 0,
"remaining_cents": 109500,
"status": "open",
"line_count": 1,
"metadata": { "job_code": "J-4401" },
"notes": "Monthly aggregate rental",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:00:00Z"
}
]
}

GET /api/books/commitments/:id

Scope: read

Returns the commitment with lines and allocations:

{
"commitment": {
"id": 1,
"number": "PO-2026-001",
"contact_id": 42,
"contact_name": "Acme Materials",
"external_source": "quarry",
"external_id": "ORD-8834",
"service_address": "123 Job Site Rd, Springfield IL",
"period_start": "2026-03-01",
"period_end": "2026-03-31",
"currency": "USD",
"expected_total_cents": 109500,
"matched_total_cents": 109500,
"remaining_cents": 0,
"status": "fully_matched",
"line_count": 1,
"metadata": { "job_code": "J-4401" },
"notes": "Monthly aggregate rental",
"lines": [
{
"id": 10,
"position": 0,
"description": "Excavator rental — March",
"quantity": 1,
"unit_price_cents": 109500,
"amount_cents": 109500,
"account_id": 301,
"period_start": "2026-03-01",
"period_end": "2026-03-31",
"external_line_id": "LINE-001",
"metadata": {}
}
],
"allocations": [
{
"id": 5,
"commitment_id": 1,
"bill_id": 12,
"bill_number": "BILL-00042",
"invoice_document_id": 7,
"allocated_cents": 109500,
"invoice_total_cents": 109500,
"unallocated_remainder_cents": 0,
"variance_cents": 0,
"allocation_type": "manual",
"status": "approved",
"reviewed_by": "user@example.com",
"reviewed_at": "2026-03-12T14:30:00Z",
"notes": null,
"created_at": "2026-03-12T14:30:00Z"
}
],
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-12T14:30:00Z"
}
}
FieldTypeDescription
idintegerUnique identifier
numberstringPO / commitment number (unique per entity)
contact_idintegerVendor contact ID
contact_namestringVendor name
external_sourcestringSource system identifier (e.g. quarry, procore)
external_idstringID in the source system
service_addressstringJob site or delivery address
period_startdateExpected service/delivery start
period_enddateExpected service/delivery end
currencystring3-letter currency code
expected_total_centsintegerTotal expected cost
matched_total_centsintegerSum of approved allocations (derived, read-only)
remaining_centsintegerExpected minus matched (derived, read-only)
statusstringDerived from allocation totals (see lifecycle above)
line_countintegerNumber of line items
metadataobjectArbitrary key-value data from source system
notesstringFree-text notes
FieldTypeDescription
idintegerLine ID
positionintegerDisplay order
descriptionstringLine description
quantitynumberQuantity
unit_price_centsintegerUnit price
amount_centsintegerLine total (quantity x unit price)
account_idintegerExpense account ID
period_startdateLine-level period start
period_enddateLine-level period end
external_line_idstringID in source system
metadataobjectArbitrary key-value data
FieldTypeDescription
idintegerAllocation ID
commitment_idintegerParent commitment
bill_idintegerBill created from this allocation (null until bill creation)
bill_numberstringBill number
invoice_document_idintegerSource invoice document
allocated_centsintegerAmount allocated from this invoice
invoice_total_centsintegerSnapshot of invoice total at allocation time
unallocated_remainder_centsintegerInvoice amount not covered by any allocation
variance_centsintegerDifference: allocated minus commitment remaining
allocation_typestringmanual, suggested, or auto
statusstringpending, approved, or rejected
reviewed_bystringEmail of reviewer
reviewed_attimestampWhen reviewed

POST /api/books/commitments

Scope: write

{
"commitment": {
"contact_id": 42,
"number": "PO-2026-001",
"service_address": "123 Job Site Rd, Springfield IL",
"period_start": "2026-03-01",
"period_end": "2026-03-31",
"currency": "USD",
"expected_total_cents": 109500,
"notes": "Monthly aggregate rental",
"metadata": { "job_code": "J-4401" },
"commitment_lines_attributes": [
{
"position": 0,
"description": "Excavator rental — March",
"quantity": 1,
"unit_price_cents": 109500,
"account_id": 301,
"period_start": "2026-03-01",
"period_end": "2026-03-31",
"external_line_id": "LINE-001"
}
]
}
}
FieldTypeRequiredDescription
contact_idintegeryesVendor contact ID
numberstringyesPO / commitment number (must be unique per entity)
expected_total_centsintegeryesTotal expected cost
currencystringnoDefaults to entity currency
service_addressstringnoJob site or delivery address
period_startdatenoService/delivery start
period_enddatenoService/delivery end
notesstringnoNotes
metadataobjectnoArbitrary key-value data
commitment_lines_attributesarraynoLine items (see Line Fields above)

Returns: 201 Created


PUT /api/books/commitments/by_external/:external_source/:external_id

Scope: write

This is the primary endpoint for ERP integrations. Creates a commitment if it doesn’t exist, or updates it if it does. Idempotent — safe to call on every sync.

The external_source and external_id are part of the URL path, not the request body.

Same as Create Commitment.

PUT /api/books/commitments/by_external/quarry/ORD-8834
{
"commitment": {
"contact_id": 42,
"number": "PO-2026-001",
"service_address": "123 Job Site Rd, Springfield IL",
"period_start": "2026-03-01",
"period_end": "2026-03-31",
"currency": "USD",
"expected_total_cents": 109500,
"notes": "Monthly aggregate rental",
"metadata": { "job_code": "J-4401" },
"commitment_lines_attributes": [
{
"position": 0,
"description": "Excavator rental — March",
"quantity": 1,
"unit_price_cents": 109500,
"account_id": 301
}
]
}
}

When a commitment with the given external_source + external_id already exists:

  • No approved allocations: All fields can be updated, including financial fields (expected_total_cents, currency, contact_id).
  • Has approved allocations: Only non-financial fields can be updated: service_address, period_start, period_end, notes, and metadata. Attempts to change financial fields return 409 Conflict.
  • Canceled: Returns 409 Conflict. Canceled commitments cannot be reactivated.

Returns: 201 Created (new) or 200 OK (updated)


PATCH /api/books/commitments/:id

Scope: write

Same parameters as Create. Subject to the same financial field restrictions as upsert when allocations exist.

Returns 409 Conflict if:

  • Commitment is canceled
  • Attempting to modify expected_total_cents, currency, or contact_id after allocations are approved

POST /api/books/commitments/:id/cancel

Scope: write

Cancels a commitment. Cannot be undone. The commitment must not have any approved allocations — deallocate first.

Returns 409 Conflict if approved allocations exist.


These endpoints live on the Invoice Documents resource but are documented here for context since they form the commitment workflow.

GET /api/books/invoice_documents/:id/commitment_candidates

Scope: read

Returns commitments that may match a given invoice, ranked by match quality. Matching is deterministic — no AI or scoring engine.

Match signals:

ReasonMeaning
exact_amountInvoice total equals commitment remaining (or expected total for first match)
amount_within_toleranceWithin $25 or 5% of remaining
address_overlapService address tokens overlap
period_overlapInvoice date or extracted period falls within commitment period
vendor_matchVendor resolved to same contact (always present)
{
"invoice_document_id": 7,
"invoice_total_cents": 109500,
"candidates": [
{
"commitment_id": 1,
"number": "PO-2026-001",
"contact_name": "Acme Materials",
"service_address": "123 Job Site Rd, Springfield IL",
"period_start": "2026-03-01",
"period_end": "2026-03-31",
"expected_total_cents": 109500,
"remaining_cents": 109500,
"variance_cents": 0,
"variance_percent": 0.0,
"reasons": ["exact_amount", "vendor_match"]
}
]
}
POST /api/books/invoice_documents/:id/allocate

Scope: write

Approves allocations from an invoice to one or more commitments. This doesn’t create a bill — it records which commitments this invoice satisfies.

{
"allocations": [
{ "commitment_id": 1, "allocated_cents": 109500 }
]
}
FieldTypeRequiredDescription
allocations[].commitment_idintegeryesCommitment to allocate to
allocations[].allocated_centsintegeryesAmount to allocate (must be positive)

After allocation, the commitment’s matched_total_cents and remaining_cents are recalculated, and the invoice document’s commitment_review_status is set to approved.

Returns 422 if:

  • No allocations provided
  • Commitment is not in a matchable status (open or partially_matched)
POST /api/books/invoice_documents/:id/create_commitment_bill

Scope: write

Creates a bill from an invoice’s approved allocations. The invoice must have commitment_review_status: "approved" and not already be converted.

Each allocation becomes a bill line. Optionally include non-PO lines for surcharges, fees, or other charges not covered by commitments.

{
"non_po_lines": [
{
"description": "Fuel surcharge",
"amount_cents": 14500,
"account_id": 305
}
]
}
FieldTypeRequiredDescription
non_po_lines[].descriptionstringyesLine description
non_po_lines[].amount_centsintegeryesAmount in cents
non_po_lines[].account_idintegernoExpense account

The created bill:

  • Uses the contact from the first commitment
  • Sets issue/due dates from the invoice extraction
  • Attaches the invoice PDF
  • Links back to the allocations

Returns 409 Conflict if:

  • Allocations are not approved
  • Invoice is already converted to a bill

Here’s the end-to-end flow for pushing orders and processing invoices:

POST /api/books/contacts/find_or_create
{
"contact": {
"external_id": "vendor-acme",
"name": "Acme Materials",
"contact_type": "vendor"
}
}
PUT /api/books/commitments/by_external/quarry/ORD-8834
{
"commitment": {
"contact_id": 42,
"number": "PO-2026-001",
"expected_total_cents": 109500,
"service_address": "123 Job Site Rd",
"period_start": "2026-03-01",
"period_end": "2026-03-31",
"commitment_lines_attributes": [
{ "description": "Excavator rental", "quantity": 1, "unit_price_cents": 109500 }
]
}
}

Upload the vendor invoice PDF:

POST /api/books/invoice_documents
Content-Type: multipart/form-data
file=@invoice.pdf

VisiBooks extracts vendor name, amounts, dates, and invoice number from the PDF.

In the VisiBooks inbox, the invoice review screen shows matching commitments automatically. The user approves the allocation and creates the bill.

Or via API:

POST /api/books/invoice_documents/7/allocate
{ "allocations": [{ "commitment_id": 1, "allocated_cents": 109500 }] }
POST /api/books/invoice_documents/7/create_commitment_bill
{ "non_po_lines": [] }
POST /api/books/bills/12/post_bill
POST /api/books/vendor_payments
{ "vendor_payment": { "contact_id": 42, "amount_cents": 109500, "paid_at": "2026-03-15" } }
POST /api/books/vendor_payments/1/apply
{ "applications": [{ "bill_id": 12, "amount_cents": 109500 }] }