API and webhooks
Read your portfolio from your own software, and be told the moment a lease, a rent payment or an expense is recorded.
Getting started
The API gives read-only JSON access to one business's records (included in the Pro and Enterprise plans). The owner or an admin makes a key under Settings → API keys; send it with every request:
curl "https://leasystream.com/api/v1/properties?limit=5" \ -H "Authorization: Bearer lsapi_your_key_here"
- Base address:
https://leasystream.com/api/v1. HTTPS only. - The key goes in the
Authorization: Bearerheader and nowhere else. Cookies and keys in the address are ignored. - A key reads the whole business: it is not limited to a person, branch or property. It cannot change anything.
- Amounts are numbers, and every record with money has a
currencycode next to them. Times are ISO 8601 in the business's timezone; dates areYYYY-MM-DD. GET https://leasystream.com/api/v1/metells you which business a key belongs to, its currency and timezone.
Lists and pages
GET /{resource} returns up to limit records (default 50, most 100). Ask for the next page with page=2, or follow links.next until it is null:
{
"data": [ { … }, { … } ],
"meta": { "page": 1, "limit": 50, "has_more": true },
"links": { "next": "/api/v1/properties?page=2&limit=50" }
}
GET /{resource}/{id} returns one record as {"data": { … }}. Filters go in the query string; a filter the resource does not have is ignored. Remember to URL-encode values (a + in a time must be sent as %2B).
Errors and limits
| Status | code | Meaning |
|---|---|---|
| 400 | bad_request | A filter value is wrong, for example a date not written as YYYY-MM-DD. |
| 401 | unauthorized | Missing, wrong or revoked key. |
| 403 | plan, subscription_suspended | The business's plan does not include the API, or its subscription is suspended. |
| 404 | not_found | No such resource or record. |
| 405 | method_not_allowed | Only GET is supported. |
| 429 | rate_limited | More than 120 requests in a minute with one key. Wait for the seconds given in Retry-After. |
Errors look like {"error": {"code": "not_found", "message": "No record with that id."}}. Answers carry X-RateLimit-Limit and X-RateLimit-Remaining.
Resources
| Resource | What it is | Single record | Filters |
|---|---|---|---|
properties |
Buildings and plots in the portfolio. | yes | q, status, branch_id, updated_since |
units |
Rentable units inside properties. | yes | property_id, status, updated_since |
renters |
People and organisations who rent. | yes | q, status, updated_since |
landlords |
Property owners the business manages for. | yes | q, status, updated_since |
leases |
Leases, newest first. | yes | status, renter_id, landlord_id, property_id, unit_id, updated_since |
rent-charges |
What is owed for each rent period, newest first. | yes | lease_id, status, from, to |
rent-payments |
Rent received, newest first. | yes | lease_id, renter_id, status, from, to |
payments |
Other money received (deposits, application and reservation fees), newest first. | yes | purpose, status, from, to |
expenses |
Money spent, newest first. | yes | property_id, vendor_id, payment_status, from, to |
disbursements |
Payouts to landlords, newest first. | yes | landlord_id, property_id, status |
maintenance |
Maintenance requests, newest first. | yes | property_id, unit_id, status, priority |
branches |
The business's offices. | list only | none |
properties
Buildings and plots in the portfolio. GET /api/v1/properties and GET /api/v1/properties/{id}
Filters: q (name or code contains); status (active, inactive, under_construction, sold, archived); branch_id (a branch); updated_since (changed on or after this date or time).
| Field | Meaning |
|---|---|
id | Record id |
code | Property code |
name | Name |
type | residential, commercial, mixed_use, industrial or land |
category | Kind of building |
ownership_mode | owner_operator, managed or mixed |
units_count | Number of units recorded on the property |
address | line1, line2, city, state, postal_code, country |
branch | id and name, or null |
landlord | Primary owner: id and name, or null |
status | Status |
created_at | When it was added |
updated_at | When it last changed |
units
Rentable units inside properties. GET /api/v1/units and GET /api/v1/units/{id}
Filters: property_id (a property); status (available, occupied, reserved, maintenance, off_market); updated_since (changed on or after this date or time).
| Field | Meaning |
|---|---|
id | Record id |
property | id and name |
code | Unit code |
name | Unit name |
block | Block |
floor | Floor |
type | Unit type |
bedrooms | Bedrooms |
bathrooms | Bathrooms |
floor_area | Floor area |
floor_area_unit | sqm or sqft |
base_rent | Asking rent |
base_rent_frequency | monthly, weekly, daily, quarterly or annually |
currency | Currency of base_rent (the business currency) |
status | Status |
available_from | Date it becomes available |
created_at | When it was added |
updated_at | When it last changed |
renters
People and organisations who rent. GET /api/v1/renters and GET /api/v1/renters/{id}
Filters: q (name, email or phone contains); status (active, pending, suspended, blacklisted, archived); updated_since (changed on or after this date or time).
| Field | Meaning |
|---|---|
id | Record id |
number | Renter number |
type | person or organization |
name | Name |
email | |
phone | Phone |
alt_phone | Second phone |
status | Status |
branch | id and name, or null |
created_at | When it was added |
updated_at | When it last changed |
landlords
Property owners the business manages for. GET /api/v1/landlords and GET /api/v1/landlords/{id}
Filters: q (name, email or phone contains); status (active, pending, suspended, blacklisted, archived); updated_since (changed on or after this date or time).
| Field | Meaning |
|---|---|
id | Record id |
number | Landlord number |
type | person or organization |
name | Name |
email | |
phone | Phone |
alt_phone | Second phone |
status | Status |
branch | id and name, or null |
created_at | When it was added |
updated_at | When it last changed |
leases
Leases, newest first. GET /api/v1/leases and GET /api/v1/leases/{id}
Filters: status (draft, active, expired, terminated, renewed); renter_id (a renter); landlord_id (a landlord); property_id (a property); unit_id (a unit); updated_since (changed on or after this date or time).
| Field | Meaning |
|---|---|
id | Record id |
number | Lease number |
kind | head or sublease |
status | Status |
unit | id, code and name |
property | id and name |
renter | id and name |
landlord | id and name (on a sub-lease: the head tenant, who is a renter and not in landlords) |
start_date | Start date |
end_date | End date |
rent_amount | Rent per period |
currency | Currency of the lease |
rent_frequency | monthly, weekly, daily, quarterly or annually |
payment_due_day | Day of the month rent falls due |
security_deposit | Security deposit agreed |
move_in_date | Move-in date |
move_out_date | Move-out date |
terminated_at | Date terminated |
created_at | When it was added |
updated_at | When it last changed |
is_sublease | true when this lease is a sub-lease |
head_lease | id and number of the lease it is a sub-lease of, or null |
sublease_scope | whole_unit or partial on a sub-lease, otherwise null |
sublease_portion | The part of the unit sub-let, or null |
sublease_admin_fee_pct | Percentage of each sub-rent payment the business keeps, or null when not a sub-lease |
rent-charges
What is owed for each rent period, newest first. GET /api/v1/rent-charges and GET /api/v1/rent-charges/{id}
Filters: lease_id (a lease); status (pending, partial, paid, overdue, waived); from (due on or after); to (due on or before).
| Field | Meaning |
|---|---|
id | Record id |
number | Charge number |
lease | id and number |
period_start | Period start |
period_end | Period end |
due_date | Due date |
rent_due | Rent |
extras_due | Other charges |
late_fee_due | Late fees |
total_due | Total |
amount_paid | Paid so far |
balance | Still owed |
currency | Currency |
status | Status |
paid_at | When it was paid in full |
created_at | When it was raised |
rent-payments
Rent received, newest first. GET /api/v1/rent-payments and GET /api/v1/rent-payments/{id}
Filters: lease_id (a lease); renter_id (a renter); status (recorded or voided); from (paid on or after); to (paid on or before).
| Field | Meaning |
|---|---|
id | Record id |
receipt_number | Receipt number |
lease | id and number |
renter | id and name |
date | Date paid |
amount | Amount |
currency | Currency |
method | Payment method name |
reference | Transaction or cheque reference |
status | recorded or voided |
voided_at | When it was voided |
created_at | When it was recorded |
verification_code | The code printed on the receipt for checking it at /verify (XXXX-XXXX-XXXX), or null when the receipt has none yet |
allocations | Single record only: the rent charges this payment was applied to (rent_charge_id, charge_number, amount) |
payments
Other money received (deposits, application and reservation fees), newest first. GET /api/v1/payments and GET /api/v1/payments/{id}
Filters: purpose (application_fee, deposit_only, refund_received, reservation_fee, utility_deposit, other); status (recorded or voided); from (paid on or after); to (paid on or before).
| Field | Meaning |
|---|---|
id | Record id |
number | Payment number |
purpose | What it was for |
party | Who paid: id and name, or null |
lease | id and number, or null |
date | Date paid |
amount | Amount |
currency | Currency |
method | Payment method name |
reference | Transaction or cheque reference |
status | recorded or voided |
voided_at | When it was voided |
created_at | When it was recorded |
verification_code | The code printed on the receipt for checking it at /verify (XXXX-XXXX-XXXX), or null when the receipt has none yet |
expenses
Money spent, newest first. GET /api/v1/expenses and GET /api/v1/expenses/{id}
Filters: property_id (a property); vendor_id (a vendor); payment_status (paid, pending, voided); from (dated on or after); to (dated on or before).
| Field | Meaning |
|---|---|
id | Record id |
number | Expense number |
date | Expense date |
category | Category name |
description | Description |
amount | Amount |
currency | Currency |
payment_status | paid, pending or voided |
vendor | id and name, or null |
property | id and name, or null |
unit_id | Unit, or null |
chargeable_to_landlord | Whether the landlord bears it |
voided_at | When it was voided |
created_at | When it was recorded |
disbursements
Payouts to landlords, newest first. GET /api/v1/disbursements and GET /api/v1/disbursements/{id}
Filters: landlord_id (a landlord); property_id (a property); status (draft, approved, paid, cancelled).
| Field | Meaning |
|---|---|
id | Record id |
number | Disbursement number |
landlord | id and name |
property | id and name, or null |
period_start | Period start |
period_end | Period end |
gross_collected | Rent collected |
management_fee | Management fee |
tax_withheld | Tax withheld |
expenses_deducted | Expenses deducted |
adjustments | Adjustments |
net_paid | Net amount to the landlord |
currency | Currency |
status | Status |
paid_at | When it was paid |
created_at | When it was prepared |
maintenance
Maintenance requests, newest first. GET /api/v1/maintenance and GET /api/v1/maintenance/{id}
Filters: property_id (a property); unit_id (a unit); status (pending, triaged, work_order_created, in_progress, completed, cancelled, duplicate); priority (emergency, high, medium, low).
| Field | Meaning |
|---|---|
id | Record id |
number | Request number |
title | Title |
description | What was reported |
category | Category name |
priority | emergency, high, medium or low |
status | Status |
property | id and name, or null |
unit | id, code and name, or null |
reported_at | When it was reported |
completed_at | When it was completed |
created_at | When it was logged |
updated_at | When it last changed |
branches
The business's offices. GET /api/v1/branches
| Field | Meaning |
|---|---|
id | Record id |
name | Name |
code | Branch code |
location | Location |
is_main | The main branch |
is_active | In use |
Not available through the API or webhooks: staff accounts, internal notes and void reasons, national IDs and dates of birth, renters' employment details, landlords' bank details, and uploaded documents.
Webhooks
Webhooks (Growth, Pro and Enterprise plans) push events to your server instead of you asking. The owner or an admin adds an https address under Settings → Webhooks and picks the events. A POST with a JSON body arrives within about a minute:
POST /your/path HTTP/1.1
Content-Type: application/json
Leasystream-Event: lease.created
Leasystream-Delivery: evt_<number>
Leasystream-Signature: t=<unix time>,v1=<64 hex characters>
{
"id": "evt_<number>",
"type": "lease.created",
"created_at": "<ISO 8601 time>",
"business": { "id": <number>, "name": "<business name>" },
"data": { … the record, in the same shape as the API returns it … }
}
- Answer with any 2xx status within 10 seconds; do slow work after answering.
- Otherwise it is tried again after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h, then marked failed. After 50 failures in a row the webhook is switched off.
- The same event can occasionally arrive twice, and events can arrive out of order: use
idto ignore repeats. - The address must be https with a valid certificate and must be on the public internet. Redirects are not followed.
datashows the record as it was when the message was first sent; retries carry the same body.
Checking the signature
Leasystream-Signature holds a time (t) and v1, the HMAC-SHA256 of t + "." + the raw request body, keyed with the webhook's signing secret (whsec_…, shown once when the webhook is added or a new secret is made). Compute it yourself over the bytes exactly as received, compare in constant time, and refuse messages more than 5 minutes old.
// PHP
$body = file_get_contents('php://input');
$secret = getenv('LEASYSTREAM_WEBHOOK_SECRET'); // whsec_…
$header = $_SERVER['HTTP_LEASYSTREAM_SIGNATURE'] ?? '';
$ok = preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m)
&& abs(time() - (int) $m[1]) <= 300
&& hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, $secret), $m[2]);
if (!$ok) { http_response_code(400); exit; }
$event = json_decode($body, true);
http_response_code(200);
// Node.js (Express: use express.raw({ type: 'application/json' }) on this route,
// so req.body is the raw Buffer; header is req.get('Leasystream-Signature'))
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(m[1] + '.').update(rawBody).digest();
return crypto.timingSafeEqual(expected, Buffer.from(m[2], 'hex'));
}
Events
| Event | When | data is |
|---|---|---|
lease.created | A lease is entered (as a draft or already active) | one of leases |
lease.activated | A lease becomes active | one of leases |
lease.ended | A lease is terminated or expires | one of leases |
rent_charge.created | A rent charge is raised for a period | one of rent-charges |
rent_charge.paid | A rent charge is paid in full | one of rent-charges |
rent_charge.overdue | A rent charge passes its due date unpaid | one of rent-charges |
rent_payment.received | A rent payment is recorded | one of rent-payments |
rent_payment.voided | A rent payment is voided | one of rent-payments |
payment.received | Another payment is recorded (deposit, application or reservation fee, …) | one of payments |
payment.voided | One of those other payments is voided | one of payments |
renter.created | A renter is added | one of renters |
renter.updated | A renter's name, email, phone or status changes | one of renters |
landlord.created | A landlord is added | one of landlords |
landlord.updated | A landlord's name, email, phone or status changes | one of landlords |
disbursement.paid | A landlord disbursement is marked paid | one of disbursements |
expense.created | An expense is recorded | one of expenses |
expense.voided | An expense is voided | one of expenses |
maintenance.created | A maintenance request is logged | one of maintenance |
maintenance.completed | A maintenance request is completed | one of maintenance |
ping | You press "Send a test event" | {"message": "…"} |
renter.updated and landlord.updated are combined when a record changes several times before the message goes out. A rent_payment.received message includes allocations, the rent charges the payment was applied to.