Ownware
Home›Rentara›API
Rentara · API

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

MethodPathNotes
GET/api/pingVerify a key
GET/api/units`?status=vacant\occupied`
GET/api/leases`?status=active\ended\upcoming`
GET/api/leases/{id}/ledgerEvery charge + billed/paid/balance totals
POST/api/leases/{id}/paymentsRecord a payment against one charge
GET/api/arrearsEverything overdue right now
GET/api/maintenance`?status=open\in_progress\resolved`
POST/api/maintenanceOpen a ticket
POST/api/maintenance/{id}/statusMove a ticket
GET/api/openapi.jsonOpenAPI 3.0.3 — imports as a custom connector
GET/healthzUnauthenticated {"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 method becomes other rather than storing junk
  • a missing or malformed paid_date becomes 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"}}
ToolReads or writesWhat it does
list_propertiesREAD-ONLYProperties with unit counts and occupancy.
list_leasesREAD-ONLYLeases with unit, tenant, rent and status; active=true to narrow.
lease_detailREAD-ONLYOne lease with its full ledger and current balance.
rent_rollREAD-ONLYBilled vs collected for a month.
arrears_reportREAD-ONLYWho is behind and by how much.
create_ticketWRITESRaise a maintenance ticket against a unit.
advance_ticketWRITESMove 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 addresshttps://rentara.example.com/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP 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:

EventFires when
payment.recordedA payment is posted (browser or API)
maintenance.createdA ticket is opened — including tenant-portal submissions
maintenance.status_changedA 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

CodeMeaning
401 unauthorizedMissing, malformed, revoked or unknown key
404 not_foundNo such lease / ticket
409 conflictCharge already paid
422 validationRefused 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.

← Back to Rentara · Manual · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →