Skip to main content

Leasey.AI

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.

Version 3.18 · API v1 Protocol HTTPS Format JSON

Last updated: November 6, 2026

🧭
How this page is organized. Part 1 is common to both APIs — read it first. Part 2 has two tabs, General API and Syndication API: open the one that matches your integration.
⬇️
Downloads. openapi.yaml (OpenAPI 3.18 — import it in Postman and every endpoint appears pre-built) · Markdown version · llms.txt · llms-full.txt · Status page

Which API do you need?

General APISyndication API
What it is forKeeping your property data in sync with LeaseyPublishing your units to marketplaces through Leasey
You can
  • Import and update units in Leasey
  • Get properties, leads and showings out of Leasey (reports and webhooks)
  • Look up, book, reschedule, confirm and cancel showings
  • Run the leasing pipeline: create and send applications, approve or deny them, order screening checks, and send leases for e-signature
  • Publish units to 23 marketplaces and see prices before you do
  • Follow billing and what is listed where
  • Receive the leads the marketplaces generate
  • Manage many client companies as an organization
Main endpointsPOST/PUT /properties, /showings/* (lookup, availability, book, reschedule, confirm, cancel), /applications, /screening, /leases, /documents, /document-templatesPOST/PUT /properties (with marketplaces), GET /properties/{id}/marketplaces, GET /properties/leads, /org/*
Webhook eventslead.received, application.stage_changed, application.decision_changed, screening.status_changed, lease.status_changed, document.status_changedlead.received, unit.published, unit.unpublished
Not includedPublishing to marketplaces, marketplace billing, organizations.The showings endpoints are documented in the General API tab (organizations can use them per company).
Common to bothCredentials and authentication · environments · conventions and errors · the 5 reports · webhook delivery and signature · the property object
Part 1
Common to both APIs

Everything in these sections applies whether you use the General API or the Syndication API.

1Getting Started

Credentials and where to get them

CredentialWhat it isWhere 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 ownLeasey dashboard → Profile → Developers (company administrators), or POST /api-keys
API token (legacy)Partner-level bearer token, identical on every requestProvided by Leasey during onboarding
company_id (legacy)The Leasey account you act forLeasey dashboard → Settings, or your onboarding email
Company password (legacy)That account's API password (passwordapi)Leasey dashboard → Settings → API Access
🔒
Keep every credential server-side. Never ship it in browser or mobile code, and never put it in a URL: query strings end up in logs.

Which API do you use?

If you…You useGo 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 systemGeneral APIGeneral API tab
Publish units to marketplaces through Leasey and collect the leads they generate — with one account, or as an organization managing many client companiesSyndication APISyndication API tab
Only read data (dashboards, CRMs, BI)Either — the Reports API is common to bothPart 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.

http
GET /reports/syndication
Authorization: Bearer lsy_live_<40 characters>
http
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_id or password, 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 with code: 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 carries status (active, expired or revoked) and expires_soon (true in 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 with POST /api-keys. Any other route answers 403 with code: api_key_resource_not_allowed. The resources are properties (units and marketplaces), leads (/properties/leads), showings, reports, webhooks, applications, screening, leases, documents and api_keys (list, create and revoke keys). Access level and resources combine: a read-only key limited to applications can 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 needs api_keys to manage keys at all.
  • Rotate without downtime from your own system, using a full-access key:
MethodPathWhat it does
GET/api-keysYour 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 401 api_key_revoked; expired 401 api_key_expired; read-only key on a write 403 api_key_read_only; key limited to other resources 403 api_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.

CredentialWhat it isWhere it goes (GET)Where it goes (POST/PUT)
API tokenPartner-level bearer tokenAuthorization: Bearer <token>Authorization: Bearer <token>
Company passwordYour account passwordX-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.

http
GET /reports/syndication?company_id=128
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
⚠️
If your password contains characters that are special in URLs (+ & # %), URL-encode them when sending as a query parameter (+ must be %2B). Passwords sent in headers or JSON bodies need no encoding.

3Environments & Sandbox

EnvironmentBase URLUse it for
Sandboxhttps://sandbox.api.leasey.ai/v1Building and testing your integration
Productionhttps://api.leasey.ai/v1Live 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 with payment_method_required exactly 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 pending and 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:

ReportOrder
/reports/showingsDate_start ascending, then Showing_ID ascending
/reports/leadsLead_Date_Created descending, then Application_ID descending
/reports/propertiesActive units first, then published ones, then Property_ID descending
/reports/billingCreated_At descending, then Billing_ID descending
/reports/syndicationProperty_ID, then Marketplace

Operational limits

LimitValueBehavior at the limit
Request timeout29 sThe gateway answers 504 timeout: paginate or narrow the dates
Request rateprotective throttle429 Too Many Requests with Retry-After
Report response size6 MB413 with suggested_page_size — paginate or narrow dates
page_size (reports)max 5000Values above are capped to 5000
limit (/properties/leads) (Syndication API)default 100, max 500Values above are capped to 500
/showings/lookup page_size (General API)default 50, max 100Values above are capped to 100
Availability window per call (General API)30 daysUse narrower date_from/date_to for larger ranges
Photos per unitno hard capFirst photo is the cover; HTTPS URLs only
Marketplaces per publish call (Syndication API)all 23 in one callEach entry succeeds or fails individually
Webhooks per account1PUT /webhooks updates it in place
Webhook delivery timeout8 sNon-2xx or slower → retried (5 attempts, backoff)
Publish idempotency (Syndication API)state-change guardRe-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
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:

json
{
  "error": "Unauthorized",
  "code": "invalid_api_key",
  "message": "Invalid API key.",
  "request_id": "0926a21a-0073-4d9b-8c73-a2a85b22202e"
}
FieldWhat it is
codeStable, machine-readable, snake_case. Program against it: a condition keeps its code. New codes may be added
errorThe name of the HTTP status
messageFor people. It may be reworded at any time; do not parse it
request_idUnique 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.

HTTPerrorWhenWhat to do
400Bad RequestMalformed property_id (not a UUID), missing required identifiersFix the request
401UnauthorizedBad 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
403ForbiddenRead-only key on a write; unknown route (route_not_found); suspended organizationUse a full-access key / check the path and method
404Not FoundUnit doesn't exist or belongs to another accountVerify property_id
409ConflictPOST /properties with a name that already exists in your account — the body carries the existing property_idUpdate that unit with PUT /properties/{property_id}, or use a different name (unit names are unique per account)
413Payload Too LargeA report response exceeds the 6 MB capUse page/page_size or narrow start_date/end_date; the response suggests a page size
422Unprocessable EntityValidation failed — details array lists each field errorFix the listed fields
429Too Many RequestsProtective throttleWait Retry-After seconds and retry
500Internal Server ErrorUnexpected failure on our sideRetry once; report to Leasey with the request_id
503Service UnavailableTemporarily unavailableRetry later
504Gateway TimeoutThe request took longer than 29 sPaginate 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_expiredThe key does not exist, was revoked, or passed its expiration dateCreate a new key in Profile → Developers (or POST /api-keys with another full-access key)
403 api_key_read_onlyA read-only key was used for POST/PUT/DELETEUse 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 credentialsCheck path and method against this guide
429 Too Many RequestsA burst hit the protective throttleWait the seconds in Retry-After, then back off exponentially
401 Authentication token required / Invalid authentication tokenThe bearer header is missing or malformedHeader 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 passwordCompany credentials missing or wrong for the request styleGET → 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 stringThe password contains +, &, # or %URL-encode it (+ → %2B) or send it in the header/body
400 'company_id' must be an integercompany_id sent as a quoted stringSend 123, not "123" (in Postman: no quotes in Params)
422 with a details arrayValidation failed on create/updateFix every entry listed; nothing was created
404 on PUT /properties/{id}Wrong id, or the record belongs to another accountUse the exact id returned when it was created; you can only touch your own account's data
413 Payload Too LargeReport bigger than 6 MBPaginate (page, page_size) or narrow the dates — the body suggests a size
The response body looks like a JSON string inside JSONYour HTTP client is parsing an already-parsed bodyParse the body exactly once
CORS error in a browserYou are calling the API from client-side codeCall it from your backend — credentials must never live in the browser
500 Internal Server ErrorUnexpected failure on our sideRetry 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)

bash
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)

bash
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

python
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

python
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

  1. Environments → New: add base_url (the production URL above) and api_key (or, for the legacy method, api_token, company_id, company_password).
  2. New request GET {{base_url}}/reports/showings → Headers: Authorization = Bearer {{api_key}} → Send.
  3. New request POST {{base_url}}/properties → Headers: Authorization, Content-Type: application/json → Body (raw JSON): {"property": {…}} → Send.
  4. Or skip steps 2–3: File → Import the openapi.yaml and 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}/…).

http
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 sendYou get
A new keyThe 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 request422, code: idempotency_key_reused
The same key while the first request is still running409, code: idempotency_request_in_progress, with Retry-After
  • Keys belong to your credentials: another partner's key never collides with yours.
  • 5xx responses 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 /properties answers 409 with the existing property_id when the name already exists; re-sending the same marketplace state answers no_change and is never billed twice; POST /showings for 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…CallNotes
The unit portfolio (names, addresses, rent, status, days listed)GET /reports/propertiesNo date filter; cache 5–15 min — it is the heaviest report
Prospects and applicants with rating and application stateGET /reports/leadsDefaults to Jan 1 of this year — pass start_date/end_date
Showing activity with team member and lead sourceGET /reports/showingsSame date default; paginate on large accounts
What is published where and what it accumulatesGET /reports/syndicationSummary + one row per unit × marketplace
Every charge and its payment stateGET /reports/billingCharge-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

Reportstart_date / end_date filter onWithout start_date
/reports/leadsWhen the lead was created (Lead_Date_Created)January 1 of the current year
/reports/showingsWhen 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):

ReportA row counts as changed when…
/reports/leadsThe lead's application (stage, archive, approval), its applicant record or its contact details change
/reports/showingsThe showing (time, status, confirmation, feedback) or its lead changes
/reports/propertiesThe unit changes
/reports/billingA charge is created, invoiced, paid or retried
/reports/syndicationNot supported: it is a snapshot of right now (400, updated_since_not_supported)
API keyrecommendedLegacy credentials
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

GroupColumns
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
Summaryproperties_with_syndication, active_listings, total_billed, by_marketplace

updated_since is not supported on this report (see Incremental sync).

Billing Ledger

GET
/reports/billing
The charge-by-charge ledger for reconciliation: every billing item with its payment state, the Stripe invoice it went into, and the dates.

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).

API keyrecommendedLegacy credentials
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), with projected_monthly = ×30. If a client asks "what am I spending?", this is the number
  • by_marketplace — where the money goes, largest first
  • Billing_Type per 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

GroupColumns
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
Summarytotal_items, by_status, by_marketplace, outstanding, current_daily_run_rate, projected_monthly, active_paid_listings

Properties Report

GET
/reports/properties
Your full unit portfolio (the "property list"). No date filter; cache 5–15 min — it is the heaviest report.
API keyrecommendedLegacy credentials
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
    }
  ]
}
ℹ️
Historical note: earlier documentation exposed the unit listing only under reports/properties, which read as an unrelated "report/…" endpoint. It is current and supported — it is the portfolio listing.

Field reference

GroupColumns
IdentityProperty_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)
StatusUnit_Type, Unit_Status, Unit_Publish_Status, Vacancy_Status, Days_Listed, Marketing_Start, Marketing_End, Date_Available — Unit_Status: Active / Archived
DetailsRental_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
ActivityLead_Status, Lead_Count, Applicant_Status, Applicant_Count, Lease_Status, Lease_Count, Total_Applications, Showings_Disqualified, Showings_Scheduled, Showings_Confirmed, Showings_Cancelled, Showings_Total
ContactContact_Name, Contact_Email, Contact_Phone
SyncUpdated_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
Prospect/applicant detail across the account — richer than GET /properties/leads: includes rating, tenancy-application and approval status.
API keyrecommendedLegacy credentials
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

GroupColumns
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
Showing/viewing activity per unit, including which team member ran it and the lead source. Read-only reporting. Looking up and rescheduling showings is in the General API tab.
API keyrecommendedLegacy credentials
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_ScopeWhat the booking link offersUnit shown on the row
unitOne unitThat unit
buildingThe whole building; the prospect picks a unit while bookingThe 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:

json
{
  "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

GroupColumns
UnitProperty_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
ShowingShowing_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)
LeadPerson_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)
SyncUpdated_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_ID is the unit's UUID in /reports/properties and the building's integer id in /reports/showings. Join units on Unit_ID.
  • Tenancy_Application_Status is Completed / Not_Completed (the form) in /reports/leads, and the document review status (Verified, Unverified, Incomplete, Reject, Not_Completed) in /reports/showings.
  • Unit_Status in /reports/properties is Active / Archived; Unit_status in /reports/showings is the unit's vacancy text.
  • Application_Status uses the partner names in /reports/leads and the dashboard names in /reports/showings (see "Lead stages: two sets of names" in section 11).
  • Dates in /reports/leads and /reports/showings are YYYY-MM-DD HH:MM:SS; Date_Available and Updated_At in /reports/properties are ISO 8601 with a T and 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.

EventAvailable inPayload coreFires when
unit.publishedSyndication APIproperty_id, marketplace, active, chargeA marketplace activation completes (charge included — no separate event)
unit.unpublishedSyndication APIproperty_id, marketplace, activeA deactivation completes
lead.receivedGeneral API + Syndication APIfull lead (same fields as /properties/leads)A new lead is created for your company (delivered within ~5 min)
application.stage_changedGeneral APIapplication_id, person_id, property_id, stage, previous_stage, archived, previous_archived, changed_atAn 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_changedGeneral APIapplication_id, person_id, decision, previous_decision, changed_atThe application is approved, denied, cancelled, or back to in_review
screening.status_changedGeneral APIcheck_id, application_ids, type, provider, status, previous_status, report_available, completed_atA screening check is ordered or changes status (for example in_progress → completed). Never the report itself
lease.status_changedGeneral APIlease_id, property_id, status, previous_status, archived, tenant_person_ids, sent_at, signed_atA lease is created or changes status (draft → sent_for_signatures → active) or is archived
document.status_changedGeneral APIdocument_id, lease_id, property_id, status, previous_status, signer_count, signed_count, sent_at, completed_atAn 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
ℹ️
General API: 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.

json
{
  "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
Creates or updates your single webhook (one per company).
API keyrecommendedLegacy credentials
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
Current config plus the last 20 deliveries so you can debug your receiver without asking us.
http
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):

json
{
  "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 in GET /webhooks).
  • Deliveries may arrive out of order and, rarely, more than once — de-dup by the id field (for leads, data.application_id also works).
  • lead.received starts with the first delivery run after you register; leads created before that run are not sent. History is not replayed: use GET /reports/leads (or GET /properties/leads on 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 /webhooks shows each delivery's status and the response code your URL returned.
⚠️
Sandbox only. 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

FieldTypeNotes
namestringUnit display name
addressstringStreet address
citystringCity name (resolved against Leasey's city catalog)
province_statestringe.g. "BC", "Ontario", "FL"
countrystring"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

IDBuilding Amenity
1Bike storage
2Concierge
3Electrical car charging station
4Fitness center / gym
5Hot tub
6Sauna
7Steam room
8Swimming pool
9Common areas
10Community laundry
11Elevator
12On-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.

⚠️
Defaults when omitted: 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
IDFeatureType
1Air conditioningAmenity
2Balcony / patioAmenity
3BasementAmenity
4Bike storageAmenity
5ConciergeAmenity
6DishwasherAmenity
7Electrical car charging stationAmenity
8FireplaceAmenity
9Fitness center / gymAmenity
10FurnishedAmenity
11HeatingAmenity
12Hot tubAmenity
13In-suite washerAmenity
14ParkingAmenity
15SaunaAmenity
16Steam roomAmenity
17Storage lockerAmenity
18Swimming poolAmenity
19CableUtility
20ElectricityUtility
21InternetUtility
22Natural gasUtility
23WaterUtility
24Common areasAmenity
25Community laundryAmenity
26ElevatorAmenity
27On-site managerAmenity
28In-suite dryerAmenity
29Food waste disposalAmenity
30FreezerAmenity
31MicrowaveAmenity
32OvenAmenity
33RefrigeratorAmenity
34StoveAmenity
35Garbage disposalUtility
36Satellite TVUtility
37SewageUtility

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

FieldValues
Unit_TypeApartment, House, Basement suite, Duplex/triplex, Townhouse, Other (a raw numeric code may appear on legacy units)
Unit_statusVacant, Occupied, On Hold, On Notice, Coming Soon
Unit_publish_statusPublished, Unpublished
Showing_StatusScheduled, Confirmed, Cancelled, Waitlisted, Disqualified (state 0, see the General API tab)
Showing_FeedbackPositive, Negative, Not Attended, No Response, Not Reconfirmed
Prequalification_ResultQualified, Not Qualified, null
Application_StatusThe lead's stage, with the dashboard's names (see the lead-stages table below)
TeamMemberRolMember, Admin (plus any custom role names your account defines)
Lead_sourceManual, Leasey, Kijiji, Rentsync, Zillow, Facebook, Apartments, Zumper, CompanyWebsite, PadMapper, NotIdentified, null

/reports/leads

FieldValues
Application_StatusNew 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_Rating0–5 (0 = not rated) or null
Tenancy_Application_StatusNot_Completed, Completed
Prequalification_ResultQualified, Not Qualified, null
Lead_Approval_StatusIn review, Approved, Denied, Cancelled
Unit_Publish_StatusPublished, Unpublished
Vacancy_StatusVacant, Occupied

/reports/properties

FieldValues
Unit_TypeApartment, House, Basement suite, Townhouse, Duplex/triplex, Other, Unknown
Unit_StatusActive, Archived
Unit_Publish_StatusPublished, Unpublished
Rental_SpaceEntire place, Partial place, Single room
Lease_TermFixed term, renewing month-to-month, Fixed term, Month to month, Unknown
CountryCanada, 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 yetNew LeadLeadnew_lead
Went through your booking link: booked a showing, joined the waitlist or answered the pre-qualification questionsBookedBookedbooked
Application startedApplication In ProgressApplyingapplication_in_progress
Application in reviewApplication In ReviewChecking Referencesapplication_in_review
Signing the leaseSigning LeaseSigning Leasesigning_lease
TenantActive TenantsActive Tenantactive_tenant
Archived, from any stageArchived LeadArchived Leadarchived: 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

StatusWhen it occurs
200Request processed successfully
201Property created successfully
400Missing required parameter, malformed JSON, invalid type
401Missing or invalid API key, or missing or invalid API token / company password
402No card on file, or the charge for a screening check could not be created (payment_method_required, payment_failed); nothing was ordered
403Read-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
404Property, application, lease, document or template not found, or it does not belong to your company
409Duplicate unit name (the body carries the existing property_id), idempotency request in progress, or last active key
413Report response exceeds the 6 MB cap
422Valid JSON but field-level validation failed (e.g. invalid enum value)
429Protective throttle: wait Retry-After seconds
500Database error or unexpected exception
503Temporarily unavailable
504The 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

codeHTTPWhen
unauthorized401Token or company password missing or wrong
invalid_api_key401The API key does not exist
api_key_revoked401The API key was revoked
api_key_expired401The API key passed its expiration date
api_key_read_only403A read-only key was used for a write
api_key_resource_not_allowed403The key is limited to other resources (section 2), or a limited key tried to create a key beyond its own resources
forbidden403The credential cannot do this (for example a suspended organization)
route_not_found403The method and path combination does not exist
bad_request400Malformed request (for example a property_id that is not a UUID)
missing_fields400POST /showings without a required field (listed in missing_fields)
missing_questions400POST /showings without an answer to a required question
waitlist_mode_required400The event only takes waitlist requests: send date_options
time_slot_unavailable400Someone took that time a moment ago
invalid_updated_since400updated_since is not an ISO-8601 date
updated_since_not_supported400updated_since on /reports/syndication
invalid_pagination400page below 1, or page_size outside 1–500, on the leasing routes (section G5)
invalid_parameter400A filter with a value that does not exist (the message lists the valid ones)
invalid_idempotency_key400The Idempotency-Key header has invalid characters
slot_not_found, invalid_slot_id, missing_slot_id, slot_cancelled, slot_unavailable400 / 404The showing id is wrong, cancelled or no longer available
not_found404The record does not exist or belongs to another account
application_not_found, lease_not_found, document_not_found404That record does not exist in your account
method_not_allowed405The path exists but not with that method
conflict409POST /properties with a name that already exists; last active key; organization slug or admin email taken
idempotency_request_in_progress409The first request with that Idempotency-Key is still running
payload_too_large413The report is over 6 MB: paginate (suggested_page_size)
validation_failed422Field errors, listed in details
invalid_expires_at422POST /api-keys with an expires_at that is not a future ISO 8601 date or date-time
invalid_resources422POST /api-keys with resources that is not a non-empty list of known resources
idempotency_key_required400Ordering a screening check without Idempotency-Key
payment_method_required, payment_failed402No card on file, or the charge could not be created; nothing was ordered
property_not_found, template_not_found404The unit or the e-signature template does not exist in your account
application_not_sendable409The application is archived or past application_in_progress
check_not_available, order_in_progress409That 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_created409The lease was sent for signatures (only archived changes), was already sent, or already has a draft document (document_id)
document_not_sendable, document_not_voidable409Only a draft can be sent, and only a document waiting for signatures can be voided
applicant_email_required, applicant_phone_required422Sending the application needs the applicant's email and phone
esign_unavailable503E-signature through the API is not enabled in that environment (the sandbox)
idempotency_key_reused422That Idempotency-Key was used for a different request
rate_limited429Protective throttle: wait Retry-After seconds
internal_error500Unexpected failure on our side: retry once, then contact us with the request_id
upstream_error502A dependency (for example Stripe) did not answer; retry
service_unavailable503Temporarily unavailable: retry later
updated_since_unavailable503Incremental sync storage missing on that environment. Not expected in production or the sandbox any more; listed for completeness
timeout504The 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.


Part 2
Choose your API

From here on the documentation is specific to one API. Pick the one you integrate with — the sidebar and the content below switch together.

Showing the documentation for: General API
You are reading the General API. Part 1 above also applies to you. Switch to the Syndication API →
General APIProperties

G1Properties API — import and update units

Data model

Company account (your company_id in Leasey) └── Unit / Property (property_id — one per rental unit) └── Leads (generated by showings and other lead sources)

Unit / Listing / Property are the same thing (a rental unit). The API uses property.

💡
Store the property_id returned on creation — it is the permanent key for everything else about that unit.
⚠️
Publishing to marketplaces, its pricing and billing are documented in the Syndication API tab. Leads leave Leasey through the Reports API and the lead.received webhook (Part 1).
POST
/properties
Creates a new rental unit. Only name, address, city, province_state and country are required — everything else is optional (see the Property Object reference).
API keyrecommendedLegacy credentials
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"
}
✅
Save property_id. It is required for every subsequent call about this unit.
  • POST /properties with a name that already exists in your account answers 409 Conflict with the existing property_id: update that unit with PUT /properties/{property_id}, or use a different name.
  • Buildings created via building_name inherit the unit's location (address, city, province, country, postal code, coordinates).
  • Team members created through the API are Admin by default; send role by name, role_id or is_admin: false for anything else (see the Property Object Reference).
PUT
/properties/{property_id}
Updates an existing unit. Send only the fields you want to change; everything else is untouched.
API keyrecommendedLegacy credentials
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

FieldBehavior on update
photosReplaces the full photo set
features, restrictionsReplace the full list ([] clears)
everything elsePartial 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.

Book a new showing Manage an existing one -------------------------------------- ---------------------------------------- 1. GET /showings/lookup 1. GET /showings?email= or ?phone= -> event_id -> the person's active showings 2. GET /showings/questions 2. (caller picks one -> slot_id) -> what the event asks 3. POST /showings/{slot_id}/confirm 3. GET /showings/availability POST /showings/{slot_id}/cancel -> real open times PUT /showings/{slot_id}/reschedule 4. POST /showings (after a fresh availability read) -> slot_id 4. GET /showings/{slot_id} -> status (waitlist unit? send date_options)
ℹ️
A booking is complete after 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.
GET
/showings/lookup
Find a showing event from anything the caller says: unit name, address, slug, building name, or a known unit_id.

Query Parameters

ParameterTypeRequiredDescription
company_idintegerYesYour company identifier
querystringOptionalFree-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_idUUIDOptionalExact match
page / page_sizeintegerOptionalDefault 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.

json — example response 200 OK
{
  "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.

json — a waitlist unit in the catalog
{
  "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
Events can carry questions the person must answer before booking, some of them required. Fetch them up front so your agent knows what to ask.
API keyrecommendedLegacy credentials
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>
json — example response 200 OK
{
  "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

GET
/showings/availability
Returns the dates/times that are actually bookable right now, computed server-side — availability windows, team-member load, existing bookings, external calendar blocks, minimum notice, rolling-day range and per-day caps are all applied.
  • 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_to narrow 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. 404 here means "this unit isn't open for booking", not "no times available". /showings/lookup lists published events only.

Query Parameters

ParameterTypeRequiredDescription
company_idintegerYesYour company identifier
event_idintegerYesFrom Find Bookable Units
date_from / date_todateOptionalNarrow the window; at most 30 days are scanned per call
API keyrecommendedLegacy credentials
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>
json — example response, trimmed 200 OK
{
  "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.

⚠️
Always re-read availability right before booking: slots are first-come, first-served.

Book a Showing

POST
/showings
Schedule a showing. The team member is assigned automatically; never send one.

Body Fields

FieldRequiredNotes
event_idYesFrom /showings/lookup
firstname, lastnameYesThe person visiting
emailYesThe lead's identity and the key to find the showing later (GET /showings?email=)
phoneYesWithout it the API answers 400 with missing_fields: ["phone"]. It also works for lookups
dial_codeNoDefaults to +1
date, timeYes (normal events)YYYY-MM-DD and 24h HH:MM, from /showings/availability
unit_idBuilding-level events onlyWhen the person chose a specific unit of a building (bookshowingby: 1)
answersIf the event has required questions{question_id, value}; Dropdown values must be one of options
API keyrecommendedLegacy credentials
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

json
{
  "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_id is the same lead id you see in /properties/leads and /reports/leads: a showing booked here is a lead in Leasey.
  • is_update: true means this person already had a slot for this event and it was moved instead of duplicated.
  • is_disqualified: true means 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). /confirm is 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

POST
/showings (with date_options)
Join the waitlist. When a unit has is_waitlist: true it has no bookable times: the person leaves up to three preferred dates and the team reaches out.

Same endpoint and auth; swap date + time for date_options:

API keyrecommendedLegacy credentials
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
Use this before checking a status or rescheduling: the person does not know their slot_id. Identify them by email or phone; you get their pending/confirmed showings and waitlist entries (cancelled ones are excluded).
API keyrecommendedLegacy credentials
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.

json — example response 200 OK
{
  "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/{slot_id}
Re-check status at any point — for example after a write timed out — before retrying an action.
API keyrecommendedLegacy credentials
GET /showings/54949
Authorization: Bearer lsy_live_…
GET /showings/54949?company_id=128
Authorization: Bearer <YOUR_API_TOKEN>
X-Company-Password: <YOUR_COMPANY_PASSWORD>
json — example response 200 OK
{
  "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
Read the availability endpoint first for the new time. Re-runs team-member assignment and re-sends notifications; the slot returns to pending.
API keyrecommendedLegacy credentials
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.

ℹ️
The showing keeps its id. A reschedule moves the same 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
Sets the showing to cancelled (state 3) and notifies the team. Works on waitlist entries too.
API keyrecommendedLegacy credentials
POST /showings/{slot_id}/cancel
Authorization: Bearer lsy_live_…
{ "company_id": 128, "password": "<YOUR_COMPANY_PASSWORD>" }

Response 200 OK

json
{ "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

POST
/showings/{slot_id}/confirm
Sets the showing to confirmed (state 2) and notifies the team. Optional: a showing is already valid in pending after schedule or reschedule.

Same body as cancel. Call it only when the person explicitly says they will attend.

Response 200 OK

json
{ "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 /showings is 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} (or GET /showings by contact) before retrying.

Errors specific to showings

StatusWhenWhat to do
400Missing 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
401Bad key, token or company credentialsConfiguration error on your side — do not retry
404Event or showing not found, not published, or not owned by your accountRe-run lookup / list; tell the person you couldn't find it
500Unexpected server errorRetry 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 options before sending.
⚠️
Never act on a slot the caller did not clearly choose, and always confirm the specific appointment verbally before a cancel or reschedule.

G4Tutorial — find a showing, then confirm, reschedule or cancel it

The caller never knows their slot_id — always find it first by contact.

1 -> GET /showings?email=jane.prospect@example.com <- showings: [{ slot_id: 54949, display_name: "Maple Court Unit 2", date: "2026-08-31", time: "14:00:00", status: "pending" }] 0 -> "I don't see any upcoming showings under that email/number" many -> read them back (date + property) and let the caller pick one

Then, on the slot the caller clearly chose (confirm it verbally first):

Caller saysCallResult
"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) and page_size (default 100, max 500). The body carries count, total_rows, data and pagination (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_at ascending, then id).
  • Types: ids are integers (units are UUID strings), money is a number, booleans are true/false, timestamps are ISO 8601 in UTC with Z, and lease dates are YYYY-MM-DD. Every field is always present; an unknown value is null.
  • Filters take the codes below exactly; any other value answers 400 invalid_parameter with 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)
http
GET /applications/130625
Authorization: Bearer lsy_live_…
json
{
  "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.

  • stage uses the same stages as Application_Status in /reports/leads (stage_label carries that report's name). Archiving is separate: archived: true with the stage the application had, where the report says Archived Lead.
  • decision is in_review until someone approves, denies or cancels the application in Leasey (Lead_Approval_Status in the report).
  • person_id is the lead: the same id as Person_ID in the reports and person_id in a lease's tenants.
  • updated_at is 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_ids lists them all. A check ordered for an applicant with no application in your account has application_ids: [].
  • failed means 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
json
{
  "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_id is that tenant's application for the lease's unit, when there is one: that is how a lease connects to the applications above (and lease_ids there points back).
  • Money is a number; a value that was never entered is null, an explicit zero is 0.

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

  1. First load: page through each list without filters (page_size=500).
  2. Then, every few minutes: call the same lists with updated_since set to the highest updated_at you 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.
  3. Or subscribe to application.stage_changed, application.decision_changed, screening.status_changed, lease.status_changed and document.status_changed (section 9 Webhooks API) and fetch the record by id when an event arrives. Keep a daily updated_since pass 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
http
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.

http
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_failed with one entry per field in details ({"field": "decision", "message": …}).
  • No email goes to the applicant when the stage or decision changes (the dashboard sends none either). application.stage_changed and application.decision_changed (section 9 Webhooks API) tell your other systems.
  • /send works from new_lead, booked or application_in_progress and not archived; from a later stage it would move the application back, so it answers 409 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:

  1. The price is your account's price for that check, never a value you send. A price of 0 orders without a charge; a check with no price set answers 409 check_not_available.
  2. The charge is a Stripe invoice to the card on file (402 payment_method_required without a card, 402 payment_failed if the charge cannot be created). Nothing is ordered without the charge.
  3. The check starts at waiting_payment, moves to in_progress and then completed (screening.status_changed, section 9 Webhooks API). The report stays in the Leasey dashboard; report_available says 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.

http
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
http
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 }
}
  • 201 with the lease, as in Leases. Required: property_id, tenants (person_id of people with an application in your account), start_date and rent_amount. Money is a number, dates are YYYY-MM-DD, renewal and rent_frequency take the codes of Leases.
  • Terms and tenants change while the lease is not_created or draft. Once it was sent for signatures only archived changes (409 lease_not_editable).
http
POST /leases/219/send
Authorization: Bearer lsy_live_…
Content-Type: application/json

{ "template_id": 31 }
  • 201 with the document (E-signature Documents) at waiting_signatures; the lease is now sent_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 completed and the lease active: document.status_changed and lease.status_changed (section 9 Webhooks API).
  • /void cancels the signing and marks the document void; the lease goes back to draft, 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_error with that document_id: send it with POST /documents/{document_id}/send. While the lease has a draft document, POST /leases/{lease_id}/send answers 409 document_already_created with its document_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

TermMeaning
Unit / Listing / PropertyThe 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
WaitlistEvent mode with no bookable times — people leave preferred dates instead (shown as is_waitlist: true)
Team memberThe 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-Id logged on every response, so support can find any request
  • Writes send an Idempotency-Key and handle Idempotent-Replayed
  • Reports synced with updated_since, de-duplicated by Application_ID / Showing_ID / Property_ID / Billing_ID
  • GET /showings/lookup — catalog loaded or search wired; 0/1/many results handled
  • GET /showings/questions — required questions asked before booking
  • GET /showings/availability — real open times read back before POST
  • POST /showings — schedule; is_update and time_slot_unavailable handled
  • Waitlist units (is_waitlist: true) booked with date_options
  • GET /showings?email|phone → caller picks → confirm / cancel / reschedule by slot_id
  • Error handling: 401 (credentials), 404 (wrong unit), 422 (details array)
  • Passwords with special characters URL-encoded in query strings
  • POST /properties — create a test unit, store property_id
  • PUT /properties/{id} — update; field replace-vs-partial semantics understood
  • GET /reports/* wired into your dashboard, with pagination for large accounts
  • PUT /webhooks registered for lead.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-Password header 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_date on showings and leads reports
  • Design for a 60-second client timeout

🔄 Reliability

  • Retry 5xx with exponential backoff (2 s, 4 s, 8 s); never retry 4xx
  • Read state before retrying a write that timed out — writes are idempotent, but confirm rather than assume
  • Persist slot_id and application_id as 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 options before sending

📦 Data Consistency

  • photos, features, restrictions are replaced on update — send the full current set
  • team_members on update is additive; remove from the Leasey dashboard
You are reading the Syndication API. Part 1 above also applies to you. Switch to the General API →

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.

  1. Create the unit — POST /properties with at least name, address, city, province_state, country. Store the returned property_id — it is the permanent key for everything else.
  2. Show the marketplaces with prices — GET /properties/{property_id}/marketplaces. Render the 23 entries with pricing_model, daily_rate and first_month_price — that is the exact cost to display in your confirmation dialog. Free marketplaces show 0.
  3. Your user toggles and confirms in your portal. Leasey knows nothing until step 4.
  4. Publish in one call — PUT /properties/{property_id} with the marketplaces array. Each entry comes back with status (published / unpublished / no_change / error) and, for paid activations, a charge object: {type: "first_month", amount, currency, period_days: 30}. Show that amount to your user — it is what will be billed.
  5. 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 (pending until the next billing run). On disable: the flag is turned off, an unpublished log 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.
  6. See it in the admin view — GET /reports/syndication: one row per unit × marketplace with Active, Published_Since and amounts.
⚠️
Common mistakes: sending an alias like 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

Your portal Leasey Stripe | | | |-- GET /marketplaces --->| | |<-- kijiji $0.74/day ----| (price shown to | | first month $22.20 | your user BEFORE) | | | | |-- PUT {kijiji: on} ---->| | |<-- charge: $22.20 ------|-- billing item (pending) ->| | (confirmed in the | invoiced on the next | | same response) | billing run | | | | |-- GET /reports/billing->| | |<-- status: invoiced ----|<-- paid_at when settled ---|
💡
Key facts: one dedicated partner account — your platform holds a single Leasey account (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)

  1. Every 5–15 minutes call GET /properties/leads?start_date=<last_sync> with your last successful sync timestamp.
  2. For each lead, route it with property_id → your unit → your client.
  3. De-duplicate with application_id (stable per lead). The same id coming back with a new status is a status update, not a new lead.
  4. Attribute with source (kijiji, zumper, apartments…) — this is also how you show your clients which marketplace their spend converts on.
  5. Store the new last_sync = the newest date_created you processed.

Push (lower latency, optional)

  1. PUT /webhooks with your HTTPS URL and events: ["lead.received"] — store the secret from the response; it is shown once.
  2. Verify the webhook-signature header on every delivery and answer 2xx within 8 s.
  3. The payload's data has 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.

  1. GET /reports/billing, optionally with start_date/end_date on when charges were created, or status=invoiced.
  2. Each row is one charge: Billing_Type (first_month · recurring · final_proration), the covered Period_Start/Period_End, Days, Daily_Rate, Amount, Currency, Status, and Stripe_Invoice_ID with Invoiced_At / Paid_At.
  3. Match Stripe_Invoice_ID to your Stripe invoices; anything pending is not yet invoiced, anything failed was rejected by Stripe and is being retried.
  4. Use the summary for control: outstanding (not yet collected), current_daily_run_rate (what your live paid listings cost per day, today) and projected_monthly (×30), by_marketplace (where the money goes), active_paid_listings.
  5. For the per-unit view — what is live and since when — use GET /reports/syndication.

S2Properties API (Syndication)

Data model

Partner account (your company_id in Leasey) └── Unit / Property (property_id — one per rental unit you syndicate) ├── Marketplace activations (per-marketplace on/off + billing) ├── Leads (generated by marketplaces, pulled via API) └── Billing items (first month / recurring / proration)

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.

RuleDetail
First monthBilled in full on activation: daily_rate × 30, regardless of how many days the listing stays live that month.
From day 31The daily rate is billed for each day the listing remains live, invoiced in 30-day cycles.
On deactivationDays 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 marketplacesNever generate any charge.
Re-sending the same stateIdempotent: 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

  1. POST /properties → create the unit in Leasey, store property_id
  2. GET /properties/{id}/marketplaces → show your user the marketplaces WITH prices
  3. (your user toggles marketplaces and confirms in YOUR portal)
  4. PUT /properties/{id} with marketplaces → Leasey publishes and returns the charges applied
  5. GET /properties/leads → pull leads generated by the marketplaces
  6. GET /reports/syndication → admin view: everything live + everything billed
  7. GET /reports/billing → charge-by-charge ledger for reconciliation
  8. PUT /webhooks (optional) → push: receive leads & publish events instead of polling
💡
One dedicated partner account. Your platform holds a single Leasey account (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
Creates a new rental unit. Only name, address, city, province_state and country are required — everything else is optional (see the Property Object reference).
API keyrecommendedLegacy credentials
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
    }
  ]
}
✅
Save property_id. It is required for every subsequent call about this unit. charge: null means the marketplace is free — nothing was billed.
  • POST /properties with a name that already exists in your account answers 409 Conflict with the existing property_id: update that unit with PUT /properties/{property_id}, or use a different name.
  • Buildings created via building_name inherit the unit's location (address, city, province, country, postal code, coordinates).
  • Team members created through the API are Admin by default; send role by name, role_id or is_admin: false for anything else (see the Property Object Reference).

Update Unit

PUT
/properties/{property_id}
Updates an existing unit. Send only the fields you want to change; everything else is untouched.
API keyrecommendedLegacy credentials
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

FieldBehavior on update
photosReplaces the full photo set
features, restrictionsReplace the full list ([] clears)
marketplacesOnly the listed entries change; others keep their state
everything elsePartial 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
Returns every marketplace with its current state for this unit and its prices — everything you need to render your syndication screen with costs shown before your user confirms.
http — API key (recommended)
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.

ℹ️
Names are canonical. The API also accepts legacy aliases as input and normalizes them everywhere (e.g. 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}
Publishing and unpublishing marketplaces is a PUT on the unit with the marketplaces array — batch as many toggles as your user confirmed in a single call.
API keyrecommendedLegacy credentials
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

statusMeaningcharge
publishedActivated and pushed to the marketplace feedfirst_month object for paid, null for free
unpublishedDeactivated; removed from the feednull (proration handled on the next billing run)
no_changeAlready in the requested state — nothing billedabsent
errorThis entry failed (message included); others still processedabsent

Notifications: the unit's contact email receives a notification on deactivation (webhooks push these events too).

Leads

GET
/properties/leads
Leads generated on the marketplaces flow into Leasey and are pulled here. Every lead carries the property_id and the source — which marketplace produced it.
http — API key (recommended)
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

  1. Poll GET /properties/leads?start_date=<last_sync_time> every 5–15 minutes with your last successful sync timestamp
  2. Route each lead to your client using property_id → your unit mapping
  3. De-duplicate by application_id (stable per lead). A lead re-appearing with a new status is a status update, not a new lead
  4. 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).

⚠️
Whether a marketplace is paid — and its exact rate — comes from your account's pricing. Always read it from 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

KindHow it authenticatesWhat it can do
Direct client, shared tokenShared token + company_id + company passwordThe whole API, only on its own company
Direct client with an API keyAuthorization: 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/json when the request has a body.
  • The company always goes in the URL (/org/companies/64/…). The organization never sends company_id or a password in the body.
  • A suspended organization answers 403 on everything.
🔒
Your first key reaches you from Leasey, and it is shown only once. Rotate it as soon as it arrives: create a new key with 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

http
GET /org
Authorization: Bearer lsy_org_live_…
json
{ "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 Authorization header, never in the URL. Works on every API route. If the request also carries company_id or password, they are ignored: the key only acts on its own company.
  • Read only: any POST, PUT or DELETE answers 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 carries status (active, expired or revoked) and expires_soon (true in the last 7 days, while the key still works).
  • Rotate without the dashboard: GET, POST and DELETE /api-keys with a full-access key.
http — the same call, with the shared token vs. with a 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

EnvironmentBase URLStripe
Sandboxhttps://sandbox.api.leasey.ai/v1Test mode: card 4242 4242 4242 4242, no real money
Productionhttps://api.leasey.ai/v1Live mode

The way to connect is identical in both environments: same routes, same headers, same bodies. Only three things change:

What changesSandboxProduction
Base URLhttps://sandbox.api.leasey.ai/v1https://api.leasey.ai/v1
Credential valueslsy_test_…, lsy_org_test_…lsy_live_…, lsy_org_live_…
StripeTest mode, test cardsLive mode, real cards
⚠️
Use the base URL exactly as shown, including /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.

StepWhatCall
1Check the keyGET /org
2Create a company for each clientPOST /org/companies
3Save the company's card (Stripe link)POST /org/companies/{company_id}/payment-setup-link
4Confirm the company is readyGET /org/companies/{company_id}
5Add units (listings)POST /org/companies/{company_id}/properties
6See which marketplaces exist and their priceGET /org/companies/{company_id}/properties/{property_id}/marketplaces
7Turn marketplaces on or off for one unitPUT /org/companies/{company_id}/properties/{property_id}
8Same thing for many units at oncePOST /org/marketplaces/bulk → GET /org/jobs/{job_id}
9See how much is spent (organization and per company)GET /org/reports/billing
10See what each unit has listed and whereGET /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

GET
/org
Returns your organization and how many companies it has.
json — response 200 OK
{ "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

POST
/org/companies
Creates the company, its administrator and its Stripe customer in one step.
json — example request body
{
  "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." ] }
✅
Keep the company_id — every call for this client uses it.

What Leasey creates in that one step

WhatDetail
The companyWith its denial and cancellation reasons, like a company created by Leasey
Its administratorAdministrator role. No password and no welcome email
Its Stripe customerLabelled with the organization; the end client never sees it
The link to the organizationMarked as "created by the organization"
Dashboard viewReduced (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

PartFields
companycompany_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)
adminemail, 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/{company_id}
One company with its readiness. GET /org/companies lists them all.
json — response 200 OK
{ "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:

http
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

MethodPathWhat it does
POST/org/companiesCreate 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}/onboardingFinishes 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 pathSame as
POST /org/companies/{id}/properties · PUT …/properties/{property_id}POST / PUT /properties
GET …/properties/{property_id}/marketplaces · GET …/properties/leadsthe same routes in the Syndication API
GET …/showings/lookup · /questions · /availability · GET/POST …/showings · GET …/showings/{slot_id} · PUT …/reschedule · POST …/cancel · /confirmthe Showings API (General API tab)
GET …/reports/{showings,leads,properties,syndication,billing}GET /reports/…
GET · PUT …/webhooksGET / PUT /webhooks

O5Units

O5.1 Add units (listings) to a company

Add Units

POST
/org/companies/{company_id}/properties
Creates a unit for that company. Edit it later with PUT on the same path plus /{property_id}.
json — example request body
{ "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 property object (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:

http
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

GET
/org/companies/{company_id}/properties/{property_id}/marketplaces
The catalog of one unit: which marketplaces exist for that company, whether they are paid or free, their price and whether the unit already has them on.
json — response, truncated 200 OK
{ "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 USDapartmentsPremium, apartmentsStandard
zumperStandard 0.30 → 9.00 USDfacebook, lespac, listanza, lq
zumperPremium 0.50 → 15.00 USDrealtor, rentFaster, rentPanda, rentSeeker
livRent 0.61 → 18.30 USDrentalBeast, rentalSource, rew
kijiji 0.74 → 22.20 USDstreetEasy, 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: false means the company has no card yet: paid marketplaces will be refused.
⚠️
Testing: 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

PUT
/org/companies/{company_id}/properties/{property_id}
Send only the marketplaces that change. Several in one call is fine.
json — example request body
{ "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. …" } ] }
⚠️
The HTTP status is 200 even when a marketplace fails: always read the 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 marketplaceMeaningCharge
publishedTurned onFirst month (paid marketplaces)
unpublishedTurned offDays past the last billed period, at the next billing run
no_changeIt was already in that stateNone
error + code: payment_method_requiredPaid marketplace and the company has no cardNone
error + message: Unknown marketplace.Misspelled name (see §O6.1)None

O6.3 Many units at once (bulk)

Bulk Marketplaces

POST
/org/marketplaces/bulk
One list of marketplaces applied to many units, in the background. Always simulate first with dry_run.
json — dry run request
{ "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):

http
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

FilterEffect
company_idsOnly those companies
building_idsOnly units of those buildings
property_idsExactly those units
published_onlyDefault 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_ids or building_ids.
  • To turn off in bulk: the same call with "enabled": false.
  • GET /org/jobs lists 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 pending and are never collected.
  • The organization sees its own money with GET /org/reports/billing (§O7.3).
RuleDetail
First monthBilled in full on activation: daily_rate × 30
From day 31The daily rate is billed every 30 days while the listing stays live
On deactivationDays beyond the last billed period are prorated. Inside the first 30 days, no refund
Free marketplacesNever generate any charge
Re-sending the same stateIdempotent: no_change, no duplicate charge

O7.2 Save the company's card — the Stripe link

Card Payment Link

POST
/org/companies/{company_id}/payment-setup-link
Returns a Stripe-hosted page where the client saves its card. Card details never pass through Leasey or your system.
json — example request body
{ "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

  1. The organization sends that URL to its client (the company), by email or from its own system.
  2. The client enters the card on Stripe's page. In the sandbox: 4242 4242 4242 4242, any future date, any CVC.
  3. Stripe saves the card on that company's Stripe customer, and Leasey makes it the default for its invoices.
  4. 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.

Organization Leasey Stripe | POST payment-setup-link | | |------------------------->|---- page for the company ---->| |<-------- URL ------------| (its own Stripe customer) | | sends the URL to its client --- client enters card --->| card on that customer | | card saved on the company | | GET marketplaces | | |------------------------->| billing.ready: true |

O7.3 How much is being spent

Billing Report

GET
/org/reports/billing
One call answers both: the whole organization and each company.
http
GET /org/reports/billing?start_date=2026-10-01
json — response, trimmed 200 OK
{ "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

QuestionField
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 invoicedata[]

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).

StatusMeaning
pendingCharge created, not yet invoiced. In the sandbox everything stays here (the billing job is off)
invoicedInvoice created in Stripe against the company's card (Stripe_Invoice_ID)
paidPaid
failedThe card was declined; it counts as outstanding
Billing_TypeWhen
first_monthOn turning a paid marketplace on: daily rate × 30, in full
recurringEvery 30 days while it stays on
final_prorationOn 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.

⚠️
Turning a marketplace off inside its first 30 days does not refund the first month.

O8Reports

O8.1 What each company has listed, and where

Syndication Report

GET
/org/companies/{company_id}/reports/syndication
One row per unit × marketplace, with whether it is on now, since when, and how much it has been billed.
json — response 200 OK
{ "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}

typeWhat
propertiesEvery unit, with status, rent, leads, showings and its Leasey page (§O5.1)
syndicationWhat is listed where and how much it cost (this section)
billingEvery charge (§O7.3)
leadsLeads received
showingsShowings booked

O9Quick Reference — "I want to know…"

I want to know…CallLook at
Whether a company can turn on paid marketplacesGET /org/companies/{id}/properties/{property_id}/marketplacesbilling.ready
Which units a company hasGET /org/companies/{id}/reports/propertiesdata[]
Which marketplaces exist and what they costGET /org/companies/{id}/properties/{property_id}/marketplacesmarketplaces[]
What is listed where, for one companyGET /org/companies/{id}/reports/syndicationdata[] (Active) and summary
How much my whole organization spentGET /org/reports/billingtotals_by_currency
How much each company spentGET /org/reports/billingby_company[]
How much one company spent, charge by chargeGET /org/reports/billing?company_id={id}data[]
How much is still unpaidGET /org/reports/billing?status=pending (or failed)totals_by_currency
How much it will cost per month at today's paceGET /org/reports/billingby_company[].summary.projected_monthly
How a bulk job wentGET /org/jobs/{job_id}succeeded, failed, results[]
What was done with my keysGET /org/audit?limit=100events[]

O10Keys & Maintenance

ToCall
Rotate its keyPOST /org/keys (new key) → switch it in your system → DELETE /org/keys/{old_id}
See its keysGET /org/keys
See what was done with its keysGET /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 profilePATCH /org/companies/{id} (O4.3)
Finish something left pendingPOST /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

StatusMeaning
401Missing, wrong, revoked or expired organization key
403Organization suspended
404Unknown route, or the company is not in your organization (whether it exists or not)
409Conflict (slug or admin email taken, last active key)
422Validation failed; see details
502Stripe did not answer; retry
503Organizations 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:

  1. Check the key (GET /org) and create a company (§O4.1, §O4.2).
  2. Open the payment link and save the test card 4242 4242 4242 4242, any future date, any CVC (§O7.2). Confirm ready: true (§O4.3).
  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 answers no_change, so nothing is charged twice.
  4. Turn the marketplace off, and read the billing and syndication reports (§O7.3, §O8.1).
  5. Rotate the organization key: create a new one, revoke the old one, and confirm the old one now answers 401.
⚠️
Be careful
· 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 todayWhat to do
The webhook payload does not include company_idRegister 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 callCall 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 unpublishedSend "published_only": false
The payment_method_required message says to add a card in Settings → BillingFor 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 APIThe city is saved on the unit (used for the listing and its location); the report reads an older field
Saving a cardLeasey 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 APIAsk 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.


Syndication APIGuides & Reference

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

TermMeaning
Unit / Listing / PropertyThe rental unit being syndicated (one property_id)
MarketplaceExternal portal: Zillow, Kijiji, Facebook Marketplace, Apartments.com, Zumper, etc.
SyndicationPublishing a unit to one or more marketplaces via Leasey's feeds
ActivationTurning a marketplace on for a unit (may create a charge)
Daily ratePer-day price of a paid marketplace, set per partner account
First monthdaily_rate × 30, billed in full on activation
ProrationPartial-period billing at the daily rate after deactivation
Partner accountYour 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-Id logged on every response, so support can find any request
  • Writes send an Idempotency-Key and handle Idempotent-Replayed
  • Reports synced with updated_since, de-duplicated by Application_ID / Showing_ID / Property_ID / Billing_ID
  • POST /properties — create a test unit, store property_id
  • GET /properties/{id}/marketplaces — render your syndication screen with prices
  • Confirmation dialog shows first_month_price before publishing
  • PUT /properties/{id} with marketplaces — publish; surface the charge confirmation to your user
  • Handle no_change (idempotent re-publish) and per-entry error statuses
  • Unpublish flow — surface the proration note
  • GET /properties/leads polling wired into your CRM
  • GET /reports/syndication powering your admin dashboard
  • GET /reports/billing reconciled against your own invoices (check outstanding and current_daily_run_rate)
  • PUT /webhooks registered; 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-Password header 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_date on 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 5xx with exponential backoff (2 s, 4 s, 8 s); never retry 4xx
  • Read state before retrying a write that timed out — writes are idempotent, but confirm rather than assume
  • Persist property_id on 201 immediately
  • Treat every field as nullable

📋 Data Consistency

  • photos, features, restrictions are replaced on update — send the full current set
  • team_members on update is additive; remove from the Leasey dashboard
  • Marketplace names are canonical; aliases are accepted but normalized

Common to both APIsSupport & changelog

13Support

Before reporting an outage, check https://status.leasey.ai.

📧
Contact your Leasey technical contact with the 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

VersionDateChanges
3.182026-11-06New: 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.172026-11-06New: 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.162026-11-06New: 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.152026-11-06New: 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.142026-11-05New: 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.132026-11-05New: 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.122026-11-04New: 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.112026-11-03New: 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.102026-11-03Fixed: /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.92026-10-29New: 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.82026-10-29New: 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.72026-10-21Fix: 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.62026-09-22Fix: 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.52026-09-18Team members created through the API are Admin by default — no role needed. Send role, role_id or is_admin: false for anything else
3.42026-09-11Team 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.32026-08-27API-triggered publish/unpublish fires the same notification flow as the platform UI
3.22026-08-27Buildings 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.12026-08-27Payment-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.02026-08-26Complete guide: single document for every external endpoint
1.22026-08-26Money-flow diagram, lead.received webhook example, operational limits table, FAQ, PII anonymized in examples, full field catalogs
1.12026-08-26Billing ledger (/reports/billing), webhooks implemented, pagination + 413, real 422 and GET /webhooks examples, Property Object reference completed against the code
1.02026-08-26Initial partner guide: catalog with prices, charge confirmation, consolidated report, leads with property_id/source

Leasey API v3.18 — © 2026 Leasey.ai. All rights reserved.

Watch Demo Video

Share your name and email and you’ll get instant access to a 2-minute demo video showcasing exactly how Leasey.AI can revolutionize your lease management.