Leasey API Documentation
Leasey offers two separate APIs. This page starts with everything they have in common — credentials, authentication, environments, reports, webhooks and the property object — and then lets you pick the one you integrate with.
Last updated: November 6, 2026
openapi.yaml (OpenAPI 3.18 — import it in Postman and every endpoint appears pre-built) ·
Markdown version ·
llms.txt ·
llms-full.txt · Status pageWhich API do you need?
| General API | Syndication API | |
|---|---|---|
| What it is for | Keeping your property data in sync with Leasey | Publishing your units to marketplaces through Leasey |
| You can |
|
|
| Main endpoints | POST/PUT /properties, /showings/* (lookup, availability, book, reschedule, confirm, cancel), /applications, /screening, /leases, /documents, /document-templates | POST/PUT /properties (with marketplaces), GET /properties/{id}/marketplaces, GET /properties/leads, /org/* |
| Webhook events | lead.received, application.stage_changed, application.decision_changed, screening.status_changed, lease.status_changed, document.status_changed | lead.received, unit.published, unit.unpublished |
| Not included | Publishing to marketplaces, marketplace billing, organizations. | The showings endpoints are documented in the General API tab (organizations can use them per company). |
| Common to both | Credentials and authentication · environments · conventions and errors · the 5 reports · webhook delivery and signature · the property object | |
Everything in these sections applies whether you use the General API or the Syndication API.
1Getting Started
Credentials and where to get them
| Credential | What it is | Where it comes from |
|---|---|---|
| API key (recommended) | One secret per key, lsy_live_… in production and lsy_test_… in the sandbox. It identifies your company on its own | Leasey dashboard → Profile → Developers (company administrators), or POST /api-keys |
| API token (legacy) | Partner-level bearer token, identical on every request | Provided by Leasey during onboarding |
company_id (legacy) | The Leasey account you act for | Leasey dashboard → Settings, or your onboarding email |
| Company password (legacy) | That account's API password (passwordapi) | Leasey dashboard → Settings → API Access |
Which API do you use?
| If you… | You use | Go to |
|---|---|---|
| Keep your units in your own system, want them imported into Leasey, and want properties, leads, showings and the leasing pipeline (applications, screening, leases, e-signature) back out of Leasey, or run that pipeline from your system | General API | General API tab |
| Publish units to marketplaces through Leasey and collect the leads they generate — with one account, or as an organization managing many client companies | Syndication API | Syndication API tab |
| Only read data (dashboards, CRMs, BI) | Either — the Reports API is common to both | Part 1 (this part) |
2Authentication & API Keys
API keys (recommended)
Send only the key, in the Authorization header. No company_id and no company password: the key already says which company you are.
GET /reports/syndication
Authorization: Bearer lsy_live_<40 characters>
PUT /properties/39e00f97-c0a0-48f0-9ebb-38f1a50e3f5b
Authorization: Bearer lsy_live_<40 characters>
Content-Type: application/json
{ "property": { "marketplaces": [ { "name": "kijiji", "enabled": true } ] } }
- Every endpoint accepts an API key, with the same requests and responses. If a request also carries
company_idorpassword, they are ignored: a key only acts on its own company. - Access level. A Full access key can do everything. A Read only key can only
GET(reports, leads, showings, catalogs, applications, screening, leases, documents); any write answers 403 withcode: api_key_read_only. Use read-only keys for dashboards and reporting tools. - Create and revoke keys in the Leasey dashboard: Profile → Developers. Only company administrators see that section, the full key is shown once (with a copy button), and every administrator receives an email when a key is created (in the company's language). Revoking sends no email and takes effect immediately on every route.
- Expiration (optional). By default a key never expires: it works until it is revoked. When you create it you can choose a date instead (30 days, 90 days, 1 year or any date); after that moment every request with the key answers 401 with
code: api_key_expired("This API key has expired. Create a new one in Leasey: Profile > Developers."). The date cannot be changed later: before it arrives, create the replacement and revoke the old key. Each key in the list carriesstatus(active,expiredorrevoked) andexpires_soon(truein the last 7 days, while the key still works). - Limited to some resources (optional). A key can also be limited to the resources it needs, for example a voice agent that only books showings:
"resources": ["showings"]when you create it withPOST /api-keys. Any other route answers 403 withcode: api_key_resource_not_allowed. The resources areproperties(units and marketplaces),leads(/properties/leads),showings,reports,webhooks,applications,screening,leases,documentsandapi_keys(list, create and revoke keys). Access level and resources combine: a read-only key limited toapplicationscan only read applications. A key without the list, like every key created before 3.15, can use every resource. A limited key can only create keys within its own resources, and needsapi_keysto manage keys at all. - Rotate without downtime from your own system, using a full-access key:
| Method | Path | What it does |
|---|---|---|
GET | /api-keys | Your company's keys, never the secret: id, label, scope, key_prefix, key_last4, created_at, created_by, last_used_at, last_used_ip, expires_at, revoked_at, status, expires_soon, resources (null when the key can use every resource), and which one made the call (current_key_id) |
POST | /api-keys | {"label": "Production server", "scope": "full" | "read", "expires_at": "2027-06-30", "resources": ["showings"]} → 201 with api_key, shown once. expires_at is optional (leave it out for a key that never expires); a date is valid until the end of that day in UTC, a date-time without offset is read as UTC, Z and offsets are accepted. A past or malformed value answers 422 invalid_expires_at. resources is optional too: leave it out for a key that can use every resource. A list with an unknown resource answers 422 invalid_resources. |
DELETE | /api-keys/{key_id} | Revoke a key. The last active key cannot be revoked here (409); do it from the dashboard |
Rotation: create the new key, deploy it, check that last_used_at of the old key stops moving, revoke the old key.
- Errors. Unknown key 401
invalid_api_key; revoked 401api_key_revoked; expired 401api_key_expired; read-only key on a write 403api_key_read_only; key limited to other resources 403api_key_resource_not_allowed.
Legacy credentials (token + company password)
Existing integrations keep working unchanged, and both methods can be used at the same time while you migrate. Leasey will announce a retirement date with notice; new integrations should use API keys.
| Credential | What it is | Where it goes (GET) | Where it goes (POST/PUT) |
|---|---|---|---|
| API token | Partner-level bearer token | Authorization: Bearer <token> | Authorization: Bearer <token> |
| Company password | Your account password | X-Company-Password header¹ | password field in JSON body |
¹ GET /properties/{id}/marketplaces, GET /properties/leads and GET /webhooks take the password only as the password query parameter (they do not read the header). The /reports/* endpoints use the X-Company-Password header.
company_id travels as a query parameter on GET and as a JSON body field on POST/PUT.
GET /reports/syndication?company_id=128
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
+ & # %), URL-encode them when sending as a query parameter (+ must be %2B). Passwords sent in headers or JSON bodies need no encoding.3Environments & Sandbox
| Environment | Base URL | Use it for |
|---|---|---|
| Sandbox | https://sandbox.api.leasey.ai/v1 | Building and testing your integration |
| Production | https://api.leasey.ai/v1 | Live traffic, once your integration is validated |
Each environment is a separate system with its own API token, company_id, company password and keys (lsy_test_… / lsy_org_test_… only work in the sandbox, lsy_live_… / lsy_org_live_… only in production). Routes, headers and bodies are identical.
All paths in this documentation are relative to the base URL, which already includes the version: GET /reports/showings means https://api.leasey.ai/v1/reports/showings. Without /v1 the domain answers a bare 403 Forbidden.
Previous base URLs. Until November 2026 the base URLs were https://uvciw58bnh.execute-api.ca-central-1.amazonaws.com/production and https://hs52apwqwh.execute-api.ca-central-1.amazonaws.com/staging. They keep working with exactly the same routes, credentials and responses, so existing integrations need no change; new integrations should use the URLs above.
What the sandbox guarantees
- No real charges. Billing runs against Stripe in test mode. To try paid marketplaces end to end, add a Stripe test card (
4242 4242 4242 4242, any future expiry, any CVC) under Settings → Billing in the sandbox dashboard; without a card, paid activations are refused withpayment_method_requiredexactly as in production. - No publishing to live marketplaces. The feeds marketplaces read are production's, so sandbox listings never reach them. The one exception is Craigslist, which Leasey pushes to directly: don't enable it in the sandbox.
- Same code as production. Same API version.
- Your own data only. The sandbox account starts empty and is isolated from every other account.
- In the sandbox the daily billing job is off: charges stay
pendingand are never invoiced.
Health check. GET /health needs no credentials, in both environments (https://api.leasey.ai/v1/health and https://sandbox.api.leasey.ai/v1/health). It answers 200 with {"status": "ok", "checks": {"database": "ok"}, "checked_at": "2026-11-06T15:00:00Z"} when the API can serve requests, and 503 with code: service_unavailable when it cannot reach its database. The response is never cached, so you can point your own monitoring at it. Leasey checks both environments the same way every 5 minutes; the results and the incident history are public at status.leasey.ai.
Sandbox credentials come with the onboarding email, together with a dashboard user for the sandbox. Sandbox keys are created in the sandbox dashboard's Developers tab.
4Conventions, Pagination & Limits
JSON everywhere. Requests with a body send Content-Type: application/json; every response is JSON with success: true on success or an error + message pair on failure.
Dates. Send ISO-8601: YYYY-MM-DD for dates, YYYY-MM-DDTHH:MM:SS where a time matters (use 00:00:00 / 23:59:59 for start / end of day). Report rows return "YYYY-MM-DD" or "YYYY-MM-DD HH:MM:SS" strings, except Date_Available and Updated_At in /reports/properties, which are ISO 8601 with a T (see "Column reference" in section 8). The leasing routes (section G5) return ISO 8601 timestamps in UTC with a Z, and YYYY-MM-DD for lease dates.
Report date defaults. If you omit start_date on /reports/showings and /reports/leads, the report starts on January 1 of the current year (not "all history"). /reports/properties is not date-filtered. /reports/billing filters on when the charge was created; /reports/syndication on billing periods. To pull only what changed since your last sync, use updated_since (see Reports API).
Data types in responses
| Kind | How it comes back |
|---|---|
| Money / rent | A number everywhere (2249, 1749.0) |
| Counts (bedrooms, bathrooms, parking stalls) | A number |
| Booleans | true / false |
| Ids | Integers for applications, people, events, slots, leases, checks and documents; units are UUID strings |
| Empty values | null, always present — check before rendering or computing |
Older text fields and their _number twins (since 3.18). A few payloads
from before 3.14 send some values as text: application_id, person_id and
rent_amount in /properties/leads and lead.received; price, bedrooms
and bathrooms in /showings/lookup; slot_id in the reschedule response;
Parking_Stalls in /reports/showings. They stay as they are, so existing
integrations keep working, and each one now has a twin with the same value as
a number: application_id_number, person_id_number, rent_amount_number,
price_number, bedrooms_number, bathrooms_number, slot_id_number,
Parking_Stalls_Number. Use the twins; the text fields are deprecated (marked
in openapi.yaml) and stay in v1 (see "Versioning and deprecation" in section 4). A twin is null
when the stored text is not a number.
Pagination
/reports/properties, /reports/leads, /reports/showings and /reports/billing: add page and page_size (max 5000). /reports/syndication is a snapshot with one row per unit and marketplace and is not paginated. The leasing lists (section G5) are always paginated: page_size defaults to 100, with a max of 500. Without them the full report is returned, subject to a 6 MB cap — beyond it you get 413 with a suggested page size instead of a silent failure. Paginated bodies carry:
"count": 1000, "total_rows": 5005,
"pagination": { "page": 1, "page_size": 1000, "total_pages": 6, "has_more": true }
/properties/leads uses limit (max 500).
Stable sort order
Every report has a fixed order, so paging never skips or repeats a row while the data does not change:
| Report | Order |
|---|---|
/reports/showings | Date_start ascending, then Showing_ID ascending |
/reports/leads | Lead_Date_Created descending, then Application_ID descending |
/reports/properties | Active units first, then published ones, then Property_ID descending |
/reports/billing | Created_At descending, then Billing_ID descending |
/reports/syndication | Property_ID, then Marketplace |
Operational limits
| Limit | Value | Behavior at the limit |
|---|---|---|
| Request timeout | 29 s | The gateway answers 504 timeout: paginate or narrow the dates |
| Request rate | protective throttle | 429 Too Many Requests with Retry-After |
| Report response size | 6 MB | 413 with suggested_page_size — paginate or narrow dates |
page_size (reports) | max 5000 | Values above are capped to 5000 |
limit (/properties/leads) (Syndication API) | default 100, max 500 | Values above are capped to 500 |
/showings/lookup page_size (General API) | default 50, max 100 | Values above are capped to 100 |
| Availability window per call (General API) | 30 days | Use narrower date_from/date_to for larger ranges |
| Photos per unit | no hard cap | First photo is the cover; HTTPS URLs only |
| Marketplaces per publish call (Syndication API) | all 23 in one call | Each entry succeeds or fails individually |
| Webhooks per account | 1 | PUT /webhooks updates it in place |
| Webhook delivery timeout | 8 s | Non-2xx or slower → retried (5 attempts, backoff) |
| Publish idempotency (Syndication API) | state-change guard | Re-sending the same state = no_change, never re-billed |
Rate limiting
There is no usage quota. A protective throttle stops abnormal bursts, far above the volume of a normal integration. When it triggers, the API answers 429 Too Many Requests with a Retry-After header (seconds) and the standard error body. Wait the number of seconds in Retry-After and retry; if it happens again, back off exponentially (1 s, 2 s, 4 s… up to 60 s). Batching marketplace toggles into one PUT, polling leads every 5–15 minutes (or using webhooks) and paginating large reports keeps you far from the throttle. The throttle is active in both environments (50 requests/s sustained, bursts of 100).
HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-Request-Id: b72179d9-745e-4333-b541-a35b6e89e332
{ "error": "Too Many Requests", "code": "rate_limited",
"message": "Too many requests. Wait the number of seconds in the Retry-After header and retry.",
"request_id": "b72179d9-745e-4333-b541-a35b6e89e332" }
Versioning and deprecation
v1 is the major version of the API and is part of the base URL. Within v1 every change is additive — new endpoints, new optional parameters, new response fields and report columns, new values of an enumerated field, new error codes — and existing ones keep their name, type and meaning (the message text of an error may be reworded; program against code). Every change is listed with its date in the Changelog at the end of this page; the release number (currently 3.18) is the one in the header and in openapi.yaml. A change that would break existing integrations — renaming or removing a field, changing a type or the meaning of a value — ships as v2 on the same domains, announced in advance, and v1 keeps running for at least 12 months after that announcement. The legacy credentials (token + company password) keep working; Leasey will announce a retirement date with the same notice.
5Errors & Troubleshooting
Errors are always JSON, with the HTTP status that matches the problem:
{
"error": "Unauthorized",
"code": "invalid_api_key",
"message": "Invalid API key.",
"request_id": "0926a21a-0073-4d9b-8c73-a2a85b22202e"
}
| Field | What it is |
|---|---|
code | Stable, machine-readable, snake_case. Program against it: a condition keeps its code. New codes may be added |
error | The name of the HTTP status |
message | For people. It may be reworded at any time; do not parse it |
request_id | Unique per request. Every response, successful or not, also carries it in the X-Request-Id header. Quote it when you contact Leasey |
Some errors add context fields: details (422), missing_fields and missing_questions (POST /showings), is_waitlist (wrong booking mode), suggested_page_size (413), property_id (409 on a duplicate unit name).
Errors answered by the gateway itself (unknown route, throttling, timeout) use the same JSON format and carry the same code and request_id.
Error codes. The complete table is in the Error Reference.
| HTTP | error | When | What to do |
|---|---|---|---|
| 400 | Bad Request | Malformed property_id (not a UUID), missing required identifiers | Fix the request |
| 401 | Unauthorized | Bad token or company password (legacy), or a missing, unknown, revoked or expired API key (invalid_api_key, api_key_revoked, api_key_expired) | Check credentials; URL-encode special characters in query params |
| 403 | Forbidden | Read-only key on a write; unknown route (route_not_found); suspended organization | Use a full-access key / check the path and method |
| 404 | Not Found | Unit doesn't exist or belongs to another account | Verify property_id |
| 409 | Conflict | POST /properties with a name that already exists in your account — the body carries the existing property_id | Update that unit with PUT /properties/{property_id}, or use a different name (unit names are unique per account) |
| 413 | Payload Too Large | A report response exceeds the 6 MB cap | Use page/page_size or narrow start_date/end_date; the response suggests a page size |
| 422 | Unprocessable Entity | Validation failed — details array lists each field error | Fix the listed fields |
| 429 | Too Many Requests | Protective throttle | Wait Retry-After seconds and retry |
| 500 | Internal Server Error | Unexpected failure on our side | Retry once; report to Leasey with the request_id |
| 503 | Service Unavailable | Temporarily unavailable | Retry later |
| 504 | Gateway Timeout | The request took longer than 29 s | Paginate or narrow the dates |
A 422 on create/update includes details with one message per invalid field. Real example (three problems in one request — all reported at once):
{
"error": "Unprocessable Entity",
"message": "Validation failed. See 'details' for field-level errors.",
"details": [
"\"address\" is required.",
"\"unit_type\" invalid: \"castle\". Valid values: ['apartment', 'house', 'townhouse', 'duplex/triplex', 'basement suite', 'other', 'room only']",
"\"marketplaces[0].name\" = \"kijijji\" is not a valid marketplace. Valid values: ['apartmentsPremium', 'apartmentsStandard', 'craigslist', 'facebook', 'immediateRent', 'kijiji', 'lespac', 'listanza', 'livRent', 'lq', 'realtor', 'rentFaster', 'rentPanda', 'rentSeeker', 'rentalBeast', 'rentalSource', 'rew', 'viewit', 'zillow', 'zillowMulti', 'zumperPremium', 'zumperStandard']"
]
}
Nothing is created on a 422 — fix every entry in details and resend. The list of valid marketplace names in this example predates streetEasy: the API now accepts 23 names (see the Marketplace Directory in the Syndication API tab). Marketplace entries inside an otherwise-valid request fail individually (status: "error" in the response array) without aborting the rest. An error mentioning "Invalid key=value pair in Authorization header" (or 403 with code: route_not_found) means the URL/route is wrong, not the credentials.
Troubleshooting by symptom
| You see… | It means… | Fix |
|---|---|---|
401 invalid_api_key / api_key_revoked / api_key_expired | The key does not exist, was revoked, or passed its expiration date | Create a new key in Profile → Developers (or POST /api-keys with another full-access key) |
403 api_key_read_only | A read-only key was used for POST/PUT/DELETE | Use a full-access key |
403 with code: route_not_found (or an AWS message about "Invalid key=value pair in Authorization header") | The URL/route is wrong, not the credentials | Check path and method against this guide |
429 Too Many Requests | A burst hit the protective throttle | Wait the seconds in Retry-After, then back off exponentially |
401 Authentication token required / Invalid authentication token | The bearer header is missing or malformed | Header must be exactly Authorization: Bearer <token> — one space, no quotes, no trailing whitespace; make sure it is the production token |
401 'password' is required / Invalid company password | Company credentials missing or wrong for the request style | GET → X-Company-Password header (or password query param on the endpoints that accept it); POST/PUT → password inside the JSON body. Check passwordapi is set in Leasey → Settings → API Access |
401 only when the password is sent in the query string | The password contains +, &, # or % | URL-encode it (+ → %2B) or send it in the header/body |
400 'company_id' must be an integer | company_id sent as a quoted string | Send 123, not "123" (in Postman: no quotes in Params) |
422 with a details array | Validation failed on create/update | Fix every entry listed; nothing was created |
404 on PUT /properties/{id} | Wrong id, or the record belongs to another account | Use the exact id returned when it was created; you can only touch your own account's data |
413 Payload Too Large | Report bigger than 6 MB | Paginate (page, page_size) or narrow the dates — the body suggests a size |
| The response body looks like a JSON string inside JSON | Your HTTP client is parsing an already-parsed body | Parse the body exactly once |
| CORS error in a browser | You are calling the API from client-side code | Call it from your backend — credentials must never live in the browser |
500 Internal Server Error | Unexpected failure on our side | Retry once after a few seconds; if it persists, contact support with the request_id |
6Your First Request (5 minutes)
cURL — API key (one header, no company_id, no password)
curl "https://api.leasey.ai/v1/reports/showings?start_date=2026-08-01&end_date=2026-08-31" \
-H "Authorization: Bearer lsy_live_YOUR_KEY"
You should get 200 with {"success": true, "report_type": "showings", "count": …, "data": [...]}. An empty data array is still success — it means no showings in that range.
cURL — legacy credentials (second option)
curl -X GET \
"https://api.leasey.ai/v1/reports/showings?company_id=YOUR_COMPANY_ID&start_date=2026-08-01&end_date=2026-08-31" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "X-Company-Password: YOUR_COMPANY_PASSWORD"
Python — API key
import requests
BASE = "https://api.leasey.ai/v1"
HEADERS = {"Authorization": "Bearer lsy_live_YOUR_KEY"}
# Read
r = requests.get(f"{BASE}/reports/showings", headers=HEADERS,
params={"start_date": "2026-08-01"})
r.raise_for_status()
print(r.json()["count"], "showings")
# Write
r = requests.post(f"{BASE}/properties", headers=HEADERS,
json={"property": {"name": "Unit 2B", "address": "456 Oak Ave",
"city": "Toronto", "province_state": "ON",
"country": "Canada", "rent_amount": "2100"}})
print(r.status_code, r.json().get("property_id"))
Python — legacy credentials
HEADERS = {"Authorization": "Bearer YOUR_API_TOKEN",
"X-Company-Password": "YOUR_COMPANY_PASSWORD"}
# Read: credentials in headers, company_id in the query string
r = requests.get(f"{BASE}/reports/showings", headers=HEADERS,
params={"company_id": YOUR_COMPANY_ID, "start_date": "2026-08-01"})
# Write: company_id + password inside the JSON body
r = requests.post(f"{BASE}/properties",
headers={"Authorization": "Bearer YOUR_API_TOKEN"},
json={"company_id": YOUR_COMPANY_ID, "password": "YOUR_COMPANY_PASSWORD",
"property": {"name": "Unit 2B", "address": "456 Oak Ave",
"city": "Toronto", "province_state": "ON",
"country": "Canada", "rent_amount": "2100"}})
Postman in four steps
- Environments → New: add
base_url(the production URL above) andapi_key(or, for the legacy method,api_token,company_id,company_password). - New request
GET {{base_url}}/reports/showings→ Headers:Authorization=Bearer {{api_key}}→ Send. - New request
POST {{base_url}}/properties→ Headers:Authorization,Content-Type: application/json→ Body (raw JSON):{"property": {…}}→ Send. - Or skip steps 2–3: File → Import the
openapi.yamland every endpoint appears pre-built — just fill the environment.
7Retries and Idempotency-Key
Every write can be retried safely. To make a retry explicit, send an Idempotency-Key header with a value you generate for that operation (a UUID works; up to 255 characters: letters, digits, ., _, : and -). It is accepted on POST /properties, PUT /properties/{property_id}, POST /showings, PUT /showings/{slot_id}/reschedule, POST /showings/{slot_id}/cancel and POST /showings/{slot_id}/confirm (and on the same routes under /org/companies/{id}/…).
POST /properties
Authorization: Bearer lsy_live_…
Content-Type: application/json
Idempotency-Key: 6f1c2a1e-6c39-4c47-9d2e-1b8f4f0a9e21
{ "property": { "name": "Unit 2B", "address": "456 Oak Ave", "city": "Toronto",
"province_state": "ON", "country": "Canada", "rent_amount": 2100 } }
| You send | You get |
|---|---|
| A new key | The request runs normally. The response is stored for 24 hours |
| The same key and the same request again (for example after a timeout) | The stored response, without running the write again, plus the header Idempotent-Replayed: true |
| The same key with a different request | 422, code: idempotency_key_reused |
| The same key while the first request is still running | 409, code: idempotency_request_in_progress, with Retry-After |
- Keys belong to your credentials: another partner's key never collides with yours.
5xxresponses are not stored, so you can retry them with the same key.- A malformed header answers 400
invalid_idempotency_key. - Without the header the writes are still protected by their natural rules:
POST /propertiesanswers 409 with the existingproperty_idwhen the name already exists; re-sending the same marketplace state answersno_changeand is never billed twice;POST /showingsfor the same person and event updates their showing instead of creating another; reschedule, cancel and confirm always land on the same final state.
8Reports API
The five reports below are common to both APIs: same endpoints, same envelope, same pagination.
Which report for what
| You need… | Call | Notes |
|---|---|---|
| The unit portfolio (names, addresses, rent, status, days listed) | GET /reports/properties | No date filter; cache 5–15 min — it is the heaviest report |
| Prospects and applicants with rating and application state | GET /reports/leads | Defaults to Jan 1 of this year — pass start_date/end_date |
| Showing activity with team member and lead source | GET /reports/showings | Same date default; paginate on large accounts |
| What is published where and what it accumulates | GET /reports/syndication | Summary + one row per unit × marketplace |
| Every charge and its payment state | GET /reports/billing | Charge-by-charge ledger |
All five share the envelope (success, report_type, company_id, count, total_rows, data, optional pagination) and the same authentication as every other endpoint (an API key, or the legacy token + X-Company-Password). Possible values of every enumerated field are listed in the Field Values (Enums) reference — use them for filters and status badges.
Date filters
| Report | start_date / end_date filter on | Without start_date |
|---|---|---|
/reports/leads | When the lead was created (Lead_Date_Created) | January 1 of the current year |
/reports/showings | When the showing takes place (Date_start, in the company's local time) | January 1 of the current year |
Both accept a date (2026-11-03) or a date-time (2026-11-03T14:00:00). A bare date means midnight at the start of that day: end_date=2026-11-03 stops at 00:00 on November 3, so send 2026-11-04 or 2026-11-03T23:59:59 to include the whole day. A change of stage or status does not move these dates; to pick up changes, use updated_since.
Incremental sync: updated_since
Returns only the rows that changed since that moment, and every row carries Updated_At (UTC):
| Report | A row counts as changed when… |
|---|---|
/reports/leads | The lead's application (stage, archive, approval), its applicant record or its contact details change |
/reports/showings | The showing (time, status, confirmation, feedback) or its lead changes |
/reports/properties | The unit changes |
/reports/billing | A charge is created, invoiced, paid or retried |
/reports/syndication | Not supported: it is a snapshot of right now (400, updated_since_not_supported) |
GET /reports/syndication
Authorization: Bearer lsy_live_…
GET /reports/syndication?company_id=128
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
Optional start_date / end_date filter the billing periods included.
Response 200 OK — real example, truncated
{
"success": true,
"report_type": "syndication",
"company_id": 128,
"count": 32,
"summary": {
"properties_with_syndication": 8,
"active_listings": 23,
"total_billed": [ { "currency": "CAD", "amount": 142.50 } ],
"by_marketplace": [
{ "marketplace": "viewit", "active_listings": 0, "total_billed": 66.00 },
{ "marketplace": "kijiji", "active_listings": 0, "total_billed": 30.00 },
{ "marketplace": "livRent", "active_listings": 0, "total_billed": 24.90 },
{ "marketplace": "zumperStandard", "active_listings": 0, "total_billed": 12.60 }
]
},
"data": [
{
"Property_ID": "11eb3469-863d-477f-849c-19132b564e50",
"Property_Name": "Sofia Residences Unit 701",
"Marketplace": "zillow",
"Active": true,
"Published_Since": "2026-07-30 20:54:22",
"Last_Action": "published",
"Last_Action_Date": "2026-07-30 20:54:22",
"Days_Billed": 0,
"Total_Billed": 0.0,
"Pending_Amount": 0.0,
"Invoiced_Amount": 0.0,
"Paid_Amount": 0.0,
"Currency": null,
"Billed_Until": null
},
{
"Property_ID": "54881868-2c19-4f11-a30a-4d0fa4a3d6c2",
"Property_Name": "Cedarwood Unit 301",
"Marketplace": "kijiji",
"Active": false,
"Published_Since": null,
"Last_Action": "unpublished",
"Last_Action_Date": "2026-08-05 10:12:41",
"Days_Billed": 35,
"Total_Billed": 35.00,
"Pending_Amount": 0.0,
"Invoiced_Amount": 5.00,
"Paid_Amount": 30.00,
"Currency": "CAD",
"Billed_Until": "2026-08-05 00:00:00"
}
]
}
Column meanings: Active is the live state right now; Days_Billed / Total_Billed are sums over the filtered period; Pending / Invoiced / Paid split that total by billing status; Billed_Until is the end of the last billed period. Free marketplaces appear with Total_Billed: 0 and Currency: null.
Field reference
| Group | Columns |
|---|---|
| Row (one per unit × marketplace) | Property_ID, Property_Name, Marketplace, Active, Published_Since, Last_Action, Last_Action_Date, Days_Billed, Total_Billed, Pending_Amount, Invoiced_Amount, Paid_Amount, Currency, Billed_Until |
| Summary | properties_with_syndication, active_listings, total_billed, by_marketplace |
updated_since is not supported on this report (see Incremental sync).
Billing Ledger
Charge lifecycle: pending (created the moment you publish) → invoiced (our billing cron pushed it to Stripe — Stripe_Invoice_ID + Invoiced_At are set) → paid (payment confirmed — Paid_At set). A charge Stripe rejects becomes failed (it is retried; if it stays failed, contact us).
GET /reports/billing
Authorization: Bearer lsy_live_…
GET /reports/billing?company_id=47
Authorization: Bearer <token>
X-Company-Password: <password>
Optional filters: start_date / end_date (on Created_At), status, property_id, plus page/page_size. An unknown status returns 400 — status must be one of: failed, invoiced, paid, pending.
Response 200 OK — real example, trimmed to one row
{
"success": true,
"report_type": "billing",
"company_id": 47,
"count": 38,
"total_rows": 38,
"summary": {
"total_items": 38,
"by_status": [
{ "status": "invoiced", "currency": "CAD", "items": 9, "amount": 249.0 },
{ "status": "invoiced", "currency": "USD", "items": 29, "amount": 1232.1 }
],
"by_marketplace": [
{ "marketplace": "craigslist", "currency": "USD", "items": 3, "amount": 609.0 },
{ "marketplace": "viewit", "currency": "USD", "items": 4, "amount": 264.0 },
{ "marketplace": "zumperStandard", "currency": "CAD", "items": 6, "amount": 159.0 }
],
"outstanding": [],
"current_daily_run_rate": [ { "currency": "USD", "amount": 0.7 } ],
"projected_monthly": [ { "currency": "USD", "amount": 21.0 } ],
"active_paid_listings": 2
},
"data": [
{
"Billing_ID": 1261,
"Property_ID": "0097a517-7d15-4d8a-886f-aecc41ed8690",
"Property_Name": "Atlanta Unit 300",
"Marketplace": "zumperStandard",
"Billing_Type": "first_month",
"Period_Start": "2026-08-14 19:23:15",
"Period_End": "2026-09-13 19:23:16",
"Days": 30,
"Daily_Rate": 0.3,
"Amount": 9.0,
"Currency": "USD",
"Status": "invoiced",
"Stripe_Invoice_ID": "in_1U4WtrCWaiSNDissOs7DTodX",
"Created_At": "2026-08-14 19:23:16",
"Invoiced_At": "2026-08-15 02:00:45.624548",
"Paid_At": null,
"Updated_At": "2026-08-15 02:00:45.624548"
}
]
}
Updated_At is the latest of Created_At, Invoiced_At and Paid_At; it is also the field behind the updated_since filter (see Incremental sync).
Reading the summary for cost control
outstanding— pending + failed amounts not yet collected (empty = fully invoiced)current_daily_run_rate— what your currently live paid listings cost per day, today (a snapshot, independent of the date filters), withprojected_monthly= ×30. If a client asks "what am I spending?", this is the numberby_marketplace— where the money goes, largest firstBilling_Typeper row:first_month(30 days charged on activation),recurring(each subsequent 30-day block),final_proration(the last partial days when you unpublish)
Amounts are per company currency (CAD/USD by account country); accounts that switched currency show one summary line per currency.
Field reference
| Group | Columns |
|---|---|
| Row (one per charge) | Billing_ID, Property_ID, Property_Name, Marketplace, Billing_Type, Period_Start, Period_End, Days, Daily_Rate, Amount, Currency, Status, Stripe_Invoice_ID, Created_At, Invoiced_At, Paid_At, Updated_At — Billing_Type: first_month, recurring, final_proration; Status: pending, invoiced, paid, failed |
| Summary | total_items, by_status, by_marketplace, outstanding, current_daily_run_rate, projected_monthly, active_paid_listings |
Properties Report
GET /reports/properties
Authorization: Bearer lsy_live_…
GET /reports/properties?company_id=128
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
Response 200 OK — real example
{
"success": true,
"report_type": "properties",
"company_id": 128,
"count": 10,
"data": [
{
"Property_ID": "fe4280be-7443-43dc-8d8f-adeba83fe348",
"Property_URL_Slug": "qa-api-elmwood-tower-unit-101",
"Property_Name": "Elmwood Tower",
"Unit_Name": "Elmwood Tower Unit",
"Unit_Address": "789 Elmwood Tower Rd",
"Unit_Type": "Apartment",
"Unit_Status": "Active",
"Unit_Publish_Status": "Unpublished",
"Rental_Space": "Entire place",
"Lease_Term": "Month to month",
"Suite_Number": "101",
"Postal_Code": "T2N 4L4",
"Country": "Canada",
"Days_Listed": 27,
"Marketing_Start": "2026-07-30",
"Date_Available": null,
"Rent_Amount": 1900.0,
"Bedrooms": 2.0,
"Bathrooms": 1.0,
"Parking_Stalls": 0
}
]
}
reports/properties, which read as an unrelated "report/…" endpoint. It is current and supported — it is the portfolio listing.Field reference
| Group | Columns |
|---|---|
| Identity | Property_ID, Property_URL_Slug, Property_Name, Unit_Name, Unit_Address, Suite_Number, City, Province_State, Postal_Code, Country, Latitude, Longitude, Listing_URL (Leasey listing page, null when unpublished) |
| Status | Unit_Type, Unit_Status, Unit_Publish_Status, Vacancy_Status, Days_Listed, Marketing_Start, Marketing_End, Date_Available — Unit_Status: Active / Archived |
| Details | Rental_Space, Lease_Term, Rent_Amount, Parking_Stalls, Bedrooms, Bathrooms, Square_Footage, Max_Occupancy, Furnishing, Storage, Description, Images, Unit_Amenities, Unit_Amenities_Count, Building_Amenities, Building_Amenities_Count — Images is an array of URLs |
| Activity | Lead_Status, Lead_Count, Applicant_Status, Applicant_Count, Lease_Status, Lease_Count, Total_Applications, Showings_Disqualified, Showings_Scheduled, Showings_Confirmed, Showings_Cancelled, Showings_Total |
| Contact | Contact_Name, Contact_Email, Contact_Phone |
| Sync | Updated_At |
Known limitation: City and Province_State are empty on units created through the API (Unit_City / Unit_Province on the leads report are filled).
Leads Report
GET /reports/leads?start_date=2026-08-01&end_date=2026-08-31
Authorization: Bearer lsy_live_…
GET /reports/leads?company_id=128&start_date=2026-08-01&end_date=2026-08-31
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
Response 200 OK — real row, personal details anonymized
{
"Application_ID": 109099,
"Applicant_ID": 109308,
"Person_ID": 119519,
"Lead_Name": "Carlos Applicant",
"Lead_First_Name": "Carlos",
"Lead_Last_Name": "Applicant",
"Lead_Email": "carlos.applicant@example.com",
"Lead_Phone": "+15551230002",
"Application_Status": "New Lead",
"Applicant_Rating": 0,
"Tenancy_Application_Status": "Not_Completed",
"Lead_Approval_Status": "In review",
"Unit_ID": "9f3c1a52-…",
"Unit_Name": "Maple Court Unit 2",
"Unit_Address": "Edmonds Station Bus Loop",
"Full_Address": "0, Burnaby, British Columbia 10461",
"Unit_City": "Burnaby",
"Unit_Province": "British Columbia",
"Unit_Publish_Status": "Unpublished",
"Vacancy_Status": "Vacant",
"Rent_Amount": 3000.0
}
Unit_ID (since 3.10) is the unit the lead came in for, including leads that have not booked a showing yet.
Application_Status uses the stage names in the lead-stages table (see Field Values); Prequalification_Result and include_answers are described under Pre-qualification above.
Field reference
| Group | Columns |
|---|---|
| Row (one per lead / application) | Application_ID, Applicant_ID, Person_ID, Lead_Name, Lead_First_Name, Lead_Last_Name, Lead_Email, Lead_Phone, Application_Status, Applicant_Rating, Tenancy_Application_Status, Lead_Approval_Status, Unit_ID, Unit_Name, Property_Name, Unit_Address, Full_Address, Unit_City, Unit_Province, Unit_Publish_Status, Vacancy_Status, Rent_Amount, Rent_Payment_Frequency, Lease_Type, Bedrooms, Bathrooms, Furnishing, Lead_Source, Lead_Note, Lead_Date_Created, Application_Sent_Date, Application_Received_Date, Prequalification_Result, Prequalification_Answers (with include_answers=true), Updated_At (with updated_since) |
Showings Report
GET /reports/showings?page=1&page_size=500
Authorization: Bearer lsy_live_…
GET /reports/showings?company_id=128&page=1&page_size=500
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
Response 200 OK — real row
{
"Unit_ID": "5a1a83a6-2e11-4087-aedf-c33c2fabaf2d",
"Unit_Name": "168 Sanford Ave S",
"Unit_Address": "168 Sanford Ave S",
"Suite_Number": "Unit 2",
"Unit_Type": "House",
"Unit_status": "Vacant",
"Unit_publish_status": "Published",
"Rent_Amount": 1749.0,
"Bedrooms": 2.0,
"Bathrooms": 1.0,
"Square_Footage": 725,
"Date_start": "2026-08-07 18:00:00",
"Date_end": "2026-08-07 18:20:00",
"Showing_Status": "Cancelled",
"TeamMember": "Jordan Lee",
"TeamMemberRol": "Member",
"Lead_source": "Manual",
"Lead_Source": "Manual",
"Days_listed": 48
}
One row per showing
Each row is one showing. Showing_ID is unique and is the key for reconciling with your records. Event_ID / Event_Name identify the booking link the showing was booked through. Booking_Scope:
Booking_Scope | What the booking link offers | Unit shown on the row |
|---|---|---|
unit | One unit | That unit |
building | The whole building; the prospect picks a unit while booking | The unit the prospect picked |
The unit columns (Unit_ID, Suite_Number, Unit_Address, rent, bedrooms…) describe the unit the prospect actually booked, and Unit_ID matches Unit_ID on the same lead in /reports/leads. Until 3.7, rows from building-level links showed the building's first unit and lead columns could be copied from another showing at the same time slot; both fixed 2026-10-21 (re-pull if you cached earlier).
Real row from a building-level link:
{
"Showing_ID": 60875,
"Event_ID": 4530,
"Event_Name": "880 Colborne Road",
"Booking_Scope": "building",
"Unit_ID": "251399cc-72e0-4582-ae35-d7f7661846a3",
"Suite_Number": "406",
"Application_ID": 130007,
"Date_start": "2026-10-18 13:30:00"
}
Also on every showings row: Showing_Feedback (Positive, Negative, Not Attended, No Response, Not Reconfirmed), the lead columns (Person_ID, Application_ID, Lead_Name, Lead_First_Name, Lead_Last_Name, Lead_Email, Lead_Phone, Application_Status, Applicant_Rating, Tenancy_Application_Status, Lead_Approval_Status, Lead_Note) and the unit columns listed in the field reference below. /reports/showings has Lead_Source (where the lead came from, the same name as in /reports/leads) and Lead_source, a deprecated alias kept so existing integrations keep working. Use Lead_Source.
All five reports share the envelope (success, report_type, company_id, count, total_rows, data, optional pagination) and the same authentication as every other endpoint (an API key, or the legacy token + X-Company-Password).
Field reference
| Group | Columns |
|---|---|
| Unit | Property_Name, Unit_Name, Unit_Address, Unit_ID, Property_ID, Unit_Type, Unit_status, Unit_publish_status, Vacancy_Status, Rental_Space, Lease_Term, Suite_Number, Rent_Amount, Parking_Stalls, Bedrooms, Bathrooms, Square_Footage, Max_Occupancy, Furnishing, Country, Days_listed, Storage — Property_Name is the building or unit group; Property_ID is the building |
| Showing | Showing_ID, Event_ID, Event_Name, Booking_Scope, Date_start, Date_end, Showing_Status, Showing_Feedback, TeamMember, TeamMemberRol, Lead_source, Lead_Source — Booking_Scope: unit / building; Showing_Status: Scheduled, Confirmed, Cancelled, Waitlisted, Disqualified; Showing_Feedback: Positive, Negative, Not Attended, No Response, Not Reconfirmed; Lead_Source is where the lead came from; Lead_source is deprecated (use Lead_Source) |
| Lead | Person_ID, Application_ID, Lead_Name, Lead_First_Name, Lead_Last_Name, Lead_Email, Lead_Phone, Application_Status, Applicant_Rating, Tenancy_Application_Status, Lead_Approval_Status, Lead_Note, Prequalification_Result, Prequalification_Answers (with include_answers=true) — Application_Status uses the dashboard names (see Field Values) |
| Sync | Updated_At |
Column reference
Every column each report returns, with its type. The same definitions are in
openapi.yaml (PropertiesReportRow, LeadsReportRow, ShowingsReportRow),
so a generated client and this table always agree. A column marked "only
sometimes present" appears when its feature applies (for example Updated_At
once incremental sync is available, Rent_Market when the account uses market
rent, Prequalification_Answers with include_answers=true).
A few names repeat across reports with a different meaning; read each table for its own report:
Property_IDis the unit's UUID in/reports/propertiesand the building's integer id in/reports/showings. Join units onUnit_ID.Tenancy_Application_StatusisCompleted/Not_Completed(the form) in/reports/leads, and the document review status (Verified,Unverified,Incomplete,Reject,Not_Completed) in/reports/showings.Unit_Statusin/reports/propertiesisActive/Archived;Unit_statusin/reports/showingsis the unit's vacancy text.Application_Statususes the partner names in/reports/leadsand the dashboard names in/reports/showings(see "Lead stages: two sets of names" in section 11).- Dates in
/reports/leadsand/reports/showingsareYYYY-MM-DD HH:MM:SS;Date_AvailableandUpdated_Atin/reports/propertiesare ISO 8601 with aTand no offset. Showing times (Date_start,Date_end) are in the account's local time.
GET /reports/properties (54 columns, in the order they are returned)
| Column | Type | Can be null | Notes |
|---|---|---|---|
Property_ID |
string (uuid) | no | The unit (in /reports/showings Property_ID is the building's integer id). |
Property_URL_Slug |
string | yes | |
Property_Name |
string | yes | Building name; null when the unit has no building. |
Unit_Name |
string | yes | |
Unit_Address |
string | yes | |
Unit_Type |
one of Apartment, House, Townhouse, Duplex/triplex, Basement suite, Other, Unknown |
no | |
Unit_Status |
one of Active, Archived, Unknown |
no | Archived = the unit was archived in Leasey. |
Unit_Publish_Status |
one of Published, Unpublished |
no | |
Rental_Space |
one of Entire place, Partial place, Single room, Unknown |
no | |
Lease_Term |
one of Fixed term, Month to month, Fixed term, renewing month-to-month, Unknown |
no | |
Suite_Number |
string | no | Empty string when none. |
City |
string | no | Empty string when none. |
Province_State |
string | no | Empty string when none. |
Postal_Code |
string | no | Empty string when none. |
Country |
string | no | Country name (for example Canada or US); empty string when none. |
Days_Listed |
integer | no | Days from marketing start to marketing end (or today); 0 when marketing never started. |
Marketing_Start |
string (date) | yes | |
Marketing_End |
string (date) | yes | |
Date_Available |
string | yes | When the unit is available, ISO 8601 without offset (for example 2026-12-01T00:00:00). |
Rent_Market |
number | no | Only sometimes present (see notes). Market rent. Present only when the company has market rent enabled. |
Rent_Amount |
number | no | Monthly rent as a number; 0 when the stored rent is not a number. |
Parking_Stalls |
integer | no | 0 when not set. |
Bedrooms |
number | no | 0 when not set. |
Bathrooms |
number | no | 0 when not set. |
Square_Footage |
integer | no | 0 when not set. |
Max_Occupancy |
integer | no | 0 when not set. |
Furnishing |
one of Furnished, Unfurnished, Partially furnished |
no | Unfurnished when not set. |
Storage |
one of Yes, No |
no | |
Vacancy_Status |
one of Occupied, Vacant |
no | From the unit's occupied flag (Vacant when not set). |
Description |
string | yes | |
Latitude |
string | yes | As stored, as text. |
Longitude |
string | yes | As stored, as text. |
Lead_Status |
one of No Leads, 1-5 Leads, 6-10 Leads, 10+ Leads |
no | Bucket of Lead_Count. |
Lead_Count |
integer | no | Leads of the unit at the New Lead stage. |
Applicant_Status |
one of No Applicants, 1-3 Applicants, 4-10 Applicants, 10+ Applicants |
no | Bucket of Applicant_Count. |
Applicant_Count |
integer | no | Leads of the unit at the Application In Progress stage. |
Lease_Status |
one of No Active Leases, 1 Active Lease, Multiple Leases |
no | Bucket of Lease_Count. |
Lease_Count |
integer | no | Leads of the unit at the Signing Lease stage. |
Total_Applications |
integer | no | Every lead of the unit, any stage. |
Showings_Disqualified |
integer | no | Showings in state Disqualified on the booking links attached to this unit. |
Showings_Scheduled |
integer | no | Showings in state Scheduled on the booking links attached to this unit. |
Showings_Confirmed |
integer | no | Showings in state Confirmed on the booking links attached to this unit. |
Showings_Cancelled |
integer | no | Showings in state Cancelled on the booking links attached to this unit. |
Showings_Total |
integer | no | Every showing on the booking links attached to this unit, waitlist included. |
Contact_Email |
string | yes | The unit's lead-capture address, support+<company_id>+<Property_URL_Slug>@leads.leasey.ai. For a unit without a slug, the first assigned team member's email (null when none). |
Contact_Name |
string | no | First assigned team member's name; a single space when none. |
Contact_Phone |
string | no | First assigned team member's phone with dial code, or +1 <number> of the AI phone agent when it answers for the unit; empty string when none. |
Updated_At |
string | no | Only sometimes present (see notes). Last change of the unit, ISO 8601 without offset (UTC), for example 2026-11-03T14:22:05.123456. Note the T: the other reports use a space. Present where updated_since is supported. |
Listing_URL |
string (uri) | yes | The unit's public listing page; null when the unit or the company has no slug. |
Images |
array of string | no | Up to 10 photo URLs, cover first. |
Unit_Amenities |
array of string | no | |
Unit_Amenities_Count |
integer | no | |
Building_Amenities |
array of string | no | |
Building_Amenities_Count |
integer | no |
GET /reports/leads (35 columns, in the order they are returned)
| Column | Type | Can be null | Notes |
|---|---|---|---|
Application_ID |
integer | no | The lead. De-duplicate and join on it (application_id in /properties/leads, as a string). |
Applicant_ID |
integer | no | |
Person_ID |
integer | no | The person behind the lead. |
Lead_Name |
string | no | First and last name joined by a space (the space stays when the last name is missing). |
Lead_First_Name |
string | yes | |
Lead_Last_Name |
string | yes | |
Lead_Email |
string | yes | |
Lead_Phone |
string | no | Dial code followed by the number; empty string when unknown. |
Application_Status |
one of New Lead, Booked, Application In Progress, Application In Review, Signing Lease, Active Tenants, Archived Lead |
no | The lead's stage. Booked = went through the booking link (showing, waitlist or pre-qualification answers). Archived Lead wins over any other stage. |
Applicant_Rating |
integer | yes | 0–5, 0 = not rated. |
Tenancy_Application_Status |
string | no | Completed or Not_Completed. |
Lead_Approval_Status |
string | no | In review, Approved, Denied or Cancelled. |
Unit_ID |
string (uuid) | no | The unit the lead came in for, also for leads that have not booked (Property_ID in /reports/properties). |
Unit_Name |
string | yes | |
Property_Name |
string | yes | Building name; null when the unit has no building. |
Unit_Address |
string | yes | |
Full_Address |
string | no | Suite, city, province and postal code as suite, city, province postal; missing parts stay empty. |
Unit_City |
string | yes | |
Unit_Province |
string | yes | |
Unit_Publish_Status |
string | no | Published or Unpublished. |
Vacancy_Status |
string | no | Vacant or Occupied. |
Rent_Amount |
number | no | Monthly rent as a number (0 when none). A string with thousands separators (rent_amount) in /properties/leads. |
Rent_Payment_Frequency |
string | yes | |
Lease_Type |
string | yes | Fixed term, Month to month or Fixed term, renewing month-to-month. |
Bedrooms |
number | no | 0 when unknown. |
Bathrooms |
number | no | 0 when unknown. |
Furnishing |
string | yes | Furnished, Unfurnished or Partially furnished. |
Lead_Source |
string | yes | Where the lead came from, as stored (lower case), for example zillow, kijiji, zumper, facebook, craigslist, padmapper, rentals.ca, realtor.com, rew, leasey, leasey voice, listings, companywebsite, manual, ai_chatbot, ai_email. |
Lead_Note |
string | yes | |
Lead_Date_Created |
string | no | When the lead was created, YYYY-MM-DD HH:MM:SS. start_date and end_date filter on it. |
Application_Sent_Date |
string | yes | When the tenancy application was sent, YYYY-MM-DD HH:MM:SS. |
Application_Received_Date |
string | yes | When the tenancy application was received, YYYY-MM-DD HH:MM:SS. |
Updated_At |
string | no | Only sometimes present (see notes). Last change of the lead (its application, applicant record or contact details), YYYY-MM-DD HH:MM:SS UTC. Present where updated_since is supported. |
Prequalification_Result |
one of Qualified, Not Qualified |
yes | From the answers of the lead's most recent booking with answers; null when there are none. |
Prequalification_Answers |
array of object | no | Only sometimes present (see notes). Only with include_answers=true. The latest answer to each question. |
GET /reports/showings (50 columns, in the order they are returned)
| Column | Type | Can be null | Notes |
|---|---|---|---|
Property_Name |
string | yes | Building of the booked unit; null when it has none. |
Unit_Name |
string | yes | |
Unit_Address |
string | yes | |
Unit_ID |
string (uuid) | yes | The booked unit; matches Unit_ID in /reports/leads and Property_ID in /reports/properties. |
Property_ID |
integer | yes | The booked unit's building id. An integer here; in /reports/properties Property_ID is the unit's UUID. |
Unit_Type |
string | yes | Apartment, House, Townhouse, Duplex/triplex, Basement suite or Other; a raw numeric code on some legacy units. |
Unit_status |
string | yes | The unit's vacancy status as set in the dashboard (Vacant, Occupied, On Hold, On Notice, Coming Soon). Same value as Vacancy_Status. |
Unit_publish_status |
one of Published, Unpublished |
no | Unpublished also when the row has no unit. |
Vacancy_Status |
string | yes | Same value as Unit_status (in /reports/properties Vacancy_Status is only Occupied or Vacant). |
Rental_Space |
string | yes | Entire place, Partial place or Single room; a raw code on some legacy units. |
Lease_Term |
string | yes | Fixed term, Month to month or Fixed term, renewing month-to-month; a raw code on some legacy units. |
Suite_Number |
string | yes | |
Rent_Amount |
number | no | Monthly rent as a number (0 when none). |
Parking_Stalls |
string | yes | Deprecated. Parking stalls as stored, as text, kept for existing integrations. Use Parking_Stalls_Number. |
Parking_Stalls_Number |
integer | yes | Only sometimes present (see notes). Parking stalls as an integer, like Parking_Stalls in /reports/properties; null when not set. Since 3.18. |
Bedrooms |
number | yes | |
Bathrooms |
number | yes | |
Square_Footage |
integer | yes | |
Max_Occupancy |
integer | yes | |
Furnishing |
string | yes | Furnished, Unfurnished or Partially furnished; a raw code on some legacy units. |
Country |
string | yes | Country as stored on the unit. |
Days_listed |
integer | no | Days from marketing start to marketing end (or today); 0 when marketing never started. |
Storage |
string | yes | Yes or No; a raw code on some legacy units. |
Date_start |
string | no | Start of the showing, YYYY-MM-DD HH:MM:SS in the company's local time. Screened-out leads carry a placeholder (1900-01-01 or 2000-10-01). start_date and end_date filter on it. |
Date_end |
string | no | End of the showing, YYYY-MM-DD HH:MM:SS in the company's local time. |
Showing_Status |
one of Scheduled, Confirmed, Cancelled, Waitlisted, Disqualified |
yes | Disqualified = the lead went through the booking link but no visit was booked (follow it in /reports/leads with Prequalification_Result). null for an unknown state. |
TeamMember |
string | no | First and last name of the assigned team member; a single space when none is assigned. |
TeamMemberRol |
one of Admin, Member |
no | Admin when the assigned team member is an administrator, otherwise Member (also when none is assigned). |
Lead_source |
string | yes | Deprecated. Deprecated alias kept for compatibility; use Lead_Source. It carries only the applicant's origin and is null when the showing has no applicant. |
Showing_Feedback |
one of Positive, Negative, Not Attended, No Response, Not Reconfirmed |
yes | Feedback after the visit; No Response when none was given, null for an unknown code. |
Person_ID |
integer | no | The prospect. |
Application_ID |
integer | yes | The lead; matches Application_ID in /reports/leads. |
Lead_Name |
string | no | First and last name, trimmed; empty string when unknown. |
Lead_First_Name |
string | yes | |
Lead_Last_Name |
string | yes | |
Lead_Email |
string | yes | |
Lead_Phone |
string | no | Dial code followed by the number; empty string when unknown. |
Application_Status |
string | yes | The lead's stage with the dashboard's names: Lead, Booked, Applying, Checking References, Signing Lease, Active Tenant, Archived Lead (Application In Progress, Documents Requested or Application In Review when the dashboard has no name for the stage). null without a lead. These names differ from Application_Status in /reports/leads. |
Applicant_Rating |
integer | no | 0–5, 0 = not rated. |
Tenancy_Application_Status |
string | no | The tenancy application's review status as shown in the dashboard (for example Not_Completed, Unverified, Completed); Not_Completed when none. |
Lead_Approval_Status |
string | no | In review (default), Approved, Denied or Cancelled. |
Lead_Source |
string | yes | Where the lead came from: the applicant's origin, or the showing's own origin when there is no applicant (for example Manual, Leasey, Zillow, Kijiji, Facebook, Zumper). |
Lead_Note |
string | yes | |
Showing_ID |
integer | no | Unique per row; reconcile on it. |
Event_ID |
integer | no | The booking link. |
Event_Name |
string | no | |
Booking_Scope |
one of unit, building |
no | building = the booking link covers a whole building and the prospect picked the unit. |
Updated_At |
string | no | Only sometimes present (see notes). Last change of the showing or its lead, YYYY-MM-DD HH:MM:SS UTC. Present where updated_since is supported. |
Prequalification_Result |
one of Qualified, Not Qualified |
yes | From the answers given for this showing; null when there are none. |
Prequalification_Answers |
array of object | no | Only sometimes present (see notes). Only with include_answers=true. The latest answer to each question. |
9Webhooks API
Push delivery instead of polling. The mechanism — registering one endpoint, the signed delivery format and the retries — is the same for both APIs; what changes is which events each API can receive.
| Event | Available in | Payload core | Fires when |
|---|---|---|---|
unit.published | Syndication API | property_id, marketplace, active, charge | A marketplace activation completes (charge included — no separate event) |
unit.unpublished | Syndication API | property_id, marketplace, active | A deactivation completes |
lead.received | General API + Syndication API | full lead (same fields as /properties/leads) | A new lead is created for your company (delivered within ~5 min) |
application.stage_changed | General API | application_id, person_id, property_id, stage, previous_stage, archived, previous_archived, changed_at | An application changes stage (booked → application_in_progress → … → active_tenant) or is archived or unarchived. A new lead at New Lead is announced by lead.received, not by this event |
application.decision_changed | General API | application_id, person_id, decision, previous_decision, changed_at | The application is approved, denied, cancelled, or back to in_review |
screening.status_changed | General API | check_id, application_ids, type, provider, status, previous_status, report_available, completed_at | A screening check is ordered or changes status (for example in_progress → completed). Never the report itself |
lease.status_changed | General API | lease_id, property_id, status, previous_status, archived, tenant_person_ids, sent_at, signed_at | A lease is created or changes status (draft → sent_for_signatures → active) or is archived |
document.status_changed | General API | document_id, lease_id, property_id, status, previous_status, signer_count, signed_count, sent_at, completed_at | An e-signature document is created or changes status (draft → waiting_signatures → completed, or void / archived). One signer signing does not change the status; signed_count says how many have signed. Never templates |
lead.received and the five leasing events. Syndication API: unit.published, unit.unpublished and lead.received — unit.published and unit.unpublished fire when a marketplace is activated or deactivated, which is a Syndication API action.Leasing events. They use the same names and values as the Leasing API (section G5), and their full schemas are in openapi.yaml (ApplicationStageChangedEvent, ApplicationDecisionChangedEvent, ScreeningStatusChangedEvent, LeaseStatusChangedEvent, DocumentStatusChangedEvent). Leasey compares each record with the last state it announced to you, so a record that is touched without changing stage, decision or status sends nothing. They start from the moment you subscribe: the history is not replayed (read it with the Leasing API), and they arrive within about 10 minutes of the change. A previous value is null when the record is new to your webhook. Delivery is at least once: deduplicate on the record id, the new value and changed_at.
{
"id": "msg_48213",
"event": "application.stage_changed",
"created_at": "2026-11-06T15:05:02Z",
"data": {
"application_id": 130625,
"applicant_id": 130558,
"person_id": 142403,
"property_id": "e00b719c-0208-4d0d-be9e-d1cda7584a14",
"changed_at": "2026-11-06T15:00:41Z",
"stage": "signing_lease",
"stage_label": "Signing Lease",
"archived": false,
"previous_stage": "application_in_review",
"previous_archived": false
}
}
Register Endpoint
PUT /webhooks
Authorization: Bearer lsy_live_…
Content-Type: application/json
{
"url": "https://partner.example.com/leasey/webhooks",
"events": ["unit.published", "unit.unpublished", "lead.received"]
}
{
"company_id": 135,
"password": "<your API password>",
"url": "https://partner.example.com/leasey/webhooks",
"events": ["unit.published", "unit.unpublished", "lead.received"]
}
Response — the secret appears only when first created or when you send "regenerate_secret": true
{
"success": true,
"webhook": {
"url": "https://partner.example.com/leasey/webhooks",
"events": ["lead.received", "unit.published", "unit.unpublished"],
"is_active": true
},
"secret": "whsec_9f2c…",
"secret_note": "Store this secret now - it is shown only once. Use it to verify the webhook-signature header."
}
Rules: the URL must be HTTPS; events must be a non-empty subset of the table above; send "is_active": false to pause delivery without losing the config.
GET /webhooks
Authorization: Bearer lsy_live_…
Legacy credentials: GET /webhooks reads the company password only from the password query parameter (it does not read the header).
{
"success": true,
"webhook": {
"url": "https://partner.example.com/leasey/webhooks",
"events": ["lead.received", "unit.published", "unit.unpublished"],
"is_active": true,
"created_at": "2026-08-27T00:43:22.134307",
"updated_at": "2026-08-27T00:43:22.134307"
},
"recent_deliveries": []
}
Each entry in recent_deliveries looks like this (a delivery that failed once and is waiting for its retry):
{
"id": 841,
"event": "unit.published",
"status": "pending",
"attempts": 1,
"response_code": null,
"last_error": "HTTPSConnectionPool: Max retries exceeded",
"created_at": "2026-08-26T23:44:19.406130",
"delivered_at": null
}
status is pending (queued or between retries), delivered (your server answered 2xx — delivered_at set) or failed (all 5 attempts exhausted). If no webhook is registered yet, the response is 200 with "webhook": null and a hint to register one.
Events are delivered by a job that runs every 5 minutes, so expect up to ~5 minutes between the action and the delivery.
Delivery format and signature
Events are HTTPS POSTs (JSON), signed per the Standard Webhooks spec:
POST <your url>
webhook-id: msg_841
webhook-timestamp: 1756224000
webhook-signature: v1,K5oZfzN95Z9UVpg…
{
"id": "msg_841",
"event": "unit.published",
"created_at": "2026-08-26T15:04:05Z",
"data": {
"property_id": "e6bc6604-1e7f-451f-8014-3560620a0489",
"marketplace": "kijiji",
"active": true,
"charge": { "type": "first_month", "amount": 22.20, "currency": "USD", "period_days": 30 }
}
}
A lead.received delivery carries the same fields as /properties/leads, so one parser serves both the pull and the push path. An unit.unpublished delivery has the same data shape as unit.published minus the charge (active: false).
A lead.received delivery looks like this (data has the same fields as GET /properties/leads):
{
"id": "msg_843",
"event": "lead.received",
"created_at": "2026-08-26T21:15:02Z",
"data": {
"application_id": "117278",
"application_id_number": 117278,
"person_id": "128338",
"person_id_number": 128338,
"property_id": "80240cf8-0666-484a-af80-02d6a2133522",
"source": "manual",
"first_name": "Maria",
"last_name": "Prospect",
"email": "maria.prospect@example.com",
"phone": "+15551230001",
"status": "Booked",
"date_created": "2026-08-26T21:13:39.434000",
"property_name": "40 Ashley St",
"property_address": "40 Ashley St",
"city": "Hamilton",
"province": "Ontario",
"rent_amount": "2,249",
"rent_amount_number": 2249
}
}
data.status uses the same stage names as Application_Status in /reports/leads: New Lead, Booked, Application In Progress, Application In Review, Signing Lease, Active Tenants (see the lead-stages table in Field Values).
Verify the signature
import base64, hashlib, hmac
def verify(secret, headers, raw_body):
key = secret.split("_", 1)[-1].encode()
signed = "{}.{}.{}".format(headers["webhook-id"],
headers["webhook-timestamp"], raw_body)
expected = base64.b64encode(
hmac.new(key, signed.encode(), hashlib.sha256).digest()).decode()
return hmac.compare_digest(expected, headers["webhook-signature"].split(",", 1)[1])
Retries and reliability
- Respond 2xx within 8 s to acknowledge. Anything else is retried.
- Backoff: +2 min, +10 min, +30 min, +2 h — 5 attempts total, then the delivery is marked
failed(visible inGET /webhooks). - Deliveries may arrive out of order and, rarely, more than once — de-dup by the
idfield (for leads,data.application_idalso works). lead.receivedstarts with the first delivery run after you register; leads created before that run are not sent. History is not replayed: useGET /reports/leads(orGET /properties/leadson the Syndication API) for backfill.
Testing your webhook
- Before your receiver exists, point the webhook at a request inspector (webhook.site) to see real deliveries with headers.
- Use a tunnel (ngrok) to test your own receiver locally.
GET /webhooksshows each delivery's status and the response code your URL returned.
lead.received carries the lead's name, email and phone; never register a public inspector URL on a production account.10Property Object Reference
Fields accepted in the property object (create and update). Your side maps its own model into these; Leasey maps them onward into each marketplace feed.
Required on create
| Field | Type | Notes |
|---|---|---|
name | string | Unit display name |
address | string | Street address |
city | string | City name (resolved against Leasey's city catalog) |
province_state | string | e.g. "BC", "Ontario", "FL" |
country | string | "Canada" or "United States" |
POST /properties with a name that already exists in your account answers 409 Conflict with the existing property_id (unit names are unique per account).
Optional — identity, location & building
suite_unit, postal_code. For multi-unit properties: building_name + number_of_units (groups the unit into a building, created if it doesn't exist — response echoes building_id); building_id (attach to an existing building instead of matching by name); building_amenities — array of {id, value} (1 included · 2 available · 3 not available), requires building_id/building_name/number_of_units in the same request; floorplan_id or floorplan (string name or object {name, bedrooms, bathrooms, size_sqft}).
Building amenity id catalog
| ID | Building Amenity |
|---|---|
1 | Bike storage |
2 | Concierge |
3 | Electrical car charging station |
4 | Fitness center / gym |
5 | Hot tub |
6 | Sauna |
7 | Steam room |
8 | Swimming pool |
9 | Common areas |
10 | Community laundry |
11 | Elevator |
12 | On-site manager |
Buildings created via building_name inherit the unit's location (address, city, province, country, postal code, coordinates); a building that already exists with blank location fields is completed from the next unit (existing values never overwritten).
Optional — commercial
rent_amount (string or number), security_deposit, rent_payment_frequency (daily|weekly|bi-weekly|monthly|yearly|one-time), renewal_frequency (fixed term|month to month|fixed term, renewing month-to-month), date_available (YYYY-MM-DD), rental_application_url + use_custom_application_link: true — your own application form URL; leads on marketplaces are sent to it instead of Leasey's application flow.
unit_type: apartment, rental_space: entire place, rent_payment_frequency: monthly, renewal_frequency: month to month, furnishing: 2 (unfurnished), storage_available: 2 (no), country: Canada, max_occupancy: bedrooms × 2.Optional — physical
unit_type (apartment|house|townhouse|duplex/triplex|basement suite|room only|other), rental_space (entire place|partial place), bedrooms, bathrooms, size_sqft, furnishing (1 furnished · 2 unfurnished · 3 partially), parking_stalls, storage_available, max_occupancy.
Optional — content
description (plain text; shown on marketplaces), photos (array of {url} — first photo is the cover; HTTPS required), features (array of {id, value} — full catalog below), restrictions (array of ids: 1 no dogs · 2 no smoking · 3 no short-term · 4 no subletting · 5 no cats).
Optional — people
owner_contact — object with first_name, last_name, email, phone, dial_code, notify_email (bool), notify_sms (bool); it is the notification recipient for every notification about the unit (publish/unpublish, showing cancellations on deactivation, lead notices) when notify_email is true; the enable/disable payloads carry no email field.
team_members — assign existing staff to the unit or create new ones in the same call. Each entry is either an existing member ({"team_member_id": 45}) or a new one by email:
"team_members": [
{ "team_member_id": 45 },
{
"email": "agent@acmepm.com",
"first_name": "Ana",
"last_name": "Ruiz",
"role_id": 3,
"is_admin": false,
"timezone": "America/Vancouver"
}
]
A member created through the API is Admin by default. To set another role use, in order of precedence: role_id, role by name ("Admin", "Member", "Showing Coordinator" or any custom role of your account), or is_admin: false. timezone is optional (your account's default is used when omitted). The response echoes role_id, role and is_admin. An unknown role fails that entry (in skipped) without aborting the rest. Changing the role of an existing member is not done here. The response echoes what happened to every entry:
"team_members": {
"assigned": [45, 61],
"created": [
{
"team_member_id": 61,
"user_id": 5120,
"person_id": 131044,
"email": "agent@acmepm.com",
"already_existed": false
}
],
"skipped": []
}
assigned lists every member now attached to the unit (existing and newly created); created details the accounts that were created; a failed entry lands in skipped as an explanatory string without aborting the unit or the other entries. An email that already exists under your account is re-used (assigned, not duplicated); an email belonging to another company is skipped.owner_user_id — assign the unit to a specific user of your account (validated against your company_id); defaults to your account's admin. marketplaces — publishing to marketplaces, its pricing and billing are documented in the Syndication API tab.
Marketplace-specific formatting (photo sizes, description length limits, feed-specific enums) is handled by Leasey — you send the canonical fields above once.
Feature (unit amenity & utility) catalog
Each features entry has an id and a value: 1 = Included in rent · 2 = Available, not included · 3 = Not available. Only include features you want to set — unspecified features default to not available.
Unit features (amenities and utilities) 37 rows
| ID | Feature | Type |
|---|---|---|
1 | Air conditioning | Amenity |
2 | Balcony / patio | Amenity |
3 | Basement | Amenity |
4 | Bike storage | Amenity |
5 | Concierge | Amenity |
6 | Dishwasher | Amenity |
7 | Electrical car charging station | Amenity |
8 | Fireplace | Amenity |
9 | Fitness center / gym | Amenity |
10 | Furnished | Amenity |
11 | Heating | Amenity |
12 | Hot tub | Amenity |
13 | In-suite washer | Amenity |
14 | Parking | Amenity |
15 | Sauna | Amenity |
16 | Steam room | Amenity |
17 | Storage locker | Amenity |
18 | Swimming pool | Amenity |
19 | Cable | Utility |
20 | Electricity | Utility |
21 | Internet | Utility |
22 | Natural gas | Utility |
23 | Water | Utility |
24 | Common areas | Amenity |
25 | Community laundry | Amenity |
26 | Elevator | Amenity |
27 | On-site manager | Amenity |
28 | In-suite dryer | Amenity |
29 | Food waste disposal | Amenity |
30 | Freezer | Amenity |
31 | Microwave | Amenity |
32 | Oven | Amenity |
33 | Refrigerator | Amenity |
34 | Stove | Amenity |
35 | Garbage disposal | Utility |
36 | Satellite TV | Utility |
37 | Sewage | Utility |
Example: {"id": 6, "value": 1} = dishwasher included in the rent; {"id": 20, "value": 2} = electricity available but paid separately.
11Field Values (Enums)
Use these for filters, badges and validation. They come from the live data, not from a spec — if a new value appears it will be additive.
/reports/showings
| Field | Values |
|---|---|
Unit_Type | Apartment, House, Basement suite, Duplex/triplex, Townhouse, Other (a raw numeric code may appear on legacy units) |
Unit_status | Vacant, Occupied, On Hold, On Notice, Coming Soon |
Unit_publish_status | Published, Unpublished |
Showing_Status | Scheduled, Confirmed, Cancelled, Waitlisted, Disqualified (state 0, see the General API tab) |
Showing_Feedback | Positive, Negative, Not Attended, No Response, Not Reconfirmed |
Prequalification_Result | Qualified, Not Qualified, null |
Application_Status | The lead's stage, with the dashboard's names (see the lead-stages table below) |
TeamMemberRol | Member, Admin (plus any custom role names your account defines) |
Lead_source | Manual, Leasey, Kijiji, Rentsync, Zillow, Facebook, Apartments, Zumper, CompanyWebsite, PadMapper, NotIdentified, null |
/reports/leads
| Field | Values |
|---|---|
Application_Status | New Lead, Booked, Application In Progress, Application In Review, Signing Lease, Active Tenants, Archived Lead (a raw numeric code may appear for statuses without a label) |
Applicant_Rating | 0–5 (0 = not rated) or null |
Tenancy_Application_Status | Not_Completed, Completed |
Prequalification_Result | Qualified, Not Qualified, null |
Lead_Approval_Status | In review, Approved, Denied, Cancelled |
Unit_Publish_Status | Published, Unpublished |
Vacancy_Status | Vacant, Occupied |
/reports/properties
| Field | Values |
|---|---|
Unit_Type | Apartment, House, Basement suite, Townhouse, Duplex/triplex, Other, Unknown |
Unit_Status | Active, Archived |
Unit_Publish_Status | Published, Unpublished |
Rental_Space | Entire place, Partial place, Single room |
Lease_Term | Fixed term, renewing month-to-month, Fixed term, Month to month, Unknown |
Country | Canada, US |
Lead stages: two sets of names
| Stage | /reports/leads Application_Status, /properties/leads and lead.received status | /reports/showings Application_Status | /applications stage (section G5) |
|---|---|---|---|
| New lead, nothing else yet | New Lead | Lead | new_lead |
| Went through your booking link: booked a showing, joined the waitlist or answered the pre-qualification questions | Booked | Booked | booked |
| Application started | Application In Progress | Applying | application_in_progress |
| Application in review | Application In Review | Checking References | application_in_review |
| Signing the lease | Signing Lease | Signing Lease | signing_lease |
| Tenant | Active Tenants | Active Tenant | active_tenant |
| Archived, from any stage | Archived Lead | Archived Lead | archived: true, with the stage the application had |
In the Leasing API the stage is a code and stage_label carries the /reports/leads name.
Until 3.10 the Booked stage was called Application Received in /properties/leads and in the lead.received webhook, and /reports/leads returned the raw value 3. Integrations that mapped the old name should now map Booked.
12Error Reference
| Status | When it occurs |
|---|---|
| 200 | Request processed successfully |
| 201 | Property created successfully |
| 400 | Missing required parameter, malformed JSON, invalid type |
| 401 | Missing or invalid API key, or missing or invalid API token / company password |
| 402 | No card on file, or the charge for a screening check could not be created (payment_method_required, payment_failed); nothing was ordered |
| 403 | Read-only key on a write (api_key_read_only), key limited to other resources (api_key_resource_not_allowed), unknown route (route_not_found), or suspended organization |
| 404 | Property, application, lease, document or template not found, or it does not belong to your company |
| 409 | Duplicate unit name (the body carries the existing property_id), idempotency request in progress, or last active key |
| 413 | Report response exceeds the 6 MB cap |
| 422 | Valid JSON but field-level validation failed (e.g. invalid enum value) |
| 429 | Protective throttle: wait Retry-After seconds |
| 500 | Database error or unexpected exception |
| 503 | Temporarily unavailable |
| 504 | The request took longer than 29 s |
Every error carries code and request_id; see Errors & Troubleshooting for the format and the complete list of codes below.
Error codes
code | HTTP | When |
|---|---|---|
unauthorized | 401 | Token or company password missing or wrong |
invalid_api_key | 401 | The API key does not exist |
api_key_revoked | 401 | The API key was revoked |
api_key_expired | 401 | The API key passed its expiration date |
api_key_read_only | 403 | A read-only key was used for a write |
api_key_resource_not_allowed | 403 | The key is limited to other resources (section 2), or a limited key tried to create a key beyond its own resources |
forbidden | 403 | The credential cannot do this (for example a suspended organization) |
route_not_found | 403 | The method and path combination does not exist |
bad_request | 400 | Malformed request (for example a property_id that is not a UUID) |
missing_fields | 400 | POST /showings without a required field (listed in missing_fields) |
missing_questions | 400 | POST /showings without an answer to a required question |
waitlist_mode_required | 400 | The event only takes waitlist requests: send date_options |
time_slot_unavailable | 400 | Someone took that time a moment ago |
invalid_updated_since | 400 | updated_since is not an ISO-8601 date |
updated_since_not_supported | 400 | updated_since on /reports/syndication |
invalid_pagination | 400 | page below 1, or page_size outside 1–500, on the leasing routes (section G5) |
invalid_parameter | 400 | A filter with a value that does not exist (the message lists the valid ones) |
invalid_idempotency_key | 400 | The Idempotency-Key header has invalid characters |
slot_not_found, invalid_slot_id, missing_slot_id, slot_cancelled, slot_unavailable | 400 / 404 | The showing id is wrong, cancelled or no longer available |
not_found | 404 | The record does not exist or belongs to another account |
application_not_found, lease_not_found, document_not_found | 404 | That record does not exist in your account |
method_not_allowed | 405 | The path exists but not with that method |
conflict | 409 | POST /properties with a name that already exists; last active key; organization slug or admin email taken |
idempotency_request_in_progress | 409 | The first request with that Idempotency-Key is still running |
payload_too_large | 413 | The report is over 6 MB: paginate (suggested_page_size) |
validation_failed | 422 | Field errors, listed in details |
invalid_expires_at | 422 | POST /api-keys with an expires_at that is not a future ISO 8601 date or date-time |
invalid_resources | 422 | POST /api-keys with resources that is not a non-empty list of known resources |
idempotency_key_required | 400 | Ordering a screening check without Idempotency-Key |
payment_method_required, payment_failed | 402 | No card on file, or the charge could not be created; nothing was ordered |
property_not_found, template_not_found | 404 | The unit or the e-signature template does not exist in your account |
application_not_sendable | 409 | The application is archived or past application_in_progress |
check_not_available, order_in_progress | 409 | That check has no price set for your account, or another screening order of your account is being charged right now |
lease_not_editable, lease_not_sendable, document_already_created | 409 | The lease was sent for signatures (only archived changes), was already sent, or already has a draft document (document_id) |
document_not_sendable, document_not_voidable | 409 | Only a draft can be sent, and only a document waiting for signatures can be voided |
applicant_email_required, applicant_phone_required | 422 | Sending the application needs the applicant's email and phone |
esign_unavailable | 503 | E-signature through the API is not enabled in that environment (the sandbox) |
idempotency_key_reused | 422 | That Idempotency-Key was used for a different request |
rate_limited | 429 | Protective throttle: wait Retry-After seconds |
internal_error | 500 | Unexpected failure on our side: retry once, then contact us with the request_id |
upstream_error | 502 | A dependency (for example Stripe) did not answer; retry |
service_unavailable | 503 | Temporarily unavailable: retry later |
updated_since_unavailable | 503 | Incremental sync storage missing on that environment. Not expected in production or the sandbox any more; listed for completeness |
timeout | 504 | The request took longer than 29 s: paginate or narrow the dates |
Per-marketplace entries inside a 200 response carry their own status: "error" with code: payment_method_required or message: "Unknown marketplace."; they are not HTTP errors.
From here on the documentation is specific to one API. Pick the one you integrate with — the sidebar and the content below switch together.
G1Properties API — import and update units
Data model
Unit / Listing / Property are the same thing (a rental unit). The API uses property.
property_id returned on creation — it is the permanent key for everything else about that unit.lead.received webhook (Part 1).POST /properties
Authorization: Bearer lsy_live_…
Content-Type: application/json
{
"property": {
"name": "Maplewood Residences Unit 201",
"address": "2180 Whitestone Drive",
"suite_unit": "201",
"city": "Vancouver",
"province_state": "BC",
"postal_code": "V6B 0B9",
"country": "Canada",
"description": "Bright 2-bedroom with balcony and in-suite laundry.",
"bedrooms": 2,
"bathrooms": 1.5,
"size_sqft": 950,
"rent_amount": "1800",
"date_available": "2026-10-01",
"owner_contact": {
"first_name": "Alex",
"last_name": "Manager",
"email": "manager@company.com",
"notify_email": true
},
"photos": [
{ "url": "https://cdn.example.com/unit201/cover.jpg" }
]
}
}
{
"company_id": 128,
"password": "<YOUR_COMPANY_PASSWORD>",
"property": {
"name": "Maplewood Residences Unit 201",
"address": "2180 Whitestone Drive",
"suite_unit": "201",
"city": "Vancouver",
"province_state": "BC",
"postal_code": "V6B 0B9",
"country": "Canada",
"description": "Bright 2-bedroom with balcony and in-suite laundry.",
"bedrooms": 2,
"bathrooms": 1.5,
"size_sqft": 950,
"rent_amount": "1800",
"date_available": "2026-10-01",
"owner_contact": {
"first_name": "Alex",
"last_name": "Manager",
"email": "manager@company.com",
"notify_email": true
},
"photos": [
{ "url": "https://cdn.example.com/unit201/cover.jpg" }
]
}
}
Response 201 Created — real example
{
"success": true,
"message": "Property created successfully.",
"property_id": "b94c71ea-412d-4969-99be-fa9db983f090",
"url_slug": "maplewood-residences-unit-201-201"
}
property_id. It is required for every subsequent call about this unit.POST /propertieswith anamethat already exists in your account answers 409 Conflict with the existingproperty_id: update that unit withPUT /properties/{property_id}, or use a different name.- Buildings created via
building_nameinherit the unit's location (address, city, province, country, postal code, coordinates). - Team members created through the API are Admin by default; send
roleby name,role_idoris_admin: falsefor anything else (see the Property Object Reference).
PUT /properties/{property_id}
Authorization: Bearer lsy_live_…
Content-Type: application/json
{
"property": { "rent_amount": "1950", "date_available": "2026-11-01" }
}
{
"company_id": 128,
"password": "<YOUR_COMPANY_PASSWORD>",
"property": { "rent_amount": "1950", "date_available": "2026-11-01" }
}
Update semantics per field
| Field | Behavior on update |
|---|---|
photos | Replaces the full photo set |
features, restrictions | Replace the full list ([] clears) |
| everything else | Partial update |
Errors: 400 malformed property_id (must be a UUID), 404 unit not found.
G2Showings API
Look up bookable units, read real availability, book a showing (or join the waitlist), find a person's existing showings, and reschedule, confirm or cancel them. These endpoints reuse the same logic the booking widget uses — team-member assignment, conflict checks, notifications and lead-journey logging.
How booking works
Two vocabulary items carry the whole API:
- Showing event (
event_id) — the per-unit (or per-building) booking configuration: availability windows, team members, pre-qualification questions, waitlist mode. You find it with/showings/lookup. - Slot (
slot_id) — one booked (or waitlisted) visit by one person. It is what you confirm, cancel or reschedule; you find it by the person's email or phone.
Slot states: 0 qualified lead · 1 pending (booked, valid, notified) · 2 confirmed (the person said they will attend) · 3 cancelled · 4 waitlisted.
State 0 is a lead who went through the booking link without booking a visit time (typically screened out by the pre-qualification questions). GET /showings and GET /showings/{slot_id} label it status: "qualified_lead"; /reports/showings labels the same state Showing_Status: "Disqualified". They are the same thing.
POST /showings. It lands in pending, already valid and notified. /confirm is an optional, separate action for when the person explicitly says they will attend.Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
company_id | integer | Yes | Your company identifier |
query | string | Optional | Free-text, partial, case-insensitive match across seven fields at once (property name, address, URL slug, suite/unit number, building name, building address, event name) |
unit_id | UUID | Optional | Exact match |
page / page_size | integer | Optional | Default 50, max 100 |
With no query and no unit_id the endpoint returns every bookable unit of the account, paginated — load the inventory upfront instead of guessing search terms.
{
"success": true,
"count": 1,
"total": 1,
"page": 1,
"page_size": 50,
"has_more": false,
"events": [
{
"event_id": 4067,
"event_name": "Maple Court Unit 2, Edmonds Station Bus Loop, Burnaby, British Columbia, Canada, 10461, Showing",
"bookshowingby": 0,
"unit_id": "86bdb8c6-82d0-4981-b593-c998879bd85f",
"unit_name": "Maple Court Unit 2",
"suite_unit": "0",
"unit_address": "Edmonds Station Bus Loop",
"unit_slug": "maple-court-unit-2",
"bedrooms": "3.0",
"bathrooms": "3.0",
"price": "3,000",
"bedrooms_number": 3,
"bathrooms_number": 3,
"price_number": 3000,
"city": "Burnaby",
"province": "British Columbia",
"building_id": 0,
"building_name": null,
"building_address": null,
"is_waitlist": false
}
]
}
bookshowingby: 0 = the event books a specific unit; 1 = a building-level event (one calendar for the whole building — pass unit_id when booking if the person wants a specific unit). is_waitlist: true = the unit takes preferred dates instead of a booked time — it has no bookable times to read from availability.
{
"event_id": 1601,
"event_name": " Showing",
"bookshowingby": 0,
"unit_id": "a62fdbb3-43b9-4a4c-a9ff-399e6422eb46",
"unit_name": "101 GrandView",
"unit_address": "2180 Whitestone Dr",
"unit_slug": "101-grandview",
"bedrooms": "2.0",
"bathrooms": "5.0",
"price": "12,003",
"bedrooms_number": 2,
"bathrooms_number": 5,
"price_number": 12003,
"city": "Riverside",
"province": "California",
"building_id": null,
"is_waitlist": true
}
Pre-Qual. Questions
GET /showings/questions?event_id=1599
Authorization: Bearer lsy_live_…
GET /showings/questions?company_id=47&event_id=1599
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
{
"success": true,
"event_id": 1599,
"event_name": "101 Ventue way, Showing",
"count": 1,
"questions": [
{
"question_id": 19525,
"question": "Do you own any pets?",
"type": "Dropdown",
"options": ["Yes", "No"],
"required": true,
"order": 0
}
]
}
type is Dropdown (answer must be one of options) or Text (free text, options empty). An event without questions returns count: 0 and an empty questions array. The answers are stored with the booking. /reports/leads and /reports/showings return the Qualified / Not Qualified result on every row (Prequalification_Result) and, with include_answers=true, the answers themselves (see Reports API).
Availability
- How far ahead the calendar goes depends on the event: a rolling event (the most common) offers today + N days (N is the event's rolling-days setting, typically 7 or 14); a fixed date-range event offers only that range; an open-ended event is capped at 30 days per call.
date_from/date_tonarrow the window, they never extend it. - Only published events are bookable. An unpublished event returns 404 ("Event not found, not published, or not yours."), never a 200 with an empty list.
404here means "this unit isn't open for booking", not "no times available"./showings/lookuplists published events only.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
company_id | integer | Yes | Your company identifier |
event_id | integer | Yes | From Find Bookable Units |
date_from / date_to | date | Optional | Narrow the window; at most 30 days are scanned per call |
GET /showings/availability?event_id=1409&date_from=2026-09-01&date_to=2026-09-14
Authorization: Bearer lsy_live_…
GET /showings/availability?company_id=47&event_id=1409&date_from=2026-09-01&date_to=2026-09-14
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
{
"success": true,
"availability": {
"event_id": 1409,
"time_zone": "Pacific Time (PT)",
"step_minutes": 10,
"is_waitlist": false,
"dates": [
{
"date": "2026-09-02",
"weekday": 4,
"slots": [
{ "time": "03:15:00", "formatted_time": "3:15 AM", "team_member_id": 1038 },
{ "time": "03:25:00", "formatted_time": "3:25 AM", "team_member_id": 1038 },
{ "time": "03:35:00", "formatted_time": "3:35 AM", "team_member_id": 1038 }
]
}
]
}
}
An event with no availability configured returns the same envelope with dates: [] — hand the caller to a human or a callback. is_waitlist: true with no dates means the event only accepts waitlist requests, so there is nothing to read here.
Book a Showing
Body Fields
| Field | Required | Notes |
|---|---|---|
event_id | Yes | From /showings/lookup |
firstname, lastname | Yes | The person visiting |
email | Yes | The lead's identity and the key to find the showing later (GET /showings?email=) |
phone | Yes | Without it the API answers 400 with missing_fields: ["phone"]. It also works for lookups |
dial_code | No | Defaults to +1 |
date, time | Yes (normal events) | YYYY-MM-DD and 24h HH:MM, from /showings/availability |
unit_id | Building-level events only | When the person chose a specific unit of a building (bookshowingby: 1) |
answers | If the event has required questions | {question_id, value}; Dropdown values must be one of options |
POST /showings
Authorization: Bearer lsy_live_…
Content-Type: application/json
{
"event_id": 4067,
"firstname": "Jane",
"lastname": "Prospect",
"email": "jane.prospect@example.com",
"phone": "6045550101",
"dial_code": "+1",
"date": "2026-08-31",
"time": "14:00",
"answers": [ { "question_id": 19525, "value": "No" } ]
}
{
"company_id": 128,
"password": "<YOUR_COMPANY_PASSWORD>",
"event_id": 4067,
"firstname": "Jane",
"lastname": "Prospect",
"email": "jane.prospect@example.com",
"phone": "6045550101",
"dial_code": "+1",
"date": "2026-08-31",
"time": "14:00",
"answers": [ { "question_id": 19525, "value": "No" } ]
}
The team member is assigned automatically; never send one. Required questions are enforced: a missing answer fails with 400 and code: missing_questions (each listed with question, type, options).
Response 201 Created
{
"success": true,
"slot_id": 54949,
"applicant_id": 117651,
"application_id": 117517,
"person_id": 128580,
"team_member_id": 1657,
"is_disqualified": false,
"is_update": false,
"lead_status": "showing",
"message": "Showing created successfully"
}
- Save
slot_id: it is what you confirm, cancel or reschedule. application_idis the same lead id you see in/properties/leadsand/reports/leads: a showing booked here is a lead in Leasey.is_update: truemeans this person already had a slot for this event and it was moved instead of duplicated.is_disqualified: truemeans an answer failed the event's screening rules; the slot still exists but the team is flagged.- The booking is complete and notified at this point (state
pending)./confirmis optional.
Failure cases: 400 missing_fields, 400 missing_questions, 400 time_slot_unavailable (someone took the time first: re-read availability), 400 waitlist_mode_required (the event only takes waitlist requests; the body carries is_waitlist: true).
Join Waitlist
Same endpoint and auth; swap date + time for date_options:
POST /showings
Authorization: Bearer lsy_live_…
Content-Type: application/json
{
"event_id": 1601,
"firstname": "Jane", "lastname": "Prospect", "email": "jane.prospect@example.com", "phone": "6045550101",
"date_options": [ "2026-09-18", "2026-09-20T14:00", { "date": "2026-09-22", "time": "09:30" } ]
}
{
"company_id": 128, "password": "<YOUR_COMPANY_PASSWORD>", "event_id": 1601,
"firstname": "Jane", "lastname": "Prospect", "email": "jane.prospect@example.com", "phone": "6045550101",
"date_options": [ "2026-09-18", "2026-09-20T14:00", { "date": "2026-09-22", "time": "09:30" } ]
}
Each entry may be a plain date, a date with a preferred time (YYYY-MM-DDTHH:MM), or an object {date, time}. One to three entries; past dates are rejected with 400. The response has the same shape as POST /showings with lead_status: "waitlisted" (state 4), no team member and no time held. date/time on a waitlist event, or date_options on a normal event, both return 400 with an explanatory message and the event's is_waitlist flag.
List Showings
GET /showings?email=jane.prospect%40example.com
Authorization: Bearer lsy_live_…
GET /showings?company_id=128&email=jane.prospect%40example.com
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
Optional include_waitlist=false omits waitlist entries.
{
"success": true,
"count": 1,
"showings": [
{
"slot_id": 54949,
"event_id": 4067,
"state": 1,
"status": "pending",
"date": "2026-08-31",
"time": "14:00:00",
"time_zone": "Pacific Time (PT)",
"virtual": 0,
"display_name": "Maple Court Unit 2",
"location": "Edmonds Station Bus Loop, Burnaby, British Columbia, Canada"
}
]
}
A waitlist entry in this list has status: "waitlisted", date/time null and a preferred_dates array instead. virtual: 0 in person · 1 virtual · 2 either.
Showing Status
GET /showings/54949
Authorization: Bearer lsy_live_…
GET /showings/54949?company_id=128
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
{
"success": true,
"slot_id": 54949,
"state": 1,
"status": "pending",
"date": "2026-08-31",
"time": "14:00:00",
"time_zone": "Pacific Time (PT)",
"event_id": 4067,
"person": {
"id": 128580,
"firstname": "Jane",
"lastname": "Prospect",
"email": "jane.prospect@example.com",
"phone": "6045550101",
"dial_code": "+1",
"language": "en"
},
"event": {
"event_name": "Maple Court Unit 2, Edmonds Station Bus Loop, Burnaby, British Columbia, Canada, 10461, Showing",
"property_name": "Maple Court Unit 2",
"location": "Edmonds Station Bus Loop, Burnaby, British Columbia, Canada - 10461"
}
}
Reschedule
PUT /showings/{slot_id}/reschedule
Authorization: Bearer lsy_live_…
Content-Type: application/json
{ "date": "2026-09-01", "time": "15:30" }
{ "company_id": 128, "password": "<YOUR_COMPANY_PASSWORD>", "date": "2026-09-01", "time": "15:30" }
Response 200 OK
{
"success": true,
"slot_id": "54949",
"slot_id_number": 54949,
"date_start": "2026-09-01 15:30:00",
"date_end": "2026-09-01 15:40:00",
"team_member_id": 0,
"message": "Slot rescheduled successfully"
}
date_end = start + the event's step_minutes. team_member_id is 0 when no team member has availability at the new time — the showing still exists and the team is notified; pick a time from Availability to get an assigned member.
slot_id (Showing_ID in /reports/showings); only the date, time and team member change. The same applies when the lead reschedules from their confirmation link or your team moves it in Leasey. A new id is created only when the lead books again after the previous showing was cancelled or its date has passed; the old one keeps its status (for example Cancelled).Cancel
POST /showings/{slot_id}/cancel
Authorization: Bearer lsy_live_…
{ "company_id": 128, "password": "<YOUR_COMPANY_PASSWORD>" }
Response 200 OK
{ "success": true, "slot_id": 54949, "state": 3, "message": "Slot cancelled successfully", "language": "en" }
A cancelled slot no longer appears in GET /showings but GET /showings/{slot_id} still returns it with status: "cancelled".
Confirm
Same body as cancel. Call it only when the person explicitly says they will attend.
Response 200 OK
{ "success": true, "slot_id": 54949, "state": 2, "message": "Slot confirmed successfully", "language": "en" }
language is the person's language (en/fr); notifications go out in it.
G3Showings — Idempotency, Errors & Dialogue Rules
Idempotency & retries
- Reads (
GET) are always safe to retry. POST /showingsis keyed to the person + event: re-sending the same contact for the same event updates their existing slot instead of creating a duplicate (is_update: true).- Reschedule, cancel and confirm are state transitions: repeating them lands on the same final state.
- For an explicit retry on any write, see Retries and Idempotency-Key in Part 1.
- If a write times out with no response, read the true state with
GET /showings/{slot_id}(orGET /showingsby contact) before retrying.
Errors specific to showings
| Status | When | What to do |
|---|---|---|
| 400 | Missing fields (missing_fields); missing required questions (missing_questions, each listed with question, type, options); booking conflict (time_slot_unavailable); wrong booking mode (waitlist_mode_required: date/time on a waitlist event or date_options on a normal one — the body carries is_waitlist) | Ask for the missing info, or re-read availability and offer another time |
| 401 | Bad key, token or company credentials | Configuration error on your side — do not retry |
| 404 | Event or showing not found, not published, or not owned by your account | Re-run lookup / list; tell the person you couldn't find it |
| 500 | Unexpected server error | Retry once; then fall back to a human / callback |
Dialogue rules for conversational agents
Lookups and lists return zero, one or many results — branch on it:
lookup→ 0: ask for the address or unit name · 1: confirm it ("I found X — is that right?") · many: read back the top few and let the caller pick.GET /showings→ 0: "I don't see any upcoming showings under that number/email" · 1: confirm it, then act · many: read them back with date + property and let the caller choose.- Map spoken answers to one of a question's
optionsbefore sending.
G4Tutorial — find a showing, then confirm, reschedule or cancel it
The caller never knows their slot_id — always find it first by contact.
Then, on the slot the caller clearly chose (confirm it verbally first):
| Caller says | Call | Result |
|---|---|---|
| "Yes, I'll be there" | POST /showings/54949/confirm (Confirm) | state: 2 confirmed, team notified |
| "Can we do Tuesday at 3:30 instead?" | GET /showings/availability for the new day, then PUT /showings/54949/reschedule with date/time (Reschedule) | back to pending, team re-assigned and notified |
| "Cancel it" | POST /showings/54949/cancel (Cancel) | state: 3 cancelled, team notified; works on waitlist entries too |
| "Is my viewing still on?" | GET /showings/54949 (Showing status) | current status, date/time, unit and person |
All four are safe to repeat — they land on the same final state. If a call timed out with no answer, read the status before retrying.
G5Leasing API — applications, screening, leases and e-signature
What happens after the lead: the application, the screening checks ordered
for the applicant, the lease and its signatures. The reads (the read routes below) work
with any key, a read-only one included; the writes (the change routes below) need a
full-access key. A key limited to resources needs applications,
screening, leases or documents ("Limited to some resources" in section 2). Both also work with the legacy
credentials (company_id in the query string plus the X-Company-Password
header, like the reports).
All four lists share the same rules:
- Always paginated:
page(default 1) andpage_size(default 100, max 500). The body carriescount,total_rows,dataandpagination(page,page_size,total_pages,has_more), the same envelope as the reports. Outside those limits:400 invalid_pagination. - Order: newest first (by id, descending). With
updated_since, the sync order instead: oldest change first (updated_atascending, then id). - Types: ids are integers (units are UUID strings), money is a number,
booleans are
true/false, timestamps are ISO 8601 in UTC withZ, and lease dates areYYYY-MM-DD. Every field is always present; an unknown value isnull. - Filters take the codes below exactly; any other value answers
400 invalid_parameterwith the list of valid ones. - Responses carry
Cache-Control: private, no-store: they contain applicants' personal data.
What you can read, and what stays in Leasey
| You get | You never get through the API |
|---|---|
| Application stage, decision, form and document review status, dates | The application form answers and uploaded documents |
| Each screening check: type, provider, status, cost, dates, whether a report exists | The report itself (scores, records, PDFs): it stays in the Leasey dashboard |
| Lease terms, rent, deposits, fees, tenants, sent and signed dates | The lease document and its files |
| Each e-signature document's status and who signed when | The document content |
The writes of the change routes below make the same changes your team makes in the dashboard: create and send the application, move its stage, approve or deny it, order a check, and create, send and void the lease's documents.
Applications
One application per lead and unit. GET /applications/{application_id}
returns one, with its screening checks and the ids of its leases.
| Filter | Values |
|---|---|
stage |
new_lead, booked, application_in_progress, application_in_review, signing_lease, active_tenant |
decision |
in_review, approved, denied, cancelled |
archived |
true (only archived), false (only active); omit for both |
property_id |
A unit UUID |
updated_since |
ISO 8601 (see "Incremental sync" in section 8) |
GET /applications/130625
Authorization: Bearer lsy_live_…
{
"success": true,
"data": {
"application_id": 130625,
"applicant_id": 130558,
"person_id": 142403,
"property_id": "e00b719c-0208-4d0d-be9e-d1cda7584a14",
"property_name": "Unit 4B",
"building_id": null,
"stage": "active_tenant",
"stage_label": "Active Tenants",
"archived": false,
"decision": "approved",
"form_completed": true,
"document_review": "unverified",
"source": "Facebook",
"applicant": {
"first_name": "Jordan",
"last_name": "Lee",
"email": "jordan.lee@example.com",
"phone": "+15551230002"
},
"lead_created_at": "2026-10-20T00:04:19Z",
"application_sent_at": "2026-10-20T00:40:52Z",
"application_received_at": "2026-10-20T01:06:01Z",
"updated_at": "2026-10-20T01:06:01Z",
"screening": [
{
"check_id": 2236,
"applicant_id": 130558,
"person_id": 142403,
"application_ids": [130625],
"type": "credit_criminal_eviction",
"name": "TenantAlert Credit + Criminal & Eviction Check",
"provider": "tenantalert",
"status": "completed",
"report_available": true,
"cost": 0.0,
"currency": "USD",
"requested_at": "2026-10-20T17:32:40Z",
"completed_at": "2026-10-20T17:41:40Z",
"updated_at": "2026-10-20T17:41:40Z"
}
],
"lease_ids": [219]
}
}
Real response from production, personal details anonymized.
stageuses the same stages asApplication_Statusin/reports/leads(stage_labelcarries that report's name). Archiving is separate:archived: truewith the stage the application had, where the report saysArchived Lead.decisionisin_reviewuntil someone approves, denies or cancels the application in Leasey (Lead_Approval_Statusin the report).person_idis the lead: the same id asPerson_IDin the reports andperson_idin a lease's tenants.updated_atis the last change of the application, its applicant or the lead, counting when the application was sent and received.
Screening Checks
Every check of your account. GET /applications/{application_id}/screening
returns the checks of one application's applicant.
| Filter | Values |
|---|---|
status |
waiting_payment, in_progress, completed, payment_failed, failed |
updated_since |
ISO 8601 (/screening only) |
| Field | Values |
|---|---|
type |
identity_verification, social_media_audit, credit_check, softcheck, tenant_report, credit_criminal_eviction |
provider |
certn, singlekey, verifast, tenantalert, zumper |
name |
The product name, for example Equifax Canadian Credit Check |
report_available |
true when the provider returned a report; open it in the dashboard |
- A check belongs to the applicant, not to one application: an applicant
with two applications in your account shows the same checks in both, and
application_idslists them all. A check ordered for an applicant with no application in your account hasapplication_ids: []. failedmeans the provider could not complete the check;payment_failed, that its payment did not go through.
Leases
Every lease, with its tenants and its e-signature documents.
GET /leases/{lease_id} returns one.
| Filter | Values |
|---|---|
status |
not_created, draft, sent_for_signatures, active |
archived |
true / false |
property_id |
A unit UUID |
updated_since |
ISO 8601 |
{
"lease_id": 219,
"name": "Lease – Unit 4B",
"property_id": "e00b719c-0208-4d0d-be9e-d1cda7584a14",
"property_name": "Unit 4B",
"status": "active",
"status_label": "Active",
"archived": false,
"start_date": "2026-11-01",
"end_date": "2027-11-30",
"renewal": "fixed_term_renewing_monthly",
"rent_frequency": "annually",
"other_term": null,
"rent_amount": 3150.0,
"security_deposit": 1500.0,
"pet_deposit": 0.0,
"fees": { "parking": 0.0, "storage": 0.0, "pet": 0.0 },
"utilities": "Water and heat included",
"incentive": null,
"sent_at": "2026-10-20T01:42:55Z",
"signed_at": "2026-10-20T01:45:15Z",
"updated_at": "2026-10-20T01:45:15Z",
"tenants": [
{ "person_id": 142403, "application_id": 130625, "first_name": "Jordan", "last_name": "Lee", "email": "jordan.lee@example.com" },
{ "person_id": 142415, "application_id": 130633, "first_name": "Sam", "last_name": "Rivera", "email": "sam.rivera@example.com" }
],
"documents": [
{
"document_id": 1261,
"name": "Residential Tenancy Agreement",
"status": "completed",
"lease_id": 219,
"property_id": "e00b719c-0208-4d0d-be9e-d1cda7584a14",
"created_at": "2026-10-20T01:31:02Z",
"scheduled_for": null,
"sent_at": "2026-10-20T01:42:55Z",
"completed_at": "2026-10-20T01:45:15Z",
"updated_at": "2026-10-20T01:45:15Z",
"signers": [
{ "signer_id": 1579, "person_id": 142403, "first_name": "Jordan", "last_name": "Lee", "email": "jordan.lee@example.com", "order": 1, "signed": true, "signed_at": "2026-10-20T01:45:13Z" },
{ "signer_id": 1580, "person_id": 142415, "first_name": "Sam", "last_name": "Rivera", "email": "sam.rivera@example.com", "order": 1, "signed": true, "signed_at": "2026-10-20T01:45:13Z" }
]
}
]
}
One item of data, real response from production, personal details
anonymized.
| Field | Values |
|---|---|
renewal |
fixed_term, month_to_month, fixed_term_renewing_monthly, annually |
rent_frequency |
weekly, bi_weekly, monthly, annually, one_time |
status |
not_created (terms saved, no document yet), draft, sent_for_signatures, active (every signer signed) |
- A tenant's
application_idis that tenant's application for the lease's unit, when there is one: that is how a lease connects to the applications above (andlease_idsthere points back). - Money is a number; a value that was never entered is
null, an explicit zero is0.
E-signature Documents
Documents sent (or to be sent) for signature through Leasey, never templates.
GET /documents/{document_id} returns one. A lease's documents also come
inside the lease (see Leases above). document.status_changed (section 9 Webhooks API) announces each
change of status.
| Filter | Values |
|---|---|
status |
draft, scheduled, waiting_signatures, completed, void, archived |
lease_id |
Only the documents of that lease |
property_id |
A unit UUID |
updated_since |
ISO 8601 |
Each signer carries signed and signed_at; order is the signing order
when the document is signed in sequence. The document's completed_at is when
the last signer signed.
Staying in sync
- First load: page through each list without filters (
page_size=500). - Then, every few minutes: call the same lists with
updated_sinceset to the highestupdated_atyou stored, minus a minute of overlap, and upsert by id. The order is oldest change first, so you can stop at any page and resume. - Or subscribe to
application.stage_changed,application.decision_changed,screening.status_changed,lease.status_changedanddocument.status_changed(section 9 Webhooks API) and fetch the record by id when an event arrives. Keep a dailyupdated_sincepass as a safety net.
updated_since on /screening and /leases answers 503
updated_since_unavailable in an environment where it is not enabled yet; the
lists themselves work everywhere.
Change Applications
The same actions your team takes in the dashboard, with a full-access key
(a read-only key answers 403 api_key_read_only; a key limited to resources
needs applications; see "Limited to some resources" in section 2). Every write accepts Idempotency-Key (section 7 Retries and Idempotency-Key) and
answers with the whole application, as GET /applications/{application_id}.
| Route | What it does |
|---|---|
POST /applications |
Creates the lead for a unit at new_lead: the person, the applicant and the application. A lead.received webhook follows |
PATCH /applications/{application_id} |
Changes stage, decision (approved, denied, cancelled, in_review), archived, and the applicant's first_name, last_name, email, phone, dial_code and language. Send only what changes |
POST /applications/{application_id}/send |
Sends the application to the applicant by email and text message, like the dashboard's Send Application button, and moves it to application_in_progress |
POST /applications
Authorization: Bearer lsy_live_…
Idempotency-Key: 6f1c2a1e-6c39-4c47-9d2e-1b8f4f0a9e21
Content-Type: application/json
{
"property_id": "e00b719c-0208-4d0d-be9e-d1cda7584a14",
"first_name": "Jordan",
"last_name": "Lee",
"email": "jordan.lee@example.com",
"phone": "(555) 123-0002",
"language": "en"
}
201 with the application (stage: "new_lead", source: "API"). Required:
property_id (a unit of your account), first_name, email and phone.
phone is the national number (dial_code defaults to +1) or an E.164
+1 number. The same email for the same unit answers 409 conflict with the
existing application_id: there is one application per person and unit.
PATCH /applications/130625
Authorization: Bearer lsy_live_…
Content-Type: application/json
{ "stage": "signing_lease", "decision": "approved" }
- The values are the codes of Applications; any other answers
422 validation_failedwith one entry per field indetails({"field": "decision", "message": …}). - No email goes to the applicant when the stage or decision changes (the
dashboard sends none either).
application.stage_changedandapplication.decision_changed(section 9 Webhooks API) tell your other systems. /sendworks fromnew_lead,bookedorapplication_in_progressand not archived; from a later stage it would move the application back, so it answers409 application_not_sendable. The applicant needs an email and a phone (422 applicant_email_required,applicant_phone_required).
Order a Screening Check
Orders a check for the application's applicant and charges your card on file, exactly as the dashboard does:
- The price is your account's price for that check, never a value you send.
A price of
0orders without a charge; a check with no price set answers409 check_not_available. - The charge is a Stripe invoice to the card on file (
402 payment_method_requiredwithout a card,402 payment_failedif the charge cannot be created). Nothing is ordered without the charge. - The check starts at
waiting_payment, moves toin_progressand thencompleted(screening.status_changed, section 9 Webhooks API). The report stays in the Leasey dashboard;report_availablesays when it is there.
Idempotency-Key is required here (400 idempotency_key_required
without it): a retry with the same key returns the same order and never
charges twice. Orders of one account run one at a time: a second order sent
while another is being charged answers 409 order_in_progress; retry it a
few seconds later. If the check cannot be ordered after the charge was
created, the charge is voided.
type |
Check | Needs |
|---|---|---|
credit_check |
Certn Equifax Canadian Credit Check | date_of_birth, and an address in Canada (street, city, province_state, postal_code, country) |
softcheck |
Certn Softcheck | date_of_birth |
date_of_birth can be left out when Leasey already has it. The applicant must
be 18 to 100. The other providers (SingleKey, TransUnion, TenantAlert) are
ordered from the dashboard.
POST /applications/130625/screening
Authorization: Bearer lsy_live_…
Idempotency-Key: 3c9b0d5e-0f3a-4a52-8b7f-6a2c1d9e4b10
Content-Type: application/json
{
"type": "credit_check",
"date_of_birth": "1990-04-12",
"address": {
"street": "20 King St W",
"city": "Toronto",
"province_state": "ON",
"postal_code": "M5H 1A1",
"country": "CA"
}
}
201 with the check, as in Screening Checks (status: "waiting_payment", cost,
currency). Everything is validated before the charge: an address outside
Canada for credit_check answers 422 and nothing is charged. In the sandbox
the charge uses Stripe test mode and the providers' test environments.
Leases and E-signature
| Route | What it does |
|---|---|
POST /leases |
Creates a lease at not_created with its terms and tenants |
PATCH /leases/{lease_id} |
Changes terms, tenants (the list is replaced) or archived |
GET /document-templates |
Your e-signature templates |
POST /leases/{lease_id}/send |
Creates the document from a template, with every tenant as a signer, and sends it for signature |
POST /documents/{document_id}/send |
Sends a draft document for signature |
POST /documents/{document_id}/void |
Voids a document that is waiting for signatures |
POST /leases
Authorization: Bearer lsy_live_…
Content-Type: application/json
{
"property_id": "e00b719c-0208-4d0d-be9e-d1cda7584a14",
"tenants": [142403],
"start_date": "2026-12-01",
"end_date": "2027-11-30",
"renewal": "fixed_term",
"rent_frequency": "monthly",
"rent_amount": 2300,
"security_deposit": 2300,
"fees": { "parking": 75 }
}
201with the lease, as in Leases. Required:property_id,tenants(person_idof people with an application in your account),start_dateandrent_amount. Money is a number, dates areYYYY-MM-DD,renewalandrent_frequencytake the codes of Leases.- Terms and tenants change while the lease is
not_createdordraft. Once it was sent for signatures onlyarchivedchanges (409 lease_not_editable).
POST /leases/219/send
Authorization: Bearer lsy_live_…
Content-Type: application/json
{ "template_id": 31 }
201with the document (E-signature Documents) atwaiting_signatures; the lease is nowsent_for_signatures. Each tenant receives the signing email in their language. The template must already have its signature fields (templates are prepared in the Leasey dashboard).- When every signer signs, the document is
completedand the leaseactive:document.status_changedandlease.status_changed(section 9 Webhooks API). /voidcancels the signing and marks the documentvoid; the lease goes back todraft, so it can be sent again with a corrected template or terms.- If the signing service fails after the document was created, the answer is
502 upstream_errorwith thatdocument_id: send it withPOST /documents/{document_id}/send. While the lease has a draft document,POST /leases/{lease_id}/sendanswers409 document_already_createdwith itsdocument_id, so a retry never creates a second document.
Leasing Error Codes
Besides the codes of section 12:
code |
HTTP | When |
|---|---|---|
validation_failed |
422 | A field is missing or invalid; details lists each one as {field, message} |
conflict |
409 | That person already has an application for the unit (application_id is the existing one), or the new email is taken in that unit |
property_not_found |
404 | The unit does not exist in your account |
template_not_found |
404 | The template does not exist in your account |
application_not_sendable |
409 | The application is archived or past application_in_progress |
applicant_email_required, applicant_phone_required |
422 | The invitation goes by email and text message |
idempotency_key_required |
400 | Ordering a screening check without Idempotency-Key |
order_in_progress |
409 | Another screening order of your account is being charged right now; retry in a few seconds |
document_already_created |
409 | The lease already has a draft document (document_id): send that one |
check_not_available |
409 | That check has no price set for your account |
payment_method_required, payment_failed |
402 | No card on file, or the charge could not be created; nothing was ordered |
lease_not_editable |
409 | The lease was sent for signatures: only archived changes |
lease_not_sendable |
409 | The lease was already sent for signatures |
document_not_sendable, document_not_voidable |
409 | Only a draft can be sent, and only a document waiting for signatures can be voided |
upstream_error |
502 | The email, payment, screening or e-signature service did not answer; retry |
esign_unavailable |
503 | E-signature through the API is not enabled in that environment yet |
G6FAQ
Can I book, confirm or cancel a showing with this API?
Yes. Book with POST /showings (or join the waitlist with date_options), then confirm, cancel or reschedule by slot_id. See the Showings API above.
Can I approve an application, order a screening check or send a lease with this API?
Yes, with a full-access key. PATCH /applications/{application_id} approves, denies or cancels the application and moves its stage; POST /applications/{application_id}/screening orders a Certn credit check or Softcheck, charged to your card on file at your account's price; POST /leases/{lease_id}/send sends the lease for e-signature with one of your templates. The five leasing webhooks (section 9) tell your system when anything changes. Screening reports and signed files stay in the Leasey dashboard.
Should I use an API key or the token + password?
An API key. It is one secret, it is tied to one company, your administrators create and revoke it themselves in Profile → Developers, it can be read-only and it can expire. The token + password keep working while you migrate.
Should I use webhooks or poll for leads?
Both work and can run together. lead.received webhooks deliver within ~5 minutes with retries; polling GET /reports/leads is simpler and doubles as backfill. Start with polling, add the webhook when you want lower latency.
How do I test without touching real client data?
Use the sandbox account from onboarding: it is isolated from every real account.
A report returned 413 — is something broken?
No: the response is bigger than 6 MB. Add page/page_size (the 413 body suggests a size) or narrow start_date/end_date.
Can I publish a unit to marketplaces with the General API?
No — publishing and unpublishing units to marketplaces, and the marketplace billing, are part of the Syndication API tab. Lead webhooks (lead.received) are available here (see Webhooks in Part 1).
G7Glossary
| Term | Meaning |
|---|---|
| Unit / Listing / Property | The rental unit tied to a showing event (one property_id) |
Showing event (event_id) | The per-unit (or per-building) booking configuration: availability windows, team members, questions, waitlist mode |
Slot (slot_id) | One booked (or waitlisted) visit by one person |
| Waitlist | Event mode with no bookable times — people leave preferred dates instead (shown as is_waitlist: true) |
| Team member | The staff member auto-assigned to run a showing or created via team_members |
G8Integration Checklist
Use this to track your integration end to end:
- API key created in Profile → Developers (read-only for dashboards), stored server-side; legacy token/password only if you already had them
X-Request-Idlogged on every response, so support can find any request- Writes send an
Idempotency-Keyand handleIdempotent-Replayed - Reports synced with
updated_since, de-duplicated byApplication_ID/Showing_ID/Property_ID/Billing_ID GET /showings/lookup— catalog loaded or search wired; 0/1/many results handledGET /showings/questions— required questions asked before bookingGET /showings/availability— real open times read back beforePOSTPOST /showings— schedule;is_updateandtime_slot_unavailablehandled- Waitlist units (
is_waitlist: true) booked withdate_options GET /showings?email|phone→ caller picks → confirm / cancel / reschedule byslot_id- Error handling: 401 (credentials), 404 (wrong unit), 422 (
detailsarray) - Passwords with special characters URL-encoded in query strings
POST /properties— create a test unit, storeproperty_idPUT /properties/{id}— update; field replace-vs-partial semantics understoodGET /reports/*wired into your dashboard, with pagination for large accountsPUT /webhooksregistered forlead.received; signature verification implemented; secret stored on first response- Webhook receiver answers 2xx within 8 s; delivery states monitored via
GET /webhooks - Load test with your expected call/showing volume
G9Best Practices
🔒 Security
- Call the API only from your backend — never expose the token or password in browser/mobile code
- Use an API key; if you still use the legacy pair, prefer the
X-Company-Passwordheader over the query string - Rotate keys periodically; give each system its own key so a leak is contained
- Keep the token in environment variables, never in source control
- Verify webhook signatures and reject anything that fails
⚡ Performance
- Load the showings catalog once instead of searching per call
- Pass explicit
start_date/end_dateon showings and leads reports - Design for a 60-second client timeout
🔄 Reliability
- Retry
5xxwith exponential backoff (2 s, 4 s, 8 s); never retry4xx - Read state before retrying a write that timed out — writes are idempotent, but confirm rather than assume
- Persist
slot_idandapplication_idas soon as you book
📋 Conversational Agents
- Never act on a slot the caller did not clearly choose
- Always confirm the specific appointment verbally before a cancel or reschedule
- Map spoken answers to one of a question's
optionsbefore sending
📦 Data Consistency
photos,features,restrictionsare replaced on update — send the full current setteam_memberson update is additive; remove from the Leasey dashboard
S1Tutorials
Each tutorial is a complete path with the real calls in order. The full request/response of every endpoint is in the reference sections below; here you get the sequence and what to carry from one step to the next.
S1.1 Publish a unit and see exactly what it costs
Goal: a unit exists in Leasey, your user sees the price, publishes to two marketplaces, and you show them the confirmed charge.
- Create the unit —
POST /propertieswith at leastname,address,city,province_state,country. Store the returnedproperty_id— it is the permanent key for everything else. - Show the marketplaces with prices —
GET /properties/{property_id}/marketplaces. Render the 23 entries withpricing_model,daily_rateandfirst_month_price— that is the exact cost to display in your confirmation dialog. Free marketplaces show0. - Your user toggles and confirms in your portal. Leasey knows nothing until step 4.
- Publish in one call —
PUT /properties/{property_id}with themarketplacesarray. Each entry comes back withstatus(published/unpublished/no_change/error) and, for paid activations, achargeobject:{type: "first_month", amount, currency, period_days: 30}. Show that amount to your user — it is what will be billed. - What just happened inside Leasey — on enable: the listing is flagged for that marketplace's feed, a publication log entry is written, and if the marketplace is paid on your account a billing item for the first 30 days is created (
pendinguntil the next billing run). On disable: the flag is turned off, anunpublishedlog entry is written, no new billing is created and the current cycle closes with the final days prorated. Re-sending the same state is a no-op (no_change) — never a duplicate charge. - See it in the admin view —
GET /reports/syndication: one row per unit × marketplace withActive,Published_Sinceand amounts.
zillowStandard (accepted and normalized to zillow, but use canonical names in new code); publishing before the unit has photos (allowed, but marketplaces convert better with a cover photo); expecting a second first_month charge when re-enabling — the guard prevents it only while the state is unchanged; disable then enable again does start a new first month.The money flow on a paid marketplace, end to end
company_id + password); your end clients do not create Leasey accounts, you create units on their behalf and keep the mapping client → unit on your side. You own the UI and the confirmation step — Leasey knows nothing about toggles until you call PUT. Prices are visible before publishing and charges are confirmed in the publish response — no surprises.S1.2 Get the leads back into your platform
Goal: every lead a marketplace generates for a unit you syndicated shows up in your CRM, attributed to the right client and the right marketplace.
Pull (simplest — start here)
- Every 5–15 minutes call
GET /properties/leads?start_date=<last_sync>with your last successful sync timestamp. - For each lead, route it with
property_id→ your unit → your client. - De-duplicate with
application_id(stable per lead). The same id coming back with a newstatusis a status update, not a new lead. - Attribute with
source(kijiji,zumper,apartments…) — this is also how you show your clients which marketplace their spend converts on. - Store the new
last_sync= the newestdate_createdyou processed.
Push (lower latency, optional)
PUT /webhookswith your HTTPS URL andevents: ["lead.received"]— store thesecretfrom the response; it is shown once.- Verify the
webhook-signatureheader on every delivery and answer2xxwithin 8 s. - The payload's
datahas the same fields as the pull endpoint, so one parser serves both. History is not replayed — use pull for backfill.
Both can run together: pull as the safety net, push for speed.
S1.3 Reconcile billing against your own invoices
Goal: finance can tie every Leasey charge to a Stripe invoice and know the current spend.
GET /reports/billing, optionally withstart_date/end_dateon when charges were created, orstatus=invoiced.- Each row is one charge:
Billing_Type(first_month·recurring·final_proration), the coveredPeriod_Start/Period_End,Days,Daily_Rate,Amount,Currency,Status, andStripe_Invoice_IDwithInvoiced_At/Paid_At. - Match
Stripe_Invoice_IDto your Stripe invoices; anythingpendingis not yet invoiced, anythingfailedwas rejected by Stripe and is being retried. - Use the
summaryfor control:outstanding(not yet collected),current_daily_run_rate(what your live paid listings cost per day, today) andprojected_monthly(×30),by_marketplace(where the money goes),active_paid_listings. - For the per-unit view — what is live and since when — use
GET /reports/syndication.
S2Properties API (Syndication)
Data model
Unit / Listing / Property are the same thing (a rental unit). The API uses property. A unit's marketplace activations are independent toggles: activating Kijiji does not affect Zillow. All data is strictly scoped to your company_id: you can never read or modify another account's units, and vice versa.
Pricing and billing model
Each marketplace is either free or paid. Paid marketplaces have a daily rate configured per partner account (your negotiated pricing), in CAD or USD depending on your account's country.
| Rule | Detail |
|---|---|
| First month | Billed in full on activation: daily_rate × 30, regardless of how many days the listing stays live that month. |
| From day 31 | The daily rate is billed for each day the listing remains live, invoiced in 30-day cycles. |
| On deactivation | Days beyond the last billed period are prorated at the daily rate (you never pay a full month for a partial period after the first). |
| Free marketplaces | Never generate any charge. |
| Re-sending the same state | Idempotent: activating an already-active marketplace (or deactivating an inactive one) is a no-op — no duplicate charges, no duplicate logs. |
Where prices appear in the API: before publishing — GET /properties/{id}/marketplaces returns daily_rate and first_month_price per marketplace; when publishing — the PUT response includes a charge object per activated marketplace; afterwards — GET /reports/syndication aggregates everything billed.
The syndication flow at a glance
POST /properties→ create the unit in Leasey, storeproperty_idGET /properties/{id}/marketplaces→ show your user the marketplaces WITH prices- (your user toggles marketplaces and confirms in YOUR portal)
PUT /properties/{id}withmarketplaces→ Leasey publishes and returns the charges appliedGET /properties/leads→ pull leads generated by the marketplacesGET /reports/syndication→ admin view: everything live + everything billedGET /reports/billing→ charge-by-charge ledger for reconciliationPUT /webhooks(optional) → push: receive leads & publish events instead of polling
company_id + password). Your end clients do not create Leasey accounts; you create units on their behalf and keep the mapping client → unit on your side. Prices are visible before publishing and charges are confirmed in the publish response — no surprises. Store the property_id returned on creation — it is the permanent key for everything else.Create Unit
POST /properties
Authorization: Bearer lsy_live_…
Content-Type: application/json
{
"property": {
"name": "Maplewood Residences Unit 201",
"address": "2180 Whitestone Drive",
"suite_unit": "201",
"city": "Vancouver",
"province_state": "BC",
"postal_code": "V6B 0B9",
"country": "Canada",
"description": "Bright 2-bedroom with balcony and in-suite laundry.",
"bedrooms": 2,
"bathrooms": 1.5,
"size_sqft": 950,
"rent_amount": "1800",
"date_available": "2026-10-01",
"owner_contact": {
"first_name": "Alex",
"last_name": "Manager",
"email": "manager@acmepm.com",
"notify_email": true
},
"photos": [
{ "url": "https://cdn.example.com/unit201/cover.jpg" },
{ "url": "https://cdn.example.com/unit201/kitchen.jpg" }
],
"marketplaces": [
{ "name": "kijiji", "enabled": true },
{ "name": "zillow", "enabled": true }
]
}
}
{
"company_id": 128,
"password": "<YOUR_COMPANY_PASSWORD>",
"property": {
"name": "Maplewood Residences Unit 201",
"address": "2180 Whitestone Drive",
"suite_unit": "201",
"city": "Vancouver",
"province_state": "BC",
"postal_code": "V6B 0B9",
"country": "Canada",
"description": "Bright 2-bedroom with balcony and in-suite laundry.",
"bedrooms": 2,
"bathrooms": 1.5,
"size_sqft": 950,
"rent_amount": "1800",
"date_available": "2026-10-01",
"owner_contact": {
"first_name": "Alex",
"last_name": "Manager",
"email": "manager@acmepm.com",
"notify_email": true
},
"photos": [
{ "url": "https://cdn.example.com/unit201/cover.jpg" },
{ "url": "https://cdn.example.com/unit201/kitchen.jpg" }
],
"marketplaces": [
{ "name": "kijiji", "enabled": true },
{ "name": "zillow", "enabled": true }
]
}
}
Response 201 Created — real example
{
"success": true,
"message": "Property created successfully.",
"property_id": "b94c71ea-412d-4969-99be-fa9db983f090",
"url_slug": "maplewood-residences-unit-201-201",
"marketplaces": [
{
"name": "kijiji",
"status": "published",
"active": true,
"daily_rate": 0.74,
"currency": "USD",
"charge": {
"type": "first_month",
"amount": 22.20,
"currency": "USD",
"period_days": 30,
"note": "First month billed in full on activation. From day 31 the daily rate applies for each day the listing stays live."
}
},
{
"name": "zillow",
"status": "published",
"active": true,
"daily_rate": 0,
"currency": "CAD",
"charge": null
}
]
}
property_id. It is required for every subsequent call about this unit. charge: null means the marketplace is free — nothing was billed.POST /propertieswith anamethat already exists in your account answers 409 Conflict with the existingproperty_id: update that unit withPUT /properties/{property_id}, or use a different name.- Buildings created via
building_nameinherit the unit's location (address, city, province, country, postal code, coordinates). - Team members created through the API are Admin by default; send
roleby name,role_idoris_admin: falsefor anything else (see the Property Object Reference).
Update Unit
PUT /properties/{property_id}
Authorization: Bearer lsy_live_…
Content-Type: application/json
{
"property": {
"rent_amount": "1950",
"date_available": "2026-11-01"
}
}
{
"company_id": 128,
"password": "<YOUR_COMPANY_PASSWORD>",
"property": {
"rent_amount": "1950",
"date_available": "2026-11-01"
}
}
Response 200 OK
{
"success": true,
"message": "Property updated successfully.",
"property_id": "b94c71ea-412d-4969-99be-fa9db983f090"
}
Update semantics per field
| Field | Behavior on update |
|---|---|
photos | Replaces the full photo set |
features, restrictions | Replace the full list ([] clears) |
marketplaces | Only the listed entries change; others keep their state |
| everything else | Partial update |
Errors specific to this endpoint: 400 malformed property_id (must be a UUID), 404 unit not found or not owned by your account.
Marketplace Prices
GET /properties/{property_id}/marketplaces?lt;YOUR_COMPANY_PASSWORD>
Authorization: Bearer lsy_live_…
Legacy credentials: see Authentication — this endpoint reads the company password only from the query string.
Response 200 OK — real example, truncated
{
"success": true,
"property_id": "b94c71ea-412d-4969-99be-fa9db983f090",
"count": 21,
"billing_model": {
"first_month": "Billed in full (daily_rate x 30) when a paid marketplace is activated.",
"after_first_month": "Daily rate is billed every 30 days while the listing stays live.",
"on_deactivation": "Days beyond the last billed period are prorated at the daily rate.",
"free_marketplaces": "No charges are ever created."
},
"marketplaces": [
{ "name": "kijiji", "active": true, "pricing_model": "paid", "daily_rate": 0.74, "first_month_price": 22.20, "currency": "USD" },
{ "name": "zillow", "active": true, "pricing_model": "free", "daily_rate": 0, "first_month_price": 0, "currency": "CAD" },
{ "name": "craigslist", "active": false, "pricing_model": "paid", "daily_rate": 0.30, "first_month_price": 9.00, "currency": "USD" },
{ "name": "livRent", "active": false, "pricing_model": "paid", "daily_rate": 0.61, "first_month_price": 18.30, "currency": "USD" },
{ "name": "viewit", "active": false, "pricing_model": "paid", "daily_rate": 1.62, "first_month_price": 48.60, "currency": "USD" },
{ "name": "zumperPremium", "active": false, "pricing_model": "paid", "daily_rate": 0.50, "first_month_price": 15.00, "currency": "USD" },
{ "name": "zumperStandard", "active": false, "pricing_model": "paid", "daily_rate": 0.30, "first_month_price": 9.00, "currency": "USD" },
{ "name": "apartmentsStandard", "active": false, "pricing_model": "free", "daily_rate": 0, "first_month_price": 0, "currency": "CAD" },
{ "name": "facebook", "active": false, "pricing_model": "free", "daily_rate": 0, "first_month_price": 0, "currency": "CAD" }
]
}
Sorted: active first, then paid, then alphabetical. Rates are your account's configured pricing — safe to display to your end user as the exact cost.
zillowStandard → zillow, apartmentsMulti → apartmentsPremium, logisQuebec → lq, lesPac → lespac, immediate → immediateRent). Always use the canonical name in new code. Whether a marketplace is paid — and its exact rate — comes from your account's pricing: read it from this endpoint rather than hardcoding a list.Publish / Unpublish
PUT /properties/{property_id}
Authorization: Bearer lsy_live_…
Content-Type: application/json
{
"property": {
"marketplaces": [
{ "name": "kijiji", "enabled": true },
{ "name": "facebook", "enabled": true },
{ "name": "viewit", "enabled": false }
]
}
}
{
"company_id": 128,
"password": "<YOUR_COMPANY_PASSWORD>",
"property": {
"marketplaces": [
{ "name": "kijiji", "enabled": true },
{ "name": "facebook", "enabled": true },
{ "name": "viewit", "enabled": false }
]
}
}
Response 200 OK — confirms per entry what happened and what was charged
{
"success": true,
"message": "Property updated successfully.",
"property_id": "b94c71ea-412d-4969-99be-fa9db983f090",
"marketplaces": [
{
"name": "kijiji",
"status": "published",
"active": true,
"daily_rate": 0.74,
"currency": "USD",
"charge": { "type": "first_month", "amount": 22.20, "currency": "USD", "period_days": 30,
"note": "First month billed in full on activation. From day 31 the daily rate applies for each day the listing stays live." }
},
{
"name": "facebook",
"status": "published",
"active": true,
"daily_rate": 0,
"currency": "CAD",
"charge": null
},
{
"name": "viewit",
"status": "unpublished",
"active": false,
"daily_rate": 1.62,
"currency": "USD",
"charge": null,
"note": "Unpublished. Any days past the last billed period are prorated at the daily rate on the next billing run."
}
]
}
Possible status values per entry
| status | Meaning | charge |
|---|---|---|
published | Activated and pushed to the marketplace feed | first_month object for paid, null for free |
unpublished | Deactivated; removed from the feed | null (proration handled on the next billing run) |
no_change | Already in the requested state — nothing billed | absent |
error | This entry failed (message included); others still processed | absent |
Notifications: the unit's contact email receives a notification on deactivation (webhooks push these events too).
Leads
GET /properties/leads?lt;YOUR_COMPANY_PASSWORD>&start_date=2026-08-01&limit=50
Authorization: Bearer lsy_live_…
Legacy credentials: see Authentication — this endpoint reads the company password only from the query string.
Optional filters: start_date, end_date (ISO-8601 on date_created), property_id (one unit only), limit (default 100, max 500).
Response 200 OK — real example, personal details anonymized
{
"success": true,
"count": 3,
"leads": [
{
"application_id": "117278",
"application_id_number": 117278,
"person_id": "128338",
"person_id_number": 128338,
"property_id": "80240cf8-0666-484a-af80-02d6a2133522",
"source": "manual",
"first_name": "Maria",
"last_name": "Prospect",
"email": "maria.prospect@example.com",
"phone": "+15551230001",
"status": "Booked",
"date_created": "2026-08-26T21:13:39.434000",
"property_name": "40 Ashley St",
"property_address": "40 Ashley St",
"city": "Hamilton",
"province": "Ontario",
"rent_amount": "2,249",
"rent_amount_number": 2249
},
{ "application_id": "117201", "property_id": "3b540346-…", "source": "apartments", "first_name": "Alex", "last_name": "Renter", "…": "…" },
{ "application_id": "117188", "property_id": "17a8aacd-…", "source": "zumper", "first_name": "Sam", "last_name": "Applicant", "…": "…" }
]
}
source values map to the marketplace that produced the lead (zillow, kijiji, zumper, apartments, facebook, listanza, …), plus leasey / companywebsite / manual for leads that did not come from a syndicated marketplace. status uses the same stage names as Application_Status in /reports/leads (New Lead, Booked, Application In Progress, Application In Review, Signing Lease, Active Tenants; see the lead-stages table in Field Values). Until 3.10 this endpoint called the Booked stage Application Received; integrations that mapped the old name should now map Booked. The endpoint also carries person_id. application_id and person_id come back as strings here and as integers in /reports/leads.
Recommended sync loop
- Poll
GET /properties/leads?start_date=<last_sync_time>every 5–15 minutes with your last successful sync timestamp - Route each lead to your client using
property_id→ your unit mapping - De-duplicate by
application_id(stable per lead). A lead re-appearing with a newstatusis a status update, not a new lead - Attribute the lead to the marketplace using
source— this is also how you show your clients which marketplace their spend converts on
S3Marketplace Directory
The API accepts 23 marketplace names.
Full marketplace directory — paid: zumperPremium, zumperStandard, kijiji, viewit, livRent, craigslist, immediateRent · free: zillow, zillowMulti, apartmentsStandard, apartmentsPremium, facebook, rentPanda, rentalBeast, lespac, listanza, rentalSource, rentFaster, realtor, rentSeeker, rew, lq, streetEasy (NYC/NJ feed).
GET /properties/{id}/marketplaces rather than hardcoding this list; treat this directory as a name reference only.Organizations
An organization is a Syndication API client that manages many client companies with one key. Everything above for a single account still applies to each company; this part adds the organization key, the /org/… routes, bulk marketplace jobs and organization-wide billing. Section numbers in this part start with O.
O1Concepts
The three kinds of API client
| Kind | How it authenticates | What it can do |
|---|---|---|
| Direct client, shared token | Shared token + company_id + company password | The whole API, only on its own company |
| Direct client with an API key | Authorization: Bearer lsy_live_… | The same, without sending company_id or a password. Full-access or read-only keys |
| Organization (this guide) | Authorization: Bearer lsy_org_live_… | Create companies, collect their cards, manage units, marketplaces, webhooks and reports of the companies it created or that Leasey linked to it. Switch marketplaces in bulk. See its charges |
Inside an organization, every company is a normal Leasey company with its own Stripe customer, its own card, its own units and leads. The only difference is that the organization can manage it through the API.
Companies an organization can see
- Companies the organization creates with
POST /org/companies. The call creates the company, its administrator user (no password set, no email sent; they sign in with Forgot password when you decide), its Stripe customer, and links it to the organization. - Companies Leasey links to the organization after confirming with the client: linking an existing Leasey company needs Leasey.
- A company belongs to at most one organization. Any company that is not the organization's always answers 404.
What needs Leasey: creating the organization and its first key, linking an existing Leasey company, unlinking, suspending and reactivating. Companies created through the API start as external clients (reduced dashboard: Properties, Syndication, Screening, Team); company.external_client: false gives the full dashboard. GET /org/companies lists the companies you can manage (company_id and name); every other call takes that company_id in the URL.
O2Authentication & API Keys
Organization key
Authorization: Bearer lsy_org_live_…
lsy_org_test_…in the sandbox,lsy_org_live_…in production. A key from one environment does not work in the other.- Add
Content-Type: application/jsonwhen the request has a body. - The company always goes in the URL (
/org/companies/64/…). The organization never sendscompany_idor a password in the body. - A suspended organization answers 403 on everything.
POST /org/keys, put it in your system and revoke the one Leasey sent with DELETE /org/keys/{id}. From then on nobody at Leasey knows the key in use. Keep it server-side — never in browser or mobile code.Leasey stores only a SHA-256 hash of each key; a lost key cannot be recovered, only replaced. GET /org/keys returns key_last4, created_at, last_used_at, last_used_ip, revoked_at and current_key_id; the last active key cannot be revoked (409).
First call: verify the key
GET /org
Authorization: Bearer lsy_org_live_…
{ "success": true, "organization": { "id": 7, "name": "Acme Property Systems", "slug": "acme-property-systems",
"contact_email": "integrations@acme.example", "status": "active", "created_at": "2026-10-29T18:02:11", "companies": 12 } }
Company keys (Developers section)
Each company can also create its own keys, for the company only:
- Visible only to company administrators, in Profile → Developers.
- Create: a name plus the access level — Full access or Read only. The full key (
lsy_live_…/lsy_test_…) is shown once, with a copy button. - Notice: every new key sends an email to the company's administrators, in the company's language. Revoking sends no email.
- Revoke: button in the list; the key stops working instantly on every route.
- Usage: only in the
Authorizationheader, never in the URL. Works on every API route. If the request also carriescompany_idorpassword, they are ignored: the key only acts on its own company. - Read only: any
POST,PUTorDELETEanswers 403. - Expiration (optional). By default a key never expires: it works until it is revoked. When you create it you can choose a date instead (30 days, 90 days, 1 year or any date); after that moment every request with the key answers 401 with
code: api_key_expired. The date cannot be changed later: before it arrives, create the replacement and revoke the old key. Each key in the list carriesstatus(active,expiredorrevoked) andexpires_soon(truein the last 7 days, while the key still works). - Rotate without the dashboard:
GET,POSTandDELETE /api-keyswith a full-access key.
# Shared token
GET /reports/syndication?company_id=14
Authorization: Bearer <shared token>
X-Company-Password: <company password>
# With a company API key
GET /reports/syndication
Authorization: Bearer lsy_live_…
Both ways work at the same time, so a client can switch gradually. A key is better for the client: if it leaks, they revoke it themselves instantly and create another.
Companies linked to an organization
- The organization works with its
lsy_org_…key. Linked companies keep their own credentials (legacy password and keys); the organization uses its key. - If a company's administrator signs in to the dashboard, they can also create their own keys in Developers.
O3Environments & URLs
| Environment | Base URL | Stripe |
|---|---|---|
| Sandbox | https://sandbox.api.leasey.ai/v1 | Test mode: card 4242 4242 4242 4242, no real money |
| Production | https://api.leasey.ai/v1 | Live mode |
The way to connect is identical in both environments: same routes, same headers, same bodies. Only three things change:
| What changes | Sandbox | Production |
|---|---|---|
| Base URL | https://sandbox.api.leasey.ai/v1 | https://api.leasey.ai/v1 |
| Credential values | lsy_test_…, lsy_org_test_… | lsy_live_…, lsy_org_live_… |
| Stripe | Test mode, test cards | Live mode, real cards |
/v1. The previous base URLs (section 3) keep working for organizations too.O4The Flow at a Glance
The example used throughout this guide: the organization Docs Example Org creates two client companies, Maple Street Rentals (id 64, saves its card) and Oak Park Homes (id 65, no card yet), adds units, lists them on marketplaces and checks what is spent.
| Step | What | Call |
|---|---|---|
| 1 | Check the key | GET /org |
| 2 | Create a company for each client | POST /org/companies |
| 3 | Save the company's card (Stripe link) | POST /org/companies/{company_id}/payment-setup-link |
| 4 | Confirm the company is ready | GET /org/companies/{company_id} |
| 5 | Add units (listings) | POST /org/companies/{company_id}/properties |
| 6 | See which marketplaces exist and their price | GET /org/companies/{company_id}/properties/{property_id}/marketplaces |
| 7 | Turn marketplaces on or off for one unit | PUT /org/companies/{company_id}/properties/{property_id} |
| 8 | Same thing for many units at once | POST /org/marketplaces/bulk → GET /org/jobs/{job_id} |
| 9 | See how much is spent (organization and per company) | GET /org/reports/billing |
| 10 | See what each unit has listed and where | GET /org/companies/{company_id}/reports/syndication |
The table above is the order you follow the first time. The sections below are grouped by topic: companies, units, marketplaces, billing and reports.
O4.1 Check the key
Check the Key
{ "success": true, "organization": { "id": 7, "name": "Acme Property Systems", "slug": "acme-property-systems",
"contact_email": "integrations@acme.example", "status": "active", "created_at": "2026-10-29T18:02:11", "companies": 12 } }
companies: 0 is expected for a new organization.
O4.2 Create a company for each client
Create Company
{
"company": {
"company_name": "Maple Street Rentals",
"email": "office@maplestreet.example",
"time_zone": "Eastern Time (ET)",
"currency": "CAD",
"language": "en"
},
"admin": { "email": "jane.doe@maplestreet.example", "first_name": "Jane", "last_name": "Doe" }
}
Response 201 Created
{ "success": true,
"company": { "company_id": 64, "company_name": "Maple Street Rentals",
"url_slug": "maple-street-rentals", "time_zone": "Eastern Time (ET)",
"currency": "CAD", "language": "en", "external_client": true },
"admin": { "user_id": 948, "email": "jane.doe@maplestreet.example",
"login": "No password set and no email sent. The administrator signs in with 'Forgot password' on the Leasey login page when you decide." },
"stripe_customer_id": "cus_VXOXlw9zJvY4jR",
"readiness": { "ready": false, "missing": ["payment_method"], "payment_method": null },
"next_steps": [ "POST /org/companies/64/payment-setup-link to collect the company's card.",
"POST /org/companies/64/properties to add units." ] }
company_id — every call for this client uses it.What Leasey creates in that one step
| What | Detail |
|---|---|
| The company | With its denial and cancellation reasons, like a company created by Leasey |
| Its administrator | Administrator role. No password and no welcome email |
| Its Stripe customer | Labelled with the organization; the end client never sees it |
| The link to the organization | Marked as "created by the organization" |
| Dashboard view | Reduced (external client), unless "external_client": false is sent |
Right after creation only the card is missing. Typical errors: 409 if the name already exists in Leasey (send a different company.url_slug) or the administrator's email already has a Leasey login; 422 with the list of invalid fields.
Body and errors
| Part | Fields |
|---|---|
company | company_name, url_slug (optional), email (optional), time_zone (optional), currency (optional: CAD default / USD), language (optional: en default / fr), homepage (optional), external_client (optional, true default) |
admin | email, first_name, last_name (optional) |
Response 201 with company, admin, stripe_customer_id, readiness and next_steps. 409 if the slug is taken or the administrator's email already has a Leasey login; 422 with details. If Stripe is unavailable the company is still created, with a warnings entry: call /onboarding to finish.
time_zone values: Newfoundland Time (NT or NDT), Atlantic Time (AT), Eastern Time (ET), Central Time (CT), Mountain Time (MT) (default), Mountain Time (MT) - Arizona (No DST), Pacific Time (PT), Alaska Time (AKT), Hawaii-Aleutian Time (HAT or HST).
O4.3 Read, edit and confirm a company
Read & Edit Company
GET /org/companies lists them all.{ "success": true,
"company": { "company_id": 64, "company_name": "Maple Street Rentals",
"link_source": "created", "stripe_customer_id": "cus_VXOXlw9zJvY4jR",
"readiness": { "ready": true, "missing": [],
"payment_method": { "brand": "visa", "last4": "4242", "exp": "10/2027" },
"payment_method_sync": "synced" } } }
ready: true = it has an administrator, a Stripe customer and a card, so it can turn on paid marketplaces. To see all the organization's companies and which one is missing what:
GET /org/companies
{ "success": true, "count": 2,
"companies": [
{ "company_id": 64, "company_name": "Maple Street Rentals",
"readiness": { "ready": true, "missing": [], "payment_method": { "brand": "visa", "last4": "4242" } } },
{ "company_id": 65, "company_name": "Oak Park Homes",
"readiness": { "ready": false, "missing": ["payment_method"], "payment_method": null } } ] }
Company lifecycle routes
| Method | Path | What it does |
|---|---|---|
POST | /org/companies | Create a company (O4.2) |
GET | /org/companies/{id} | Profile, stripe_customer_id, readiness (also syncs the card from Stripe) |
PATCH | /org/companies/{id} | Edit company_name, email, homepage, time_zone, currency, language; external_client only on companies you created → {"success": true, "updated": [...]} |
GET | /org/companies/{id}/readiness | {ready, checks: {admin_user, stripe_customer, payment_method, reasons_seeded, api_credentials, time_zone}, missing[], payment_method} |
POST | /org/companies/{id}/onboarding | Finishes whatever is missing (Stripe customer; administrator if you send admin). Safe to repeat |
O4.4 Per-company routes
Everything in the single-account sections of this page works under /org/companies/{company_id}/… with the organization key, no company_id or password:
| Organization path | Same as |
|---|---|
POST /org/companies/{id}/properties · PUT …/properties/{property_id} | POST / PUT /properties |
GET …/properties/{property_id}/marketplaces · GET …/properties/leads | the same routes in the Syndication API |
GET …/showings/lookup · /questions · /availability · GET/POST …/showings · GET …/showings/{slot_id} · PUT …/reschedule · POST …/cancel · /confirm | the Showings API (General API tab) |
GET …/reports/{showings,leads,properties,syndication,billing} | GET /reports/… |
GET · PUT …/webhooks | GET / PUT /webhooks |
O5Units
O5.1 Add units (listings) to a company
Add Units
{ "property": { "name": "Unit 101", "address": "100 Main Street", "city": "Toronto",
"province_state": "ON", "country": "Canada", "rent_amount": 2000 } }
{ "success": true, "message": "Property created successfully.",
"property_id": "9ca8883e-ffb3-4dd5-807f-7f3ae7fc181d", "url_slug": "unit-101" }
- Keep the
property_id: marketplaces and edits use it. - Every field of the
propertyobject (bedrooms, photos, description, amenities, building…) works the same as in the Property Object Reference (§10, common to both APIs). - Edit a unit:
PUT /org/companies/64/properties/{property_id}with only the fields that change.
See every unit of a company
One row per unit, with its leads and showings:
GET /org/companies/64/reports/properties
{ "success": true, "report_type": "properties", "company_id": 64, "count": 2,
"data": [
{ "Property_ID": "ab94740f-88ac-468a-a666-7e4365138cd1", "Unit_Name": "Unit 102",
"Unit_Address": "100 Main Street", "Rent_Amount": 2150.0, "Unit_Status": "Active",
"Unit_Publish_Status": "Unpublished", "Lead_Count": 0, "Showings_Total": 0,
"Listing_URL": "https://ext.leasey.ai/listings/maple-street-rentals/unit-102" },
{ "Property_ID": "9ca8883e-ffb3-4dd5-807f-7f3ae7fc181d", "Unit_Name": "Unit 101", "…": "…" } ] }
Unit_Publish_Status: Unpublished refers to Leasey's own listing page. It does not block marketplaces: a unit created through the API can be listed on marketplaces right away.O6Marketplaces
O6.1 Which marketplaces exist and what they cost
Catalog & Prices
{ "success": true, "count": 23,
"billing": { "ready": true, "payment_method": "visa •••• 4242",
"message": "Payment method on file. Paid marketplaces can be activated." },
"billing_model": {
"first_month": "Billed in full (daily_rate x 30) when a paid marketplace is activated.",
"after_first_month": "Daily rate is billed every 30 days while the listing stays live.",
"on_deactivation": "Days beyond the last billed period are prorated at the daily rate.",
"free_marketplaces": "No charges are ever created." },
"marketplaces": [
{ "name": "immediateRent", "active": false, "pricing_model": "paid",
"daily_rate": 0.4, "first_month_price": 12.0, "currency": "USD" },
{ "name": "kijiji", "active": false, "pricing_model": "paid",
"daily_rate": 0.74, "first_month_price": 22.2, "currency": "USD" },
{ "name": "facebook", "active": false, "pricing_model": "free",
"daily_rate": 0, "first_month_price": 0, "currency": "CAD" },
"… 20 more" ] }
The 23 names, as they must be written in calls (sandbox catalog, October 30)
| Paid (daily rate → first month) | Free |
|---|---|
immediateRent 0.40 → 12.00 USD | apartmentsPremium, apartmentsStandard |
zumperStandard 0.30 → 9.00 USD | facebook, lespac, listanza, lq |
zumperPremium 0.50 → 15.00 USD | realtor, rentFaster, rentPanda, rentSeeker |
livRent 0.61 → 18.30 USD | rentalBeast, rentalSource, rew |
kijiji 0.74 → 22.20 USD | streetEasy, zillow, zillowMulti |
viewit 1.62 → 48.60 USD | |
craigslist 10.00 → 300.00 USD |
- Prices can differ per company and per environment. The catalog is the price that will be charged — always read it before turning something on.
billing.ready: falsemeans the company has no card yet: paid marketplaces will be refused.
craigslist publishes for real even from the sandbox. Use immediateRent to test.O6.2 Turn marketplaces on or off for one unit
Turn On / Off
{ "property": { "marketplaces": [ { "name": "immediateRent", "enabled": true } ] } }
Turned on — a charge for the first month is created
{ "success": true, "message": "Property updated successfully.",
"property_id": "9ca8883e-ffb3-4dd5-807f-7f3ae7fc181d",
"marketplaces": [
{ "name": "immediateRent", "status": "published", "active": true,
"daily_rate": 0.4, "currency": "USD",
"charge": { "type": "first_month", "amount": 12.0, "currency": "USD", "period_days": 30,
"note": "First month billed in full on activation. From day 31 the daily rate applies for each day the listing stays live." } } ] }
The same call on a company that has no card — nothing is turned on, nothing is charged
{ "success": true, "message": "Property updated successfully.",
"marketplaces": [
{ "name": "immediateRent", "status": "error", "code": "payment_method_required",
"active": false, "daily_rate": 0.4, "currency": "USD",
"message": "Billing profile exists but no payment method is saved. …" } ] }
status of each marketplace. For an organization's company, the fix is a payment link (§O7.2).Turn it off with "enabled": false
{ "marketplaces": [
{ "name": "immediateRent", "status": "unpublished", "active": false, "charge": null,
"note": "Unpublished. Any days past the last billed period are prorated at the daily rate on the next billing run." } ] }
status of a marketplace | Meaning | Charge |
|---|---|---|
published | Turned on | First month (paid marketplaces) |
unpublished | Turned off | Days past the last billed period, at the next billing run |
no_change | It was already in that state | None |
error + code: payment_method_required | Paid marketplace and the company has no card | None |
error + message: Unknown marketplace. | Misspelled name (see §O6.1) | None |
O6.3 Many units at once (bulk)
Bulk Marketplaces
{ "marketplaces": [ { "name": "immediateRent", "enabled": true } ],
"company_ids": [64, 65],
"published_only": false,
"dry_run": true }
{ "success": true, "dry_run": true, "total": 3,
"scope": { "company_ids": [64, 65], "published_only": false,
"units_by_company": { "64": 2, "65": 1 } } }
Nothing changed yet: it only says it would touch 3 units (2 of Maple Street, 1 of Oak Park). The same body without dry_run runs it:
{ "success": true, "job_id": 5, "status": "queued", "total": 3, "status_url": "/org/jobs/5" }
The job-creation response is HTTP 202 Accepted. Then ask for the result (repeat until status is completed; it takes seconds):
GET /org/jobs/5
{ "success": true,
"job": { "id": 5, "status": "completed", "total": 3, "succeeded": 2, "failed": 1,
"results": [
{ "ok": true, "company_id": 64, "property_id": "9ca8883e-…",
"marketplaces": [ { "name": "immediateRent", "status": "no_change",
"message": "Already in the requested state; no charge applied." } ] },
{ "ok": true, "company_id": 64, "property_id": "ab94740f-…",
"marketplaces": [ { "name": "immediateRent", "status": "published",
"charge": { "type": "first_month", "amount": 12.0, "currency": "USD" } } ] },
{ "ok": false, "company_id": 65, "property_id": "9c3148a7-…",
"marketplaces": [ { "name": "immediateRent", "status": "error",
"code": "payment_method_required" } ] } ] } }
Reading it: Unit 101 was already on, so it was not charged twice; Unit 102 was turned on and charged its first month; Oak Park has no card, so its unit was refused.
GET /org/jobs/{id} paginates results with page / page_size and returns job_type: "marketplaces_bulk", status (queued → running → completed, or failed with error), next_index, succeeded and failed. GET /org/jobs lists the last 50 jobs.
Choosing the units
| Filter | Effect |
|---|---|
company_ids | Only those companies |
building_ids | Only units of those buildings |
property_ids | Exactly those units |
published_only | Default true: only units published on Leasey's own listing page. Units created through the API are unpublished (§O5.1), so for them send false; otherwise the call answers 422 No units match |
| (no filter) | Every unit of every company in the organization |
- One job applies one list of marketplaces; for different lists, one job per group.
- Up to 5,000 units per job; above that, split it by
company_idsorbuilding_ids. - To turn off in bulk: the same call with
"enabled": false. GET /org/jobslists the organization's jobs.
O7Billing
O7.1 How billing works
- Each company pays with its own card. The organization does not pay for its companies.
- Activating a paid marketplace creates the full first-month charge (daily rate × 30). After that it is billed every 30 days while it stays active. Deactivating prorates the days beyond what was already billed.
- Pending charges are invoiced to the company's card by the daily billing job. In the sandbox that job is turned off: charges stay
pendingand are never collected. - The organization sees its own money with
GET /org/reports/billing(§O7.3).
| Rule | Detail |
|---|---|
| First month | Billed in full on activation: daily_rate × 30 |
| From day 31 | The daily rate is billed every 30 days while the listing stays live |
| On deactivation | Days beyond the last billed period are prorated. Inside the first 30 days, no refund |
| Free marketplaces | Never generate any charge |
| Re-sending the same state | Idempotent: no_change, no duplicate charge |
O7.2 Save the company's card — the Stripe link
Card Payment Link
{ "success_url": "https://app.example.com/clients/ok",
"cancel_url": "https://app.example.com/clients" }
Response
{ "success": true, "company_id": 64,
"url": "https://checkout.stripe.com/c/pay/cs_test_c1G0uoaP…",
"expires_at": 1793468267,
"note": "Open the URL to save the card on Stripe's page. The card is attached to this company's Stripe customer and becomes its default; card details never pass through Leasey or your system." }
How it is used
- The organization sends that URL to its client (the company), by email or from its own system.
- The client enters the card on Stripe's page. In the sandbox:
4242 4242 4242 4242, any future date, any CVC. - Stripe saves the card on that company's Stripe customer, and Leasey makes it the default for its invoices.
- The client returns to
success_url.
- Saving the card charges nothing.
- The link expires after 24 hours. A new one can be requested at any time; it also replaces the card.
- In production, Leasey picks up the card the next time the organization reads the company, updates a unit, or runs a bulk job. In the sandbox, Stripe's notification also arrives instantly.
How the company, its Stripe customer and its card stay linked
Nothing has to be matched afterwards: every company already has its own Stripe customer, and the link is a Stripe page for that customer, so the card can only land there. The organization never handles Stripe IDs or card data, only company_id.
O7.3 How much is being spent
Billing Report
GET /org/reports/billing?start_date=2026-10-01
{ "success": true, "report_type": "organization_billing", "organization_id": 4, "companies": 2,
"totals_by_currency": { "USD": { "pending": 24.0 } },
"by_company": [
{ "company_id": 64, "company_name": "Maple Street Rentals", "charges": 2,
"amount_by_currency": { "USD": 24.0 },
"summary": {
"by_status": [ { "status": "pending", "currency": "USD", "items": 2, "amount": 24.0 } ],
"by_marketplace": [ { "marketplace": "immediateRent", "currency": "USD", "items": 2, "amount": 24.0 } ],
"outstanding": [ { "currency": "USD", "amount": 24.0 } ],
"current_daily_run_rate": [ { "currency": "USD", "amount": 0.8 } ],
"projected_monthly": [ { "currency": "USD", "amount": 24.0 } ],
"active_paid_listings": 2 } },
{ "company_id": 65, "company_name": "Oak Park Homes", "charges": 0,
"amount_by_currency": {}, "summary": { "active_paid_listings": 0, "…": "…" } } ],
"count": 2,
"data": [
{ "Company_ID": 64, "Company_Name": "Maple Street Rentals",
"Property_ID": "ab94740f-…", "Property_Name": "Unit 102",
"Marketplace": "immediateRent", "Billing_Type": "first_month",
"Period_Start": "2026-10-30T17:37:53", "Period_End": "2026-11-29T17:37:53", "Days": 30,
"Daily_Rate": 0.4, "Amount": 12.0, "Currency": "USD", "Status": "pending",
"Stripe_Invoice_ID": null, "Invoiced_At": null, "Paid_At": null } ] }
Where each answer is
| Question | Field |
|---|---|
| How much has the whole organization spent? | totals_by_currency (split by status) |
| How much has each company spent? | by_company[].amount_by_currency |
| On which marketplaces did each company spend? | by_company[].summary.by_marketplace |
| How much is still unpaid? | by_company[].summary.outstanding (pending + failed) |
| How much does each company spend per day today, and per month at that pace? | by_company[].summary.current_daily_run_rate and projected_monthly |
| How many paid listings does each company have on now? | by_company[].summary.active_paid_listings |
| Every charge: unit, marketplace, period, amount, status, Stripe invoice | data[] |
Filters (query string, combinable): start_date and end_date (YYYY-MM-DD, on the date the charge was created), company_id (only that company), status (pending, invoiced, paid, failed), page and page_size (up to 2000). Only one company: GET /org/reports/billing?company_id=64, or the company report GET /org/companies/64/reports/billing (same summary and data, without the company columns).
Status | Meaning |
|---|---|
pending | Charge created, not yet invoiced. In the sandbox everything stays here (the billing job is off) |
invoiced | Invoice created in Stripe against the company's card (Stripe_Invoice_ID) |
paid | Paid |
failed | The card was declined; it counts as outstanding |
Billing_Type | When |
|---|---|
first_month | On turning a paid marketplace on: daily rate × 30, in full |
recurring | Every 30 days while it stays on |
final_proration | On turning it off: days past the last billed period |
Every invoice and payment of an organization's company carries company_id, organization_id, organization_name, account_type=organization_member, source and billing_item_id as Stripe metadata.
O8Reports
O8.1 What each company has listed, and where
Syndication Report
{ "success": true, "report_type": "syndication", "company_id": 64, "count": 2,
"summary": { "properties_with_syndication": 2, "active_listings": 2,
"total_billed": [ { "currency": "USD", "amount": 24.0 } ],
"by_marketplace": [ { "marketplace": "immediateRent", "active_listings": 2, "total_billed": 24.0 } ] },
"data": [
{ "Property_ID": "9ca8883e-…", "Property_Name": "Unit 101", "Marketplace": "immediateRent",
"Active": true, "Published_Since": "2026-10-30 17:37:52", "Last_Action": "published",
"Days_Billed": 30, "Total_Billed": 12.0, "Pending_Amount": 12.0,
"Invoiced_Amount": 0.0, "Paid_Amount": 0.0, "Currency": "USD",
"Billed_Until": "2026-11-29 17:37:52" },
{ "Property_ID": "ab94740f-…", "Property_Name": "Unit 102", "Marketplace": "immediateRent",
"Active": true, "…": "…" } ] }
After turning both off, the same report shows "Active": false, "Last_Action": "unpublished" and active_listings: 0; the billed amounts stay (history is not erased).
This report is per company. For the whole organization, call it for each company returned by GET /org/companies. Money for the whole organization in one call is §O7.3.
Other reports of a company
Same pattern: GET /org/companies/{company_id}/reports/{type}
type | What |
|---|---|
properties | Every unit, with status, rent, leads, showings and its Leasey page (§O5.1) |
syndication | What is listed where and how much it cost (this section) |
billing | Every charge (§O7.3) |
leads | Leads received |
showings | Showings booked |
O9Quick Reference — "I want to know…"
| I want to know… | Call | Look at |
|---|---|---|
| Whether a company can turn on paid marketplaces | GET /org/companies/{id}/properties/{property_id}/marketplaces | billing.ready |
| Which units a company has | GET /org/companies/{id}/reports/properties | data[] |
| Which marketplaces exist and what they cost | GET /org/companies/{id}/properties/{property_id}/marketplaces | marketplaces[] |
| What is listed where, for one company | GET /org/companies/{id}/reports/syndication | data[] (Active) and summary |
| How much my whole organization spent | GET /org/reports/billing | totals_by_currency |
| How much each company spent | GET /org/reports/billing | by_company[] |
| How much one company spent, charge by charge | GET /org/reports/billing?company_id={id} | data[] |
| How much is still unpaid | GET /org/reports/billing?status=pending (or failed) | totals_by_currency |
| How much it will cost per month at today's pace | GET /org/reports/billing | by_company[].summary.projected_monthly |
| How a bulk job went | GET /org/jobs/{job_id} | succeeded, failed, results[] |
| What was done with my keys | GET /org/audit?limit=100 | events[] |
O10Keys & Maintenance
| To | Call |
|---|---|
| Rotate its key | POST /org/keys (new key) → switch it in your system → DELETE /org/keys/{old_id} |
| See its keys | GET /org/keys |
| See what was done with its keys | GET /org/audit?limit=100: the latest actions made with your keys (route, company, status, key, IP, time); request bodies are never logged |
| Edit a company's profile | PATCH /org/companies/{id} (O4.3) |
| Finish something left pending | POST /org/companies/{id}/onboarding (O4.3) |
| Leads and webhooks of a company | /org/companies/{id}/properties/leads and /org/companies/{id}/webhooks (same as the Syndication API: leads in S2, webhooks in §9) |
Status codes of the organization routes
| Status | Meaning |
|---|---|
| 401 | Missing, wrong, revoked or expired organization key |
| 403 | Organization suspended |
| 404 | Unknown route, or the company is not in your organization (whether it exists or not) |
| 409 | Conflict (slug or admin email taken, last active key) |
| 422 | Validation failed; see details |
| 502 | Stripe did not answer; retry |
| 503 | Organizations not enabled in that environment yet |
Give the end client access to the dashboard
When the organization decides, the company's administrator signs in to Leasey with "Forgot password" using the email sent in admin.email. They see the reduced dashboard (Home, Properties, Syndication, Screening, Team) and, as administrator, the Developers section.
Lost key, suspension and linking
- Lost key: ask your Leasey contact for a new one. If you still have an active key, you can rotate it yourself with
/org/keys. - Suspension: Leasey can suspend and reactivate an organization; while suspended, every call answers 403.
- Unlinking a company: its data stays intact and the organization loses access.
O11Testing
Test the whole flow in the sandbox first, with lsy_org_test_… keys and the sandbox base URL (§O3). A good order, using the calls above:
- Check the key (
GET /org) and create a company (§O4.1, §O4.2). - Open the payment link and save the test card
4242 4242 4242 4242, any future date, any CVC (§O7.2). Confirmready: true(§O4.3). - Create a unit and read its marketplace catalog (§O5.1, §O6.1). Turn on
immediateRent, then run a bulk dry run and the real job (§O6.2, §O6.3) — the second time it answersno_change, so nothing is charged twice. - Turn the marketplace off, and read the billing and syndication reports (§O7.3, §O8.1).
- Rotate the organization key: create a new one, revoke the old one, and confirm the old one now answers 401.
·
craigslist publishes for real even from the sandbox. Do not activate it in tests.· In production, marketplaces publish even if the unit is not published on Leasey. A test unit with a paid marketplace shows up on that portal until the marketplace is turned off.
· In production, the first month is charged in full on activation, even if it is turned off right after.
O12Good to Know
| Behavior today | What to do |
|---|---|
The webhook payload does not include company_id | Register one URL per company with the id in the URL |
| There is no report of what is listed for all the organization's companies in one call | Call GET /org/companies/{id}/reports/syndication for each company |
The bulk job only takes published units by default (published_only: true), and units created through the API are unpublished | Send "published_only": false |
The payment_method_required message says to add a card in Settings → Billing | For an organization's company the way is the payment link (§O7.2) |
The properties report shows City and Province_State empty for units created through the API | The city is saved on the unit (used for the listing and its location); the report reads an older field |
| Saving a card | Leasey also checks Stripe every time you read the company, create or update one of its units, or run a bulk job; a card saved a moment ago counts right away |
| An organization's name and contact cannot be edited through the API | Ask your Leasey contact |
O13FAQ
Which status codes do the organization routes return?
See the status table in Keys & Maintenance (O10).
Can a company belong to two organizations?
No. At most one.
Which companies can the organization see?
Only the ones Leasey links to it. Any other company answers 404.
Can the organization see how much it spent in total, and how much each company spent?
Yes, in one call: GET /org/reports/billing. totals_by_currency is the whole organization and by_company is each company, with its marketplaces, what is unpaid and the monthly projection (§O7.3).
Can it see what is listed on each marketplace?
Yes, per company: GET /org/companies/{id}/reports/syndication (§O8.1). There is no single call for all its companies yet.
Who pays?
Each company, with its own card. The organization never pays for its companies (§O7.1).
Are prices the same for every company?
Not necessarily. The marketplace catalog of each unit gives the price that will be charged (§O6.1).
Does turning a marketplace off give money back?
Not inside the first 30 days: the first month is charged in full on activation. After that, only the days beyond the last billed period are charged.
A marketplace answered "status": "error" but the HTTP code was 200. Is that right?
Yes. One call can turn several marketplaces on or off, so each one has its own status. Always read it (§O6.2). The usual one is payment_method_required: the company has no card yet.
The bulk job answers 422 No units match.
By default it only takes units published on Leasey's own listing page, and units created through the API are not. Send "published_only": false (§O6.3).
Why do charges in the sandbox always stay pending?
The daily billing job is off in the sandbox, so nothing is ever invoiced or collected there.
Does anything change for companies that already use the shared token and password?
No. That scheme keeps working on every route with the same responses, and both ways can be used at the same time while a client migrates to a key.
S4FAQ
Should I use an API key or the token + password?
An API key. It is one secret, it is tied to one company, your administrators create and revoke it themselves in Profile → Developers, it can be read-only and it can expire. The token + password keep working while you migrate.
Why does a paid marketplace come back payment_method_required?
Paid activations need a Stripe billing profile with a saved card. Read billing.ready and billing.message from GET /properties/{id}/marketplaces before showing paid toggles; free marketplaces always work. The sandbox behaves the same way.
How do I know the exact cost before publishing?
GET /properties/{id}/marketplaces returns daily_rate and first_month_price per marketplace with your account's pricing — show those numbers in your confirmation dialog. The PUT response then confirms the actual charge applied, so your user sees the same number twice.
What happens if I publish the same marketplace twice?
Nothing: the state-change guard answers no_change and no charge is created. Same for deactivating an already-inactive marketplace. You can safely retry a PUT after a network error.
How do I map a lead back to my client?
Every lead (pull or push) carries property_id — match it to the unit you created and you have the client. source tells you which marketplace produced it; application_id is the stable de-dup key.
How do I test without real charges?
Use the sandbox account from onboarding: it is isolated, and test billing items there are never invoiced. Free marketplaces (zillow, facebook, …) also never generate charges on any account.
Leads: should I poll or use webhooks?
Both work and can run together. Webhooks (lead.received) deliver within ~5 minutes with retries; polling is simpler and doubles as backfill. Start with polling, add the webhook when you want lower latency.
What currency will I be billed in?
Your account's currency (CAD or USD, set by account country at onboarding). Every price and charge in the API carries its currency field explicitly.
A report returned 413 — is something broken?
No: the response is bigger than 6 MB. Add page/page_size (the 413 body suggests a size) or narrow start_date/end_date.
Can my end clients call this API directly?
No — the partner model is one account: you hold the credentials, your clients never touch Leasey. Keep the client → property_id mapping on your side.
S5Glossary
| Term | Meaning |
|---|---|
| Unit / Listing / Property | The rental unit being syndicated (one property_id) |
| Marketplace | External portal: Zillow, Kijiji, Facebook Marketplace, Apartments.com, Zumper, etc. |
| Syndication | Publishing a unit to one or more marketplaces via Leasey's feeds |
| Activation | Turning a marketplace on for a unit (may create a charge) |
| Daily rate | Per-day price of a paid marketplace, set per partner account |
| First month | daily_rate × 30, billed in full on activation |
| Proration | Partial-period billing at the daily rate after deactivation |
| Partner account | Your single Leasey account under which all your clients' units live |
S6Integration Checklist
Use this to track your integration end to end:
- API key created in Profile → Developers (read-only for dashboards), stored server-side; legacy token/password only if you already had them
X-Request-Idlogged on every response, so support can find any request- Writes send an
Idempotency-Keyand handleIdempotent-Replayed - Reports synced with
updated_since, de-duplicated byApplication_ID/Showing_ID/Property_ID/Billing_ID POST /properties— create a test unit, storeproperty_idGET /properties/{id}/marketplaces— render your syndication screen with prices- Confirmation dialog shows
first_month_pricebefore publishing PUT /properties/{id}withmarketplaces— publish; surface thechargeconfirmation to your user- Handle
no_change(idempotent re-publish) and per-entryerrorstatuses - Unpublish flow — surface the proration note
GET /properties/leadspolling wired into your CRMGET /reports/syndicationpowering your admin dashboardGET /reports/billingreconciled against your own invoices (checkoutstandingandcurrent_daily_run_rate)PUT /webhooksregistered; signature verification implemented; secret stored on first response- Webhook receiver answers 2xx within 8 s; delivery states monitored via
GET /webhooks - Load test with your expected unit volume
S7Best Practices
🔒 Security
- Call the API only from your backend — never expose the token or password in browser/mobile code
- Use an API key; if you still use the legacy pair, prefer the
X-Company-Passwordheader over the query string - Rotate keys periodically; give each system its own key so a leak is contained
- Keep the token in environment variables, never in source control
- Verify webhook signatures and reject anything that fails
⚡ Performance
- Pass explicit
start_date/end_dateon showings and leads reports - Cache the properties report for 5–15 minutes
- Design for a 60-second client timeout
- Batch marketplace toggles into one
PUT
🔄 Reliability
- Retry
5xxwith exponential backoff (2 s, 4 s, 8 s); never retry4xx - Read state before retrying a write that timed out — writes are idempotent, but confirm rather than assume
- Persist
property_idon201immediately - Treat every field as nullable
📋 Data Consistency
photos,features,restrictionsare replaced on update — send the full current setteam_memberson update is additive; remove from the Leasey dashboard- Marketplace names are canonical; aliases are accepted but normalized
13Support
Before reporting an outage, check https://status.leasey.ai.
request_id (in the body of every error and in the X-Request-Id header of every response), your company_id, the request without credentials and the exact response (code, message, status code). Sandbox credentials, the openapi.yaml file and pricing questions go through the same channel.API access is included with every Leasey account, with no usage quota.
The Syndication API for platforms that resell Leasey syndication to their own customers (organizations, white-label) is the Custom plan; see Pricing.
14Changelog
| Version | Date | Changes |
|---|---|---|
3.18 | 2026-11-06 | New: every value now has one type across the API. The older fields sent as text get a twin with the same value as a number: application_id_number, person_id_number and rent_amount_number in /properties/leads and lead.received; price_number, bedrooms_number and bathrooms_number in /showings/lookup; slot_id_number in the reschedule response; Parking_Stalls_Number in /reports/showings. The text fields stay and are deprecated (section 4, "Data types in responses"). rent_amount in the property object is documented as a number, which it always accepted |
3.17 | 2026-11-06 | New: leasing writes (section G5). POST /applications creates the lead for a unit, POST /applications/{application_id}/send sends the application by email and text, and PATCH /applications/{application_id} changes its stage, decision, archive and the applicant's details. POST /applications/{application_id}/screening orders a Certn credit check or Softcheck, charged to your card on file at your account's price (Idempotency-Key required). POST /leases and PATCH /leases/{lease_id} create and edit leases; GET /document-templates, POST /leases/{lease_id}/send, POST /documents/{document_id}/send and /void handle e-signature. New error codes in section 12 |
3.16 | 2026-11-06 | New: document.status_changed webhook: an e-signature document is created or changes status (sent for signatures, completed, voided, archived), with how many signers have signed (section 9, DocumentStatusChangedEvent) |
3.15 | 2026-11-06 | New: read applications, screening checks, leases and e-signature documents (section G5 Leasing API): GET /applications, /applications/{application_id}, /applications/{application_id}/screening, /screening, /leases, /leases/{lease_id}, /documents, /documents/{document_id}, paginated, with filters and updated_since. Four webhook events for the leasing pipeline: application.stage_changed, application.decision_changed, screening.status_changed, lease.status_changed (section 9). API keys can be limited to some resources with resources (section 2); keys without it keep working everywhere |
3.14 | 2026-11-05 | New: GET /health, with no credentials, for your own monitoring (section 3). Fixed: /reports/leads returned one identical row per pet of an application; each application now appears once. /showings/lookup returned bookshowingby: null for some older events; it is now 0, as documented. A non-numeric stored rent no longer makes /reports/leads or /reports/showings fail; it reads as 0, as in /reports/properties. Docs: column reference for every report (section 8), with the same row schemas in openapi.yaml, which now types every response and accepts an API key alone on every company route (company_id and password are legacy and optional); Lead_source in /reports/showings is deprecated in favor of Lead_Source; phone is required in POST /showings; the request timeout is 29 s; /reports/syndication is not paginated |
3.13 | 2026-11-05 | New: Leasey's own domains with the version in the path: https://api.leasey.ai/v1 (production) and https://sandbox.api.leasey.ai/v1 (sandbox), TLS 1.2+; versioning policy. Every response carries Strict-Transport-Security, organization routes included. The previous execute-api base URLs keep working unchanged |
3.12 | 2026-11-04 | New: every error carries a stable machine-readable code and the request_id, and every response the X-Request-Id header; errors answered by the gateway itself (unknown route, throttling, timeout) use the same JSON format; protective throttle with 429 and Retry-After; Idempotency-Key on every write; updated_since and Updated_At on /reports/leads, /reports/showings, /reports/properties and /reports/billing for incremental sync; the order of every report is documented and stable; API keys can be created with an optional expiration date (expires_at; by default they never expire) and every listed key carries status and expires_soon. Additive: error, message and every existing field keep their values |
3.11 | 2026-11-03 | New: Prequalification_Result (Qualified / Not Qualified) on every row of /reports/leads and /reports/showings, and the answers themselves with include_answers=true. Leads screened out by the questions can now be followed in /reports/leads. Additive |
3.10 | 2026-11-03 | Fixed: /reports/leads returned the raw value 3 for leads that went through the booking link; it now returns Booked. /properties/leads and lead.received called that stage Application Received; they now return Booked too. New: Unit_ID on /reports/leads. Docs: lead stages with both sets of names, which ID is which across reports, what each date filter applies to, a reschedule keeps the showing's id, Disqualified showing status |
3.9 | 2026-10-29 | New: per-company API keys (lsy_live_…) accepted by every endpoint in place of the shared token + company password; Full access and Read only keys; created and revoked by company administrators in the dashboard (Profile → Developers) or rotated through GET/POST/DELETE /api-keys; email to administrators on every new key. Responses carry Strict-Transport-Security. Legacy credentials keep working unchanged |
3.8 | 2026-10-29 | New: Organizations — one key manages many companies: create companies with their administrator and Stripe customer, collect each company's card through a Stripe page, use every existing endpoint per company under /org/companies/{id}/…, switch marketplaces in bulk as a background job, consolidated billing report, key rotation and audit log. Stripe: invoices and their payments carry company_id, source, billing_item_id and, for organization companies, organization_id metadata. Additive |
3.7 | 2026-10-21 | Fix: GET /reports/showings — on building-level booking links the unit columns showed the link's placeholder unit instead of the unit the prospect booked; lead columns could be copied from another showing at the same time slot. Rows now carry the booked unit and the exact lead of each showing. Added: Showing_ID, Event_ID, Event_Name, Booking_Scope (building / unit) on every row. Additive |
3.6 | 2026-09-22 | Fix: GET /showings/availability returned zero dates for rolling events because it read the event's stored end date instead of today + rolling days; it now matches the booking widget. Documented how far ahead each event type offers, and that unpublished events return 404 |
3.5 | 2026-09-18 | Team members created through the API are Admin by default — no role needed. Send role, role_id or is_admin: false for anything else |
3.4 | 2026-09-11 | Team members: role by name (Admin, Member, Showing Coordinator) and is_admin set the role shown in the platform; the response echoes role/role_id/is_admin. The paid-marketplace payment check applies to every account (billing.exempt removed). Marketplace directory now 23 with streetEasy |
3.3 | 2026-08-27 | API-triggered publish/unpublish fires the same notification flow as the platform UI |
3.2 | 2026-08-27 | Buildings created via building_name inherit the unit's address/city/province/country/postal/coordinates; duplicate unit name returns 409 Conflict with the existing property_id |
3.1 | 2026-08-27 | Payment-method guard: paid marketplace activations are refused with code: payment_method_required when the account has no Stripe billing profile / saved card; the catalog exposes billing.ready + message |
2.0 | 2026-08-26 | Complete guide: single document for every external endpoint |
1.2 | 2026-08-26 | Money-flow diagram, lead.received webhook example, operational limits table, FAQ, PII anonymized in examples, full field catalogs |
1.1 | 2026-08-26 | Billing ledger (/reports/billing), webhooks implemented, pagination + 413, real 422 and GET /webhooks examples, Property Object reference completed against the code |
1.0 | 2026-08-26 | Initial partner guide: catalog with prices, charge confirmation, consolidated report, leads with property_id/source |
Leasey API v3.18 — © 2026 Leasey.ai. All rights reserved.