Skip to content
Knowledge ERP Docs

API Reference ↗
Rentals

Rental Agreements

A Rental Agreement tracks the full paid-rental lifecycle — from draft through reservation, pickup, return, and final close-out — and generates a Customer Invoice with the actual duration, late fees, and any damage charges when you close it.

A Rental Agreement is the central record for a paid equipment rental. It ties a customer to one or more pieces of equipment for a date range, tracks the deposit and billing terms, and drives the status transitions that move equipment from your shelf to the customer and back. When you close the agreement a Customer Invoice is generated automatically, priced against the actual rental duration — not just the planned one.

Rental Agreements cover paid rentals billed to a customer. If you need to lend equipment at no charge, use a Loan Agreement instead.

Creating a rental agreement #

New agreements are created from Rentals → Rental Agreements → New Rental Agreement. Every agreement starts in Draft status, which lets you fill in details and add line units before committing to anything.

  • Customer — the customer being billed. Required; drives the contact list and is used to resolve customer-specific rental rates.
  • Contact — an optional contact from the selected customer for the person picking up the equipment.
  • Location — which inventory location the equipment comes from. Used for location-scoped rate lookups.
  • Start date / Expected return — the planned rental window. Both are required. The expected return date is used for overdue detection and for availability checks.
  • Security deposit — an amount (in dollars in the UI) held from the customer, then refunded as a credit memo when you close the agreement. Stored in cents internally.
  • Late fee % — a surcharge percentage applied to overdue days on top of the normal rate. The billing service converts this to a dollar line on the final invoice.
  • Damage charge — what the damage cost. You will normally not type this here: the Return action asks about damage as the equipment comes back, and adds what you record to this field. The security deposit settles against it first, so only what the deposit cannot cover reaches the invoice. Entered in dollars; stored in cents.
  • Billing cycleBill at close only, Weekly, or Monthly. Long-term rentals can be billed periodically; the billed_through_date advances after each cycle run.
  • Tax rate — an optional tax rate applied when the invoice is generated.
  • Notes — free-text visible on the agreement and invoice.

The agreement number is auto-generated in the format RA-0001, RA-0002, etc.

Rental Agreement Items #

Line items are added from the Items tab on the agreement's view page. Each line represents one type or unit of equipment being rented.

  • Rentable — what is being rented. A line can point to a Product (a class of equipment, e.g. "Lift, 40ft"), a specific Unit (a serialized unit), or a Kit (a bundle). Product-based lines use quantity; unit-based lines check out the exact unit.
  • Quantity — how many units (relevant for Product-based lines).
  • Rate periodDaily, Weekly, or Monthly. Determines the billing unit.
  • Rate amount — the price per period, in dollars in the UI (cents in the API). Leave it blank and the system snapshots the matching Rental Rate when you reserve. Type 0 and the line is rented out free of charge — a goodwill loaner, a warranty replacement, a demo unit you still want tracked and invoiced at $0.
  • Notes — per-line notes that carry through to the invoice description.

The Equipment picker only offers what can actually be rented. Consumable products — and individual units of them — are left out entirely, because renting one is refused outright. A returnable product with no rental rate is still offered, since typing a rate on the line rents it perfectly legally, but it is labelled as unpriced so you know before choosing it that a rate will be needed.

Searching Specific unit matches the product's name, the unit's own name, its lot or serial number, and its barcode — so a unit can be found by scanning or typing the label in your hand, and each option reads as the product, the lot and the barcode together rather than a lot number on its own.

Rentals are for Returnable equipment only — reserving or picking up a rental whose Product is marked Consumable is rejected, in the app and over the API alike. Renting something out promises its return, which consumable stock cannot make: it belongs on a purchase order or sold through Sales. The same rule applies to loans.

Blank is not the same as zero #

A blank rate means nobody has priced this line yet. A rate of 0 means someone decided this is free. Knowledge ERP keeps the two apart, because they lead to very different places.

If a line is still blank when you try to reserve or pick up — because no rental rate is configured for that Product and period, or the one that exists is inactive — the action is refused, naming the product and the period that is missing. The same check runs again when the invoice is raised at close. Nothing silently invoices $0.

A rate you typed yourself is never overwritten by the catalog, so a deliberate 0 stays 0 through the whole lifecycle. Zero is legitimate; unpriced is not.

If a pickup is refused, add the missing Rental Rate (or type a rate straight onto the line) and press Pick Up again — the rate is re-snapshotted on the retry. There is no need to unwind the reservation.

What the hire costs before it is invoiced #

The money columns on an agreement — the line totals, the subtotal, the total — are written when the invoice is raised at close, and are zero before that. So every screen that shows a rental's total shows an estimate until it is closed: the line items table, the agreement's Charges panel, the agreements list, and the customer's own view in the portal all label it as one. Over the API the same number is estimated_total on the agreement, and line_total on each line.

The estimate runs the invoice's own arithmetic — the same window, the same per-unit period counts, the same damage-against-deposit settlement — so the price quoted at the counter is the price the invoice will carry. It moves for the same reasons the invoice would: extending the return date, changing a rate, or bringing a unit back early.

An unpriced line contributes nothing to the estimate, which is the honest answer — nobody has decided what it costs, and reserving it is refused for that reason.

The rental lifecycle #

A rental agreement moves through a fixed set of statuses. Each transition is a distinct action in the UI and an API sub-action:

Status What it means
Draft Being built — no holds placed, nothing committed.
Reserved Availability confirmed; equipment is held for this date range. Rates are snapshotted.
Picked Up Equipment has left the building; individual units are checked out and marked unavailable.
Partially Returned Some units are back in stock; the rest are still out. The agreement stays here until the last unit returns.
Returned Equipment is back and checked in to a bin; not yet invoiced.
Closed Invoice generated, deposit refunded as a credit memo. Terminal.
Cancelled Agreement ended without completion. Cannot cancel while status is Picked Up or Partially Returned — return the equipment first.

Reserve checks availability across the full date range and confirms each line can be fulfilled. If any piece of equipment is already booked, the reserve action fails with a clear message rather than double-booking silently.

Pickup re-checks availability at the moment of departure and checks out specific inventory units for every line — Knowledge tracks the life of each individual unit, so a Product line of quantity 3 allocates three real units and a kit checks out each of its component units. Every checkout is linked back to its line. The units become Checked Out in inventory and are no longer available to others.

Return requires a destination bin, and lets you choose which units are coming back (all of them by default). The bins offered are the ones at the site the units went out from — a bin at another site is refused, since the bin is what sets the unit's location and returning to one elsewhere would move the machine rather than put it away. Permission to view bins is not needed to be offered them; returning a hire is rental work.

Return everything and the agreement moves to Returned; return only some and it becomes Partially Returned, with the returned units back in stock and available again while the rest stay out. A Partially Returned agreement can be returned again later to bring in the remainder.

The same screen is where you record damage, while the equipment is in front of you: tick the units that came back damaged, enter what the damage costs, and say what happened. The charge is added to the agreement's damage charge (a second damaged unit on a later partial return adds to it rather than replacing it), the account of it is written into the agreement's notes, and each damaged unit's check-in movement is marked — so "which one broke?" is answerable from the unit's own history months later. Only a unit being returned right now can be marked damaged.

Taking equipment back at another branch.

Return it against a bin at its origin, then move it with the Transfer action on the unit itself. (Return is the action that takes a bin; Close & Invoice takes none — it is what raises the invoice afterwards.) Two steps rather than one on purpose: it records an inter-site movement, which is what actually happened, so the stock figures at both branches stay right.

Both steps together need a role at both ends — the origin, because the Transfer action is only offered on a unit you may edit, and the destination, because the same form asks for update_inventory_items at the site you are moving it to. Both ends want update_inventory_items, not merely view. A clerk who covers only one end gets stuck halfway, so in practice this is a job for somebody who covers both branches, or for an administrator. (One exception, itself a bug: a reader who can see every bin in the account can leave the Transfer form's location blank and pick the far bin directly.)

A machine you cannot see cannot be checked in, and the app now says so. A clerk with a role only at the branch where it was handed back cannot do step one: the unit belongs to a branch they hold no role at, so it is invisible to their account and there is nothing for a return to write to. The return is refused, in these words:

Your role does not cover the site it went out from, so it cannot be checked in under your account. Ask a colleague with a role at that site to receive it, or ask an administrator to give you one.

The same sentence on the screen, from POST /rental-agreements/{id}/return as a 422, and from the MCP tool. Nothing is written — the hire stays as it was, the machine stays checked out where it is, and billing is untouched. If the agreement happens to name a branch you hold no role at either, you will not be able to open it at all: Not Found on the page, 404 from the API. From the mobile app it is refused too, whichever way you come at it — its endpoints address the unit, and the unit is the thing you cannot see.

So the answer is simply to hand the job to a colleague who covers the origin branch. There is no half-finished state to worry about any more: this used to report success and quietly write the hire to Partially Returned over a machine that never moved — which left it unbookable, kept overdue reminders running, and invoiced the customer as though it had never come back. It does not do that now.

Close generates the Customer Invoice priced by the actual rental duration. Each unit is billed per its own time out — a unit returned early stops accruing on its return date while units still out keep billing through the end of the window — so a partial return is reflected automatically. Close is available from Picked Up, Partially Returned, and Returned. Late fees (computed per unit on the days each was held past the due date) and damage charges are added as separate invoice lines. If a deposit was collected, a credit memo is issued automatically for the deposit net of any damage charge. The invoice starts in Draft status.

Because the deposit settles first, a damage charge smaller than the deposit never appears on the invoice — it comes out of the refund instead. The close confirmation spells the whole settlement out before you commit to it ("Rental $128.82 · damage $85.00 settled against the $200.00 deposit · invoicing $128.82 · refunding $115.00 of the deposit"), and the agreement's Charges panel repeats it afterwards with links to both the invoice and the credit memo.

Picked Up and Reserved statuses block availability. Other rental agreements that overlap the same dates for the same equipment will be refused at reserve time.

Availability checking #

Before committing an agreement, you can check availability from Rentals → Rental Calendar or via the API. The calendar shows what equipment is out and when, so you can spot gaps before creating agreements.

The rental agreements list #

The list (Rentals → Rental Agreements) shows each agreement's number, customer, status badge, start date, expected return, and total amount. Search by customer name or agreement number (status filtering is available via the API). Agreements are sorted newest-first.

When you close a rental agreement, two records are created automatically:

  • A Customer Invoice linked via customer_invoice_id — see Customer Invoices for how to collect payment.
  • A Credit Memo linked via deposit_credit_memo_id (if a deposit was collected), which can be applied to any outstanding invoice for the customer.

Rental agreements also carry a full change history tab powered by the change tracker.

Doing it from the API #

# Create a rental agreement in Draft
curl -X POST "https://your-domain.com/api/v1/rental-agreements" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "...",
    "start_date": "2026-07-01",
    "end_date": "2026-07-07",
    "deposit_amount": 25000,
    "billing_cycle": "none"
  }'

# Reserve it (confirms availability and snapshots rates)
curl -X POST "https://your-domain.com/api/v1/rental-agreements/{id}/reserve" \
  -H "Authorization: Bearer $TOKEN"

# Return only some units (partial return). Omit checkout_ids to return everything.
curl -X POST "https://your-domain.com/api/v1/rental-agreements/{id}/return" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"bin_id": "<bin-id>", "checkout_ids": ["<checkout-id>"]}'

# Return everything and record damage on one of the units (cents).
curl -X POST "https://your-domain.com/api/v1/rental-agreements/{id}/return" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"bin_id": "<bin-id>", "damage_charge": 8500,
       "damaged_checkout_ids": ["<checkout-id>"], "damage_notes": "Cracked chuck."}'

# Close it (generates the invoice and refunds the deposit)
curl -X POST "https://your-domain.com/api/v1/rental-agreements/{id}/close" \
  -H "Authorization: Bearer $TOKEN"

A picked-up agreement lists the units still out under out_units (each with a checkout_id) — pass a subset as checkout_ids on the return call to return only those. The agreement reports partially_returned until the last unit is back, and a rental.partially_returned webhook fires on that transition.

Money fields (deposit_amount, damage_charge, and rate_amount on line items) are integers in cents over the API (25000 = $250.00). Invalid lifecycle transitions (e.g. closing a Draft) return HTTP 422 with an error message.