Rentara API, as shipped in the download
Rentara API, MCP & Webhooks (v3.0)
Rentara's REST API exposes the rent ledger the way your accountant actually wants it: every charge, what was paid against it, and what is still owed — with the arrears list computed the same way the arrears page computes it, not by a second implementation that can drift.
Authentication
Create a key in Settings → API & webhooks. It is shown once.
Authorization: Bearer apk_<40 hex chars>
A key acts as the user it belongs to and can do exactly what that user can do in the browser. /api/* is CSRF-exempt by design: the key is the credential and browsers never send it implicitly.
curl -H "Authorization: Bearer apk_xxxx" https://rentara.example.com/api/ping
# {"ok":true,"user":"Sam","app":"rentara","version":"3.1.0","role":"admin"}
Endpoints
| Method | Path | Notes | ||
|---|---|---|---|---|
| GET | /api/ping | Verify a key | ||
| GET | /api/units | `?status=vacant\ | occupied` | |
| GET | /api/leases | `?status=active\ | ended\ | upcoming` |
| GET | /api/leases/{id}/ledger | Every charge + billed/paid/balance totals | ||
| POST | /api/leases/{id}/payments | Record a payment against one charge | ||
| GET | /api/arrears | Everything overdue right now | ||
| GET | /api/maintenance | `?status=open\ | in_progress\ | resolved` |
| POST | /api/maintenance | Open a ticket | ||
| POST | /api/maintenance/{id}/status | Move a ticket | ||
| GET | /api/openapi.json | OpenAPI 3.0.3 — imports as a custom connector | ||
| GET | /healthz | Unauthenticated {"ok":true,"app":"rentara","version":"2.0.0"} |
The lease ledger
curl -H "Authorization: Bearer apk_xxxx" https://rentara.example.com/api/leases/7/ledger
{
"lease": {"id": 7, "unit": "1A", "property": "Oakwood", "tenant": "R. Vance",
"rent_cents": 120000, "frequency": "monthly", "status": "active"},
"charges": [
{"id": 41, "type": "rent", "period": "2026-07", "label": "Rent 2026-07",
"due_date": "2026-07-01", "amount_cents": 120000, "paid": true,
"paid_date": "2026-07-02", "method": "transfer"},
{"id": 58, "type": "rent", "period": "2026-08", "label": "Rent 2026-08",
"due_date": "2026-08-01", "amount_cents": 120000, "paid": false,
"paid_date": null, "method": null}
],
"totals": {"billed_cents": 240000, "paid_cents": 120000, "balance_cents": 120000}
}
Requesting a ledger runs the same catch-up the Rent page runs, so every period that should have been billed by today is present before the totals are computed. You never see a half-posted ledger just because nobody opened the app this month.
Recording a payment
curl -X POST https://rentara.example.com/api/leases/7/payments \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"charge_id": 58, "method": "transfer", "paid_date": "2026-08-03"}'
charge_id is required — payments attach to a specific charge, which is what makes the ledger add up. The posting rules are the product's own (Billing::recordPayment), identical to the Rent page:
- an unrecognised
methodbecomesotherrather than storing junk - a missing or malformed
paid_datebecomes today - re-paying an already-paid charge is refused with
409, so a retried webhook or a double-submitted form cannot mark the same rent paid twice
{"error":"conflict","detail":"That charge is already marked paid — reverse it first to re-post."}
Arrears
curl -H "Authorization: Bearer apk_xxxx" https://rentara.example.com/api/arrears
Returns every unpaid charge whose due date has passed, with days_overdue, plus total_cents for the whole book. This is computed on request from the charge table — Rentara runs no cron, so there is no stale nightly snapshot to disagree with the screen.
Maintenance
curl -X POST https://rentara.example.com/api/maintenance \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"unit_id": 12, "title": "No hot water", "priority": "high"}'
Status moves run through the product's transition table: open ↔ in_progress → resolved, and resolved reopens to open. Resolving stamps resolved_at; reopening clears it. An illegal move is refused with 422.
MCP — the endpoint for AI agents (new in 3.0)
Rentara speaks the Model Context Protocol at POST /mcp, stateless streamable-HTTP: one JSON-RPC message in, one JSON response out. Authentication is the same API key as the REST surface, and it carries the same role. GET /mcp answers 405 with Allow: POST rather than 404, so a probing client can tell "wrong method" from "no MCP support".
POST /mcp
Authorization: Bearer rk_...
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}
| Tool | Reads or writes | What it does |
|---|---|---|
list_properties | READ-ONLY | Properties with unit counts and occupancy. |
list_leases | READ-ONLY | Leases with unit, tenant, rent and status; active=true to narrow. |
lease_detail | READ-ONLY | One lease with its full ledger and current balance. |
rent_roll | READ-ONLY | Billed vs collected for a month. |
arrears_report | READ-ONLY | Who is behind and by how much. |
create_ticket | WRITES | Raise a maintenance ticket against a unit. |
advance_ticket | WRITES | Move a ticket through the product's own transition table. |
Nothing is re-derived. arrears_report returns the output of Rent::arrears() and rent_roll that of Rent::monthTotals() — the same functions the screens render. The suite asserts equality between the tool's answer and the engine's rather than trusting the code to stay honest: an agent that disagreed with the landlord about who owes what would be worse than no agent.
advance_ticket uses the product's transition table, reopen included. The legal moves are open ⇄ in_progress → resolved and resolved → open. That last one is deliberate and it is the rule a re-implementation would quietly drop: a job marked fixed that was not fixed has to go back. Any other move is refused with the permitted ones named — resolved → in_progress, for instance, is not a legal step. An agent is held to exactly the same table as a person clicking in the app, because there is only one table.
Arguments are guarded. Every argument must be a single scalar; unknown arguments are refused by name, and values outside an enum are refused with the permitted list.
Roles apply identically. A viewer's key can call every read tool and is refused on create_ticket and advance_ticket with a message naming the role.
Connect it to an assistant
Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.
Every client needs the same three facts, and nothing in the handshake is vendor-specific:
| The address | https://rentara.example.com/mcp |
| The key | header Authorization: Bearer apk_xxxx |
| The transport | MCP over streamable HTTP, stateless |
Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:
claude mcp add --transport http rentara https://rentara.example.com/mcp \
--header "Authorization: Bearer apk_xxxx"
ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):
{
"type": "mcp",
"server_label": "rentara",
"server_url": "https://rentara.example.com/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
Own Your AI reads a list of servers in this shape:
{
"mcpServers": [
{ "id": "rentara", "name": "Rentara", "url": "https://rentara.example.com/mcp",
"token": "apk_xxxx", "enabled": true }
]
}
**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.
A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:
curl -X POST https://rentara.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Rentara implements the protocol, not an integration with a particular vendor.
Webhooks
Add a receiver in Settings → API & webhooks. Events:
| Event | Fires when |
|---|---|
payment.recorded | A payment is posted (browser or API) |
maintenance.created | A ticket is opened — including tenant-portal submissions |
maintenance.status_changed | A ticket moves state |
maintenance.created firing on the tenant portal is the one to wire up first: a tenant reporting no hot water at 22:00 reaches your phone without anyone watching a dashboard.
Each delivery is a JSON POST signed with your per-endpoint secret:
X-Rentara-Event: maintenance.created
X-Rentara-Signature: sha256=<hmac_sha256(raw_body, secret)>
import hmac, hashlib
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers["X-Rentara-Signature"]):
abort(401)
One attempt per event with a 5-second timeout — a slow receiver must never block someone recording a payment. The last 200 deliveries per endpoint are logged with their response code.
Errors
| Code | Meaning |
|---|---|
401 unauthorized | Missing, malformed, revoked or unknown key |
404 not_found | No such lease / ticket |
409 conflict | Charge already paid |
422 validation | Refused by a guard — detail says which, in plain language |
Money
Every amount is integer cents (120000 = 1,200.00) in both directions. No floats, ever.