# Leasey API Documentation **Version:** 3.18 · **API:** `v1` · **Audience:** property-management platforms, voice and chat agents, and reporting tools · **Format:** JSON over HTTPS One document for **everything** Leasey exposes to external systems. It is organized by what you want to do: fundamentals first, then step-by-step tutorials, then the reference for every endpoint, then catalogs and appendices. Every endpoint shows the full request **and** a real response captured from production (personal details anonymized). > **Machine-readable spec:** [`openapi.yaml`](https://www.leasey.ai/api/openapi.yaml) — import it into > Postman, Insomnia or Swagger UI to get a ready-to-use collection of every > endpoint below. ## Table of contents - **[Part 0 — Start here](#part-0-start-here)** - [0.1 What the API lets you do](#01-what-the-api-lets-you-do) - [0.2 Who uses what](#02-who-uses-what) - [0.3 What do you want to do?](#03-what-do-you-want-to-do) - [0.4 How this guide is organized](#04-how-this-guide-is-organized) - **[Part 1 — Fundamentals](#part-1-fundamentals)** - [1.1 Credentials and where to get them](#11-credentials-and-where-to-get-them) - [1.2 Authentication](#12-authentication) - [API keys (recommended)](#api-keys-recommended) - [Legacy credentials (token + company password)](#legacy-credentials-token-company-password) - [1.3 Environments and sandbox](#13-environments-and-sandbox) - [1.4 Conventions: JSON, dates, data types, pagination, limits](#14-conventions-json-dates-data-types-pagination-limits) - [1.5 Errors and troubleshooting](#15-errors-and-troubleshooting) - [Troubleshooting by symptom](#troubleshooting-by-symptom) - [1.6 Your first request (5 minutes)](#16-your-first-request-5-minutes) - [1.7 Retries and Idempotency-Key](#17-retries-and-idempotency-key) - **[Part 2 — Step-by-step tutorials](#part-2-step-by-step-tutorials)** - [2.1 Publish a unit and see exactly what it costs](#21-publish-a-unit-and-see-exactly-what-it-costs) - [2.2 Get the leads back into your platform](#22-get-the-leads-back-into-your-platform) - [2.3 Book a showing end to end (voice / chat agent)](#23-book-a-showing-end-to-end-voice-chat-agent) - [2.4 Manage an existing showing (confirm, reschedule, cancel, status)](#24-manage-an-existing-showing-confirm-reschedule-cancel-status) - [2.5 Reconcile billing against your own invoices](#25-reconcile-billing-against-your-own-invoices) - [2.6 Pull the reports for a dashboard or CRM](#26-pull-the-reports-for-a-dashboard-or-crm) - **[Part 3 — Syndication reference](#part-3-syndication-reference)** - [3.1 Data model](#31-data-model) - [3.2 Pricing and billing model](#32-pricing-and-billing-model) - [3.3 Create a unit — POST /properties](#33-create-a-unit-post-properties) - [3.4 Update a unit — PUT /properties/{property_id}](#34-update-a-unit-put-propertiesproperty_id) - [3.5 Marketplace catalog and prices — GET /properties/{property_id}/marketplaces](#35-marketplace-catalog-and-prices-get-propertiesproperty_idmarketplaces) - [3.6 Publish and unpublish marketplaces](#36-publish-and-unpublish-marketplaces) - [3.7 Leads — GET /properties/leads](#37-leads-get-propertiesleads) - **[Part 4 — Showings reference](#part-4-showings-reference)** - [4.1 How booking works](#41-how-booking-works) - [4.2 Find bookable units — GET /showings/lookup](#42-find-bookable-units-get-showingslookup) - [4.3 Pre-qualification questions — GET /showings/questions](#43-pre-qualification-questions-get-showingsquestions) - [4.4 Availability — GET /showings/availability](#44-availability-get-showingsavailability) - [4.5 Schedule a showing — POST /showings](#45-schedule-a-showing-post-showings) - [4.6 Join the waitlist — POST /showings with date_options](#46-join-the-waitlist-post-showings-with-date_options) - [4.7 List a person's showings — GET /showings](#47-list-a-persons-showings-get-showings) - [4.8 Showing status — GET /showings/{slot_id}](#48-showing-status-get-showingsslot_id) - [4.9 Reschedule — PUT /showings/{slot_id}/reschedule](#49-reschedule-put-showingsslot_idreschedule) - [4.10 Cancel — POST /showings/{slot_id}/cancel](#410-cancel-post-showingsslot_idcancel) - [4.11 Confirm — POST /showings/{slot_id}/confirm](#411-confirm-post-showingsslot_idconfirm) - [4.12 Idempotency, errors and dialogue rules](#412-idempotency-errors-and-dialogue-rules) - **[Part 5 — Reports reference](#part-5-reports-reference)** - [5.1 Consolidated syndication report — GET /reports/syndication](#51-consolidated-syndication-report-get-reportssyndication) - [5.2 Billing ledger — GET /reports/billing](#52-billing-ledger-get-reportsbilling) - [5.3 Properties, leads and showings reports — GET /reports/{properties|leads|showings}](#53-properties-leads-and-showings-reports-get-reportspropertiesleadsshowings) - [5.4 Field values in the reports (real, from production data)](#54-field-values-in-the-reports-real-from-production-data) - [5.5 Column reference](#55-column-reference) - **[Part 6 — Webhooks reference](#part-6-webhooks-reference)** - [6.1 Events](#61-events) - [6.2 Register your endpoint — PUT /webhooks and GET /webhooks](#62-register-your-endpoint-put-webhooks-and-get-webhooks) - [6.3 Delivery format and signature](#63-delivery-format-and-signature) - [6.4 Retries and reliability](#64-retries-and-reliability) - [6.5 Testing your webhook](#65-testing-your-webhook) - **[Part 7 — Field catalogs (the property object)](#part-7-field-catalogs-the-property-object)** - **[Part 8 — Appendices](#part-8-appendices)** - [8.1 FAQ](#81-faq) - [8.2 Glossary](#82-glossary) - [8.3 Integration checklists](#83-integration-checklists) - [8.4 Best practices](#84-best-practices) - [8.5 Support](#85-support) - [8.6 Changelog](#86-changelog) - **[Part 9 — Organizations reference (one key, many companies)](#part-9-organizations-reference-one-key-many-companies)** - [9.1 What an organization is](#91-what-an-organization-is) - [9.2 Getting access and authenticating](#92-getting-access-and-authenticating) - [9.3 Rotating keys without downtime](#93-rotating-keys-without-downtime) - [9.4 Create a company — POST /org/companies](#94-create-a-company-post-orgcompanies) - [9.5 Collect the company's card — POST /org/companies/{company_id}/payment-setup-link](#95-collect-the-companys-card-post-orgcompaniescompany_idpayment-setup-link) - [9.6 Company status — list, detail, readiness, edit](#96-company-status-list-detail-readiness-edit) - [9.7 Working inside a company — the same endpoints you already know](#97-working-inside-a-company-the-same-endpoints-you-already-know) - [9.8 Marketplaces in bulk — POST /org/marketplaces/bulk](#98-marketplaces-in-bulk-post-orgmarketplacesbulk) - [9.9 Money: billing report and Stripe labels](#99-money-billing-report-and-stripe-labels) - [9.10 Security, audit and what needs Leasey](#910-security-audit-and-what-needs-leasey) - **[Part 10 — Leasing reference (applications, screening, leases, e-signature)](#part-10-leasing-reference-applications-screening-leases-e-signature)** - [10.1 What you can read, and what stays in Leasey](#101-what-you-can-read-and-what-stays-in-leasey) - [10.2 Applications — GET /applications](#102-applications-get-applications) - [10.3 Screening checks — GET /screening](#103-screening-checks-get-screening) - [10.4 Leases — GET /leases](#104-leases-get-leases) - [10.5 E-signature documents — GET /documents](#105-e-signature-documents-get-documents) - [10.6 Staying in sync](#106-staying-in-sync) - [10.7 Changing applications — POST /applications, PATCH, /send](#107-changing-applications-post-applications-patch-send) - [10.8 Ordering a screening check — POST /applications/{application_id}/screening](#108-ordering-a-screening-check-post-applicationsapplication_idscreening) - [10.9 Leases and e-signature — POST /leases, PATCH, /send, /void](#109-leases-and-e-signature-post-leases-patch-send-void) - [10.10 Error codes of the leasing writes](#1010-error-codes-of-the-leasing-writes) --- ## Part 0 — Start here ### 0.1 What the API lets you do | Capability | In one sentence | Endpoints | |---|---|---| | **Syndicate units** | Create rental units in Leasey and publish them to 23 marketplaces (Zillow, Kijiji, Zumper, Facebook…) with the price shown **before** you publish and the charge confirmed **in the response** | `POST/PUT /properties`, `GET /properties/{id}/marketplaces` | | **Receive leads** | Pull the leads those marketplaces generate — each tagged with the unit and the marketplace it came from — or have them pushed to you | `GET /properties/leads`, webhooks | | **Book & manage showings** | Find a bookable unit, read real open times, schedule a visit, then confirm, reschedule or cancel it — the same logic the booking widget uses | `/showings/*` | | **Run the leasing pipeline** | Create the lead and send the application, move its stage and approve or deny it, order a screening check (charged to your card, like the dashboard), create the lease and send it for e-signature, void a document — and read each application, check, lease and signature, with a webhook when any of them changes | `/applications`, `/screening`, `/leases`, `/documents`, `/document-templates`, webhooks | | **Pull reports & billing** | Portfolio, leads, showings, the syndication report and the charge-by-charge billing ledger — paginated, filterable | `GET /reports/*` | ### 0.2 Who uses what | If you are… | You will mostly use | Start with | |---|---|---| | A property-management platform syndicating your clients' units | Parts 3, 5, 6 | Tutorial 2.1, then 2.2 and 2.5 | | A voice agent, chatbot or automation that books showings | Part 4 | Tutorial 2.3, then 2.4 | | A system that consumes Leasey data (dashboards, CRMs, BI) | Part 5 | Tutorial 2.6 | Everyone needs Part 1 (credentials, auth, errors) — read it once. ### 0.3 What do you want to do? | I want to… | Go to | |---|---| | Get my first successful response in 5 minutes | §1.6 | | Create a unit | §3.3 · tutorial §2.1 | | Show my user the marketplaces **with prices** before publishing | §3.5 | | Publish / unpublish and know exactly what was charged | §3.6 | | Assign or create team members for a unit | §7 (people) | | Upload photos, amenities, restrictions | §7 (content) | | Get the leads my listings generate | §3.7 · tutorial §2.2 | | Be notified instead of polling | Part 6 | | Find a unit a caller is asking about | §4.2 · tutorial §2.3 | | Know what to ask before booking | §4.3 | | Read real available times to a caller | §4.4 | | Book a showing / join a waitlist | §4.5 · §4.6 | | Find a caller's existing showing and confirm / cancel / move it | §4.7 – §4.11 · tutorial §2.4 | | See everything I have live and what it costs | §5.1 | | Reconcile every charge against Stripe | §5.2 · tutorial §2.5 | | Pull the portfolio, leads or showings for a dashboard | §5.3 · tutorial §2.6 | | Understand an error I'm getting | §1.5 | | Test without real charges or real client data | §1.3 (sandbox) | ### 0.4 How this guide is organized - **Part 1 — Fundamentals**: credentials, authentication, environments, conventions, errors, your first request. - **Part 2 — Tutorials**: six step-by-step walkthroughs with real calls. - **Part 3 — Syndication reference**: units, marketplaces, publishing, leads. - **Part 4 — Showings reference**: the twelve booking endpoints and rules. - **Part 5 — Reports reference**: syndication, billing ledger, portfolio/leads/showings, field values. - **Part 6 — Webhooks reference**. - **Part 7 — Field catalogs**: every field the `property` object accepts, with id tables. - **Part 8 — Appendices**: FAQ, glossary, checklists, best practices, support, changelog. ## Part 1 — Fundamentals ### 1.1 Credentials and where to get them | Credential | What it is | Where it comes from | |---|---|---| | **API key** (recommended) | One secret per key, `lsy_live_…` in production and `lsy_test_…` in the sandbox. It identifies your company on its own | Leasey dashboard → Profile → **Developers** (company administrators), or `POST /api-keys` | | API token (legacy) | Partner-level bearer token, identical on every request | Provided by Leasey during onboarding | | `company_id` (legacy) | The Leasey account you act for | Leasey dashboard → Settings, or your onboarding email | | Company password (legacy) | That account's API password (`passwordapi`) | Provided by Leasey | 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. ### 1.2 Authentication #### 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** in this guide 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); any write answers `403`. Use read-only keys for dashboards and reporting tools. - **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 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. - **Create and revoke** keys in the Leasey dashboard: Profile → **Developers**. Only company administrators see that section, the full key is shown once, and every administrator receives an email when a key is created. - **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 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 its `status` (`active`, `expired` or `revoked`) and `expires_soon` (`true` in the last 7 days, while it still works). - **Rotate without downtime** from your own system, using a full-access key: | Method | Path | What it does | |---|---|---| | `GET` | `/api-keys` | Your company's keys, never the secret (`key_last4`, `scope`, `created_at`, `last_used_at`, `expires_at`, `revoked_at`, `status`, `expires_soon`) 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. `resources` is optional too (leave it out for a key that can use every resource) | | `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.** Wrong, revoked or expired key: `401`. Read-only key on a write: `403`. A revoked key stops working on every endpoint immediately. #### Legacy credentials (token + company password) Existing integrations keep working unchanged. Leasey will announce a retirement date with notice; new integrations should use API keys. Two credentials on **every** request: | Credential | What it is | Where it goes (GET) | Where it goes (POST/PUT) | |---|---|---|---| | API token | Partner-level bearer token | `Authorization: Bearer ` | `Authorization: Bearer ` | | Company password | Your account password | `X-Company-Password` header¹ | `password` field in JSON body | ¹ `GET /properties/{id}/marketplaces`, `GET /properties/leads` and `GET /webhooks` accept the password as the `password` query parameter instead of 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 X-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. Both credentials are provided by Leasey during onboarding. Keep them server-side; never ship them in client-side code. ### 1.3 Environments and sandbox | Environment | Base URL | Use it for | |---|---|---| | **Sandbox** | `https://sandbox.api.leasey.ai/v1` | Building and testing your integration | | **Production** | `https://api.leasey.ai/v1` | Live traffic, once your integration is validated | All paths in this guide 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`. Each environment is a separate system with **its own API token, its own `company_id` and its own company password** — credentials from one never work on the other. **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. On those previous URLs the host and the stage go together (the sandbox host only answers under `/staging`, the production host only under `/production`; mixing them returns a bare `403 Forbidden`). **Versioning.** `v1` is the major version of the API. Within `v1` every change is additive — new endpoints, new optional parameters, new response fields, new values of an enumerated field — and is listed with its date in the changelog (§8.6); your integration keeps working without changes. A change that would break existing integrations (renaming or removing a field, changing a type) ships as `v2` on the same domains, announced in advance, and `v1` keeps running for at least 12 months after that announcement. Keys follow the same rule: `lsy_test_…` and `lsy_org_test_…` keys only work in the sandbox, `lsy_live_…` and `lsy_org_live_…` keys only in production. Everything else — routes, headers and bodies — is identical, so switching environments means changing only the base URL and the credential. **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 (e.g. `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 (§3.6). - **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.** The sandbox runs the same API version, so a flow that works there works in production. - **Your own data only.** Your sandbox account starts empty and is isolated from every other account. Everything you see in the sandbox you can also check in its dashboard with the user Leasey gives you at onboarding. **Health check.** `GET /health` needs no credentials, in both environments. 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. ### 1.4 Conventions: JSON, dates, data types, 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` (§5.5). **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` (§5.3). **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` (versioning policy, §1.3). A twin is `null` when the stored text is not a number. **Pagination** (`/reports/properties`, `/reports/leads`, `/reports/showings`, `/reports/billing`; `/reports/syndication` is a snapshot with one row per unit and marketplace and is not paginated): add `page` and `page_size` (max 5000). 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: ```json "count": 1000, "total_rows": 5005, "pagination": { "page": 1, "page_size": 1000, "total_pages": 6, "has_more": true } ``` `/showings/lookup` paginates with `page`/`page_size` (max 100) and returns `total` + `has_more`; `/properties/leads` uses `limit` (max 500). Every report has a fixed order, so paging through it never skips or repeats a row while the data does not change between pages: | Report | Order | |---|---| | `/reports/showings` | `Date_start` ascending, then `Showing_ID` ascending | | `/reports/leads` | `Lead_Date_Created` descending, then `Application_ID` descending | | `/reports/properties` | Active units first, then published ones, then `Property_ID` descending | | `/reports/billing` | `Created_At` descending, then `Billing_ID` descending | | `/reports/syndication` | `Property_ID`, then `Marketplace` | For data that keeps changing while you page, sync with `updated_since` (§5.3). **Operational limits** | Limit | Value | Behavior at the limit | |---|---|---| | Request timeout | 29 s | `504` with `code: timeout` — paginate or narrow the dates, then retry once | | Report response size | 6 MB | `413` with `suggested_page_size` — paginate or narrow dates | | `page_size` (reports) | max 5000 | Values above are capped to 5000 | | `limit` (`/properties/leads`) | default 100, max 500 | Values above are capped to 500 | | Photos per unit | no hard cap | First photo is the cover; HTTPS URLs only | | Marketplaces per publish call | all 22 in one call | Each entry succeeds or fails individually | | Webhooks per account | 1 | `PUT /webhooks` updates it in place | | Webhook delivery timeout | 8 s | Non-2xx or slower → retried (5 attempts, backoff) | | Publish idempotency | state-change guard | Re-sending the same state = `no_change`, never re-billed | | Request rate | protective throttle | `429 Too Many Requests` with `Retry-After` | **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: ```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" } ``` 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. ### 1.5 Errors and 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" } ``` | Field | What it is | |---|---| | `code` | Stable, machine-readable, `snake_case`. Program against it: a condition keeps its code. New codes may be added | | `error` | The name of the HTTP status | | `message` | For people. It may be reworded at any time; do not parse it | | `request_id` | Unique per request. **Every** response, successful or not, also carries it in the `X-Request-Id` header. Quote it when you contact Leasey and we find your request at once | Some errors add context fields: `details` (422), `missing_fields` and `missing_questions` (`POST /showings`), `suggested_page_size` (413). **Error codes** | `code` | HTTP | When | |---|---|---| | `unauthorized` | 401 | Token or company password missing or wrong | | `invalid_api_key` | 401 | The API key does not exist | | `api_key_revoked` | 401 | The API key was revoked | | `api_key_expired` | 401 | The API key expired | | `invalid_expires_at` | 422 | `POST /api-keys` with an `expires_at` that is not a future ISO 8601 date or date-time | | `api_key_read_only` | 403 | A read-only key was used for a write | | `api_key_resource_not_allowed` | 403 | The key is limited to other resources (§1.2), or a limited key tried to create a key beyond its own resources | | `invalid_resources` | 422 | `POST /api-keys` with `resources` that is not a non-empty list of known resources | | `invalid_pagination` | 400 | `page` below 1, or `page_size` outside 1–500, on the leasing routes (Part 10) | | `invalid_parameter` | 400 | A filter with a value that does not exist (the message lists the valid ones) | | `application_not_found`, `lease_not_found`, `document_not_found` | 404 | That record does not exist in your account | | `updated_since_unavailable` | 503 | `updated_since` on a route where that environment does not support it yet | | Leasing writes: `property_not_found`, `template_not_found`, `application_not_sendable`, `applicant_email_required`, `applicant_phone_required`, `idempotency_key_required`, `check_not_available`, `payment_method_required`, `payment_failed`, `lease_not_editable`, `lease_not_sendable`, `document_not_sendable`, `document_not_voidable`, `order_in_progress`, `document_already_created`, `esign_unavailable` | 400–503 | One per case, with its HTTP status, in §10.10 | | `forbidden` | 403 | The credential cannot do this | | `route_not_found` | 403 | The method and path combination does not exist | | `bad_request` | 400 | The request is malformed (for example a `property_id` that is not a UUID) | | `missing_fields` | 400 | `POST /showings` without a required field (listed in `missing_fields`) | | `missing_questions` | 400 | `POST /showings` without an answer to a required question | | `waitlist_mode_required` | 400 | The event only takes waitlist requests: send `date_options` (§4.6) | | `time_slot_unavailable` | 400 | Someone took that time a moment ago | | `invalid_updated_since` | 400 | `updated_since` is not an ISO-8601 date | | `updated_since_not_supported` | 400 | `updated_since` on `/reports/syndication` | | `invalid_idempotency_key` | 400 | The `Idempotency-Key` header has invalid characters (§1.7) | | `slot_not_found`, `invalid_slot_id`, `missing_slot_id`, `slot_cancelled`, `slot_unavailable` | 400 / 404 | The showing id is wrong, cancelled or no longer available | | `not_found` | 404 | The record does not exist or belongs to another account | | `conflict` | 409 | For example `POST /properties` with a name that already exists | | `idempotency_request_in_progress` | 409 | The first request with that `Idempotency-Key` is still running | | `payload_too_large` | 413 | The report is over 6 MB: paginate | | `validation_failed` | 422 | Field errors, listed in `details` | | `idempotency_key_reused` | 422 | That `Idempotency-Key` was used for a different request | | `rate_limited` | 429 | Protective throttle: wait `Retry-After` seconds | | `internal_error` | 500 | Unexpected failure on our side: retry once, then contact us with the `request_id` | | `service_unavailable` | 503 | Temporarily unavailable: retry later | | `timeout` | 504 | The request took longer than 29 s: paginate or narrow the dates | | HTTP | `error` | When | What to do | |---|---|---|---| | 400 | Bad Request | Malformed `property_id` (not a UUID), missing required identifiers | Fix the request | | 401 | Unauthorized | Bad token or company password | Check credentials; URL-encode special characters in query params | | 404 | Not Found | Unit doesn't exist or belongs to another account | Verify `property_id` | | 409 | Conflict | `POST /properties` with a `name` that already exists in your account — the body carries the existing `property_id` | Update that unit with `PUT /properties/{property_id}`, or use a different name (unit names are unique per account) | | 413 | Payload Too Large | A report response exceeds the 6 MB cap | Use `page`/`page_size` or narrow `start_date`/`end_date`; the response suggests a page size | | 422 | Unprocessable Entity | Validation failed — `details` array lists each field error | Fix the listed fields | | 429 | Too Many Requests | Protective throttle | Wait `Retry-After` seconds and retry | | 500 | Internal Server Error | Unexpected failure on our side | Retry once; report to Leasey with the `request_id` | Notes: - A `422` on create/update includes `details` with one message per invalid field. Real example (three problems in one request — all reported at once): ```json { "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. - Marketplace entries inside an otherwise-valid request fail **individually** (`status: "error"` in the response array) without aborting the rest. - `code: "route_not_found"`, or an AWS message mentioning *"Invalid key=value pair in Authorization header"*, means the **URL/route is wrong**, not the credentials. #### Troubleshooting by symptom | You see… | It means… | Fix | |---|---|---| | `401 Authentication token required` / `Invalid authentication token` | The bearer header is missing or malformed | Header must be exactly `Authorization: Bearer ` — one space, no quotes, no trailing whitespace; make sure it is the production token | | `401 'password' is required` / `Invalid company password` | Company credentials missing or wrong for the request style | `GET` → `X-Company-Password` header (or `password` query param on the endpoints that accept it); `POST`/`PUT` → `password` inside the JSON body. Check `passwordapi` is set in Leasey → Settings → API Access | | `401` only when the password is sent in the query string | The password contains `+`, `&`, `#` or `%` | URL-encode it (`+` → `%2B`) or send it in the header/body | | `400 'company_id' must be an integer` | `company_id` sent as a quoted string | Send `123`, not `"123"` (in Postman: no quotes in Params) | | `422` with a `details` array | Validation failed on create/update | Fix every entry listed; nothing was created | | `404` on `PUT /properties/{id}` or `/showings/{slot_id}` | Wrong id, or the record belongs to another account | Use the exact id returned when it was created; you can only touch your own account's data | | `413 Payload Too Large` | Report bigger than 6 MB | Paginate (`page`, `page_size`) or narrow the dates — the body suggests a size | | An AWS message: *"Invalid key=value pair … in Authorization header (hashed with SHA-256)"* | The **URL/route is wrong**, not the credentials | Check the path and method against this guide (e.g. `/reports/showings`, not `/report/showings`) | | The response body looks like a JSON string inside JSON | Your HTTP client is parsing an already-parsed body | Parse the body exactly once | | CORS error in a browser | You are calling the API from client-side code | Call it from your backend — credentials must never live in the browser | | `time_slot_unavailable` on `POST /showings` | Someone took that time between your availability read and the booking | Re-read `/showings/availability` and offer another time | | A marketplace entry comes back `status: "error"`, `code: "payment_method_required"` | The account has no Stripe billing profile or no saved card, so paid marketplaces cannot be activated | Add a payment method in Leasey (Settings → Billing); check `billing.ready` on the catalog before offering paid toggles | | `429 Too Many Requests` | A burst hit the protective throttle | Wait the seconds in `Retry-After`, then back off exponentially | | `500 Internal Server Error` | Unexpected failure on our side | Retry once after a few seconds; if it persists, contact support with the `request_id` | ### 1.6 Your first request (5 minutes) **cURL** — the showings report, date-filtered: ```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" ``` 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. **Python** ```python import requests BASE = "https://api.leasey.ai/v1" 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"}) r.raise_for_status() print(r.json()["count"], "showings") # 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}}) print(r.status_code, r.json().get("property_id")) ``` **Postman in four steps** 1. *Environments → New*: add `base_url` (the production URL above), `api_token`, `company_id`, `company_password`. 2. New request `GET {{base_url}}/reports/showings` → *Params*: `company_id` = `{{company_id}}` → *Headers*: `Authorization` = `Bearer {{api_token}}`, `X-Company-Password` = `{{company_password}}` → *Send*. 3. New request `POST {{base_url}}/properties` → *Headers*: `Authorization`, `Content-Type: application/json` → *Body (raw JSON)*: `{"company_id": {{company_id}}, "password": "{{company_password}}", "property": {…}}` → *Send*. 4. Or skip steps 2–3: *File → Import* the `openapi.yaml` and every endpoint appears pre-built — just fill the environment. ### 1.7 Retries 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`. ```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 send | You get | |---|---| | A new key | The request runs normally. The response is stored for 24 hours | | The same key and the same request again (for example after a timeout) | The stored response, without running the write again, plus the header `Idempotent-Replayed: true` | | The same key with a different request | `422`, `code: idempotency_key_reused` | | The same key while the first request is still running | `409`, `code: idempotency_request_in_progress`, with `Retry-After` | - Keys belong to your credentials: another partner's key never collides with yours. - `5xx` responses are not stored, so you can retry them with the same 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 (§4.12); reschedule, cancel and confirm always land on the same final state. ## Part 2 — Step-by-step tutorials Each tutorial is a complete path with the real calls in order. The full request/response of every endpoint is in the reference parts; here you get the sequence and what to carry from one step to the next. ### 2.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` (§3.3) 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` (§3.5). Render the 22 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 (§3.6). 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` (§5.1): 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 syndication flow at a glance (with the money flow):** ``` 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} { 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 ``` 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` — publish several marketplaces in one call after your user confirms. - **Prices are visible before publishing** (step 2) and **charges are confirmed in the publish response** (step 4). No surprises. - **Store the `property_id`** returned on creation. It is the permanent key for everything else. ### 2.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=` (§3.7) 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` (§6.2) 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 (§6.3) 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. ### 2.3 Book a showing end to end (voice / chat agent) **Goal:** a caller wants to see a unit and leaves with a booked visit. Each step shows the conversation, the call, and what to carry forward. ``` Caller: "I'd like to book a viewing for the unit on Edmonds Station." 1 → GET /showings/lookup?company_id=128&query=Edmonds (§4.2) ← events: [{ event_id: 4067, unit_name: "Maple Court Unit 2", is_waitlist: false }] 0 results → ask for the address or unit name · many → read the top few Agent: "I found Maple Court Unit 2 on Edmonds Station Bus Loop — is that right?" (agent holds event_id = 4067) 2 → GET /showings/questions?company_id=128&event_id=4067 (§4.3) ← questions: [] (or a list with `required` ones → ask them now) 3 → GET /showings/availability?company_id=128&event_id=4067 (§4.4) ← dates: [{ date: "2026-08-31", slots: [{ formatted_time: "2:00 PM" }, …] }] Agent: "On Monday the 31st I have 2:00 PM, 2:10 PM or 2:20 PM. Which works?" Caller: "2 PM." → collect first name, last name, email (required), phone 4 → POST /showings (§4.5) body: { company_id, password, event_id: 4067, firstname, lastname, email, phone, date: "2026-08-31", time: "14:00", answers: [...] } ← 201 { slot_id: 54949, application_id: 117517, is_update: false, lead_status: "showing" } Agent: "You're booked for Monday, August 31 at 2:00 PM. You'll get a confirmation by email and text." (agent stores slot_id = 54949) ``` The booking is **complete** at step 4 — it is `pending`, valid and notified; `/confirm` is a separate optional action (§2.4). If the unit came back with `is_waitlist: true` (no bookable times), skip step 3 and send `date_options` with up to three preferred dates instead of `date`/`time` (§4.6). If step 4 returns `time_slot_unavailable`, someone took that time — repeat step 3 and offer another. Data to collect before step 4: `event_id`, first name, last name, **email**, phone, chosen `date` + `time`, answers to every `required` question. ### 2.4 Manage an existing showing (confirm, reschedule, cancel, status) The caller never knows their `slot_id` — always find it first by contact. ``` 1 → GET /showings?company_id=128&email=jane.prospect@example.com (§4.7) ← 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 says | Call | Result | |---|---|---| | "Yes, I'll be there" | `POST /showings/54949/confirm` (§4.11) | `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` (§4.9) | back to `pending`, team re-assigned and notified | | "Cancel it" | `POST /showings/54949/cancel` (§4.10) | `state: 3` cancelled, team notified; works on waitlist entries too | | "Is my viewing still on?" | `GET /showings/54949` (§4.8) | 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 (§4.8) before retrying. ### 2.5 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` (§5.2), 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` (§5.1). ### 2.6 Pull the reports for a dashboard or CRM | You need… | Call | Notes | |---|---|---| | The unit portfolio (names, addresses, rent, status, days listed) | `GET /reports/properties` | No date filter; cache 5–15 min — it is the heaviest report | | Prospects and applicants with rating and application state | `GET /reports/leads` | Defaults to Jan 1 of this year — pass `start_date`/`end_date` | | Showing activity with team member and lead source | `GET /reports/showings` | Same date default; paginate on large accounts | | What is published where and what it accumulates | `GET /reports/syndication` | Summary + one row per unit × marketplace | | Every charge and its payment state | `GET /reports/billing` | See §2.5 | All five share the envelope (`success`, `report_type`, `company_id`, `count`, `total_rows`, `data`, optional `pagination`) and the `X-Company-Password` header for auth. Possible values of every enumerated field are listed in §5.4 — use them for filters and status badges. ## Part 3 — Syndication reference ### 3.1 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) └── Showing event (event_id) (booking config: availability, questions) └── Showings / slots (slot_id — one per booked or waitlisted visit) ``` - **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. ### 3.2 Pricing and billing model Each marketplace is either **free** or **paid**. Paid marketplaces have a **daily rate** configured per partner account (your negotiated pricing), in CAD or USD depending on your account's country. | Rule | Detail | |---|---| | **First month** | Billed **in full** on activation: `daily_rate × 30`, regardless of how many days the listing stays live that month. | | **From day 31** | The daily rate is billed for each day the listing remains live, invoiced in 30-day cycles. | | **On deactivation** | Days beyond the last billed period are **prorated** at the daily rate (you never pay a full month for a partial period after the first). | | **Free marketplaces** | Never generate any charge. | | **Re-sending the same state** | Idempotent: activating an already-active marketplace (or deactivating an inactive one) is a no-op — **no duplicate charges, no duplicate logs**. | **Billing behavior is identical whether a marketplace is enabled/disabled via this API or from the Leasey platform UI.** Both paths write the same publication flags, log entries and billing items, and the same billing engine processes them: the first month is charged in full on activation (`daily_rate × 30`), the daily rate applies from day 31 in 30-day cycles, deactivation prorates the final partial period, and re-sending the same state never creates a duplicate charge (`no_change`). There is no separate "API pricing" of any kind. Where prices appear in the API: 1. **Before publishing** — `GET /properties/{id}/marketplaces` returns `daily_rate` and `first_month_price` per marketplace, so you can show the exact cost in your confirmation dialog. 2. **When publishing** — the `PUT` response includes a `charge` object per activated marketplace confirming the amount billed. 3. **Afterwards** — `GET /reports/syndication` aggregates everything billed. ### 3.3 Create a unit — POST /properties ``` POST /properties ``` **Request** ```json { "company_id": 128, "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 } ] } } ``` Only `name`, `address`, `city`, `province_state` and `country` are required; everything else is optional (see §7 for the full field list). `marketplaces` may be included at creation or later via `PUT`. **Response — `201 Created`** (real example) ```json { "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. ### 3.4 Update a unit — PUT /properties/{property_id} ``` PUT /properties/{property_id} ``` Send only the fields you want to change; everything else is untouched. **Request** ```json { "company_id": 128, "password": "", "property": { "rent_amount": 1950, "date_available": "2026-11-01" } } ``` **Response — `200 OK`** ```json { "success": true, "message": "Property updated successfully.", "property_id": "b94c71ea-412d-4969-99be-fa9db983f090" } ``` Update semantics per field: | Field | Behavior on update | |---|---| | `photos` | **Replaces** the full photo set | | `features`, `restrictions` | **Replace** the full list (`[]` clears) | | `marketplaces` | Only the listed entries change; others keep their state | | everything else | Partial update | Errors specific to this endpoint: `400` malformed `property_id` (must be a UUID), `404` unit not found or not owned by your account. ### 3.5 Marketplace catalog and prices — GET /properties/{property_id}/marketplaces ``` GET /properties/{property_id}/marketplaces Authorization: Bearer lsy_live_<40 characters> ``` With the legacy credentials this endpoint takes `company_id` and `password` as query parameters (§1.2). 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. **Response — `200 OK`** (real example, truncated) ```json { "success": true, "property_id": "b94c71ea-412d-4969-99be-fa9db983f090", "count": 23, "billing": { "ready": true, "has_customer": true, "has_payment_method": true, "payment_method": "visa •••• 2535", "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": "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. **`billing.ready` — check it before offering paid marketplaces.** Paid activations require a Stripe billing profile **and** a saved payment method on the account. When `ready` is `false`, disable the paid toggles in your UI and show `billing.message` (it says exactly what is missing — no profile, or profile without a card); free marketplaces are never affected. An account without a way to pay would otherwise accumulate charges that can never be invoiced, so the API refuses paid activations instead (§3.6). **The rule has no exceptions** — it applies to every account, test accounts included, so what you see in a sandbox is what happens in production. Real response for an account with no billing profile: ```json "billing": { "ready": false, "has_customer": false, "has_payment_method": false, "payment_method": null, "message": "No billing profile yet. Add a payment method in Leasey (Settings → Billing) before activating paid marketplaces. Free marketplaces are not affected." } ``` 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`. > **Names are canonical.** The API also accepts these legacy aliases as input > and normalizes them everywhere (including the syndication report): > `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 the lists above. ### 3.6 Publish and unpublish marketplaces Publishing is a `PUT` on the unit with the `marketplaces` array — batch as many toggles as your user confirmed in a single call: **Request** ```json { "company_id": 128, "password": "", "property": { "marketplaces": [ { "name": "kijiji", "enabled": true }, { "name": "facebook", "enabled": true }, { "name": "viewit", "enabled": false } ] } } ``` **Response — `200 OK`**: the `marketplaces` array confirms, per entry, what happened and what was charged: ```json { "success": true, "message": "Property updated successfully.", "property_id": "b94c71ea-412d-4969-99be-fa9db983f090", "marketplaces": [ { "name": "kijiji", "status": "published", "active": true, "daily_rate": 0.74, "currency": "USD", "charge": { "type": "first_month", "amount": 22.20, "currency": "USD", "period_days": 30, "note": "First month billed in full on activation. From day 31 the daily rate applies for each day the listing stays live." } }, { "name": "facebook", "status": "published", "active": true, "daily_rate": 0, "currency": "CAD", "charge": null }, { "name": "viewit", "status": "unpublished", "active": false, "daily_rate": 1.62, "currency": "USD", "charge": null, "note": "Unpublished. Any days past the last billed period are prorated at the daily rate on the next billing run." } ] } ``` Possible `status` values per entry: | `status` | Meaning | `charge` | |---|---|---| | `published` | Activated and pushed to the marketplace feed | `first_month` object for paid, `null` for free | | `unpublished` | Deactivated; removed from the feed | `null` (proration handled on the next billing run) | | `no_change` | Already in the requested state — **nothing billed** | absent | | `error` | This entry failed (message included); others still processed | absent | | `error` + `code: "payment_method_required"` | A **paid** activation was refused because the account has no Stripe billing profile / payment method. Nothing was changed for this entry (no flag, no log, no charge). Free entries and deactivations in the same call still go through | absent (`daily_rate`/`currency` included so you can show the cost that would apply) | Real refused entry: ```json { "name": "kijiji", "status": "error", "code": "payment_method_required", "active": false, "daily_rate": 0.74, "currency": "USD", "message": "No billing profile yet. Add a payment method in Leasey (Settings → Billing) before activating paid marketplaces. Free marketplaces are not affected." } ``` Check `billing.ready` on the catalog (§3.5) first and you will never hit this in normal operation. **Notifications — API-triggered actions fire the exact same flow as the platform UI** (the API invokes the platform's own notification handler, not a copy). On publish and on unpublish, Leasey's team is alerted internally. On **unpublish**, additionally and according to the unit's settings: if *cancel showings on unpublish* is enabled, upcoming showings are cancelled and each cancellation is emailed to the lead, the **`owner_contact`** (when `notify_email` is true), the tenant contact and the assigned team member; if *notify leads on unpublish* is enabled, every lead of the unit gets an availability notice. All of these use the contacts **stored on the unit** (§7) — the enable/disable payload never carries an email. Those two unpublish settings default to off and are configured on the unit in Leasey. Webhooks push `unit.published`/`unit.unpublished` to your endpoint too (§6). ### 3.7 Leads — GET /properties/leads ``` GET /properties/leads?start_date=2026-08-01&limit=50 Authorization: Bearer lsy_live_<40 characters> ``` With the legacy credentials this endpoint takes `company_id` and `password` as query parameters (§1.2). Leads generated on the marketplaces flow into Leasey and are pulled here. Every lead carries the **`property_id`** (map it straight to your unit) and the **`source`** — which marketplace produced it. 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) ```json { "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` is the lead's stage, with the same labels as `/reports/leads`: `New Lead`, `Application In Progress`, `Booked`, `Application In Review`, `Signing Lease`, `Active Tenants` or `Archived Lead` (meaning of each in §5.4). `property_id` is the unit the lead came in for, the same value as `Unit_ID` in `/reports/leads` and `/reports/showings`. #### Getting leads back into your platform Recommended sync loop (or subscribe to `lead.received` webhooks — see §6): 1. Poll `GET /properties/leads?start_date=` 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. ## Part 4 — Showings reference Everything a voice agent, chatbot or automation needs to **book and manage showings** without the booking widget. These endpoints reuse the exact same logic the widget uses — team-member assignment, conflict checks, notifications, lead-journey logging — so a showing created here is identical to one created on the website. Auth is the same as everywhere else (§1.2): bearer token on every call; `company_id` + password in the query string / `X-Company-Password` header for `GET`, inside the JSON body for `POST`/`PUT`. A caller can only ever read or modify showings belonging to the `company_id` it authenticated with. ### 4.1 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. Slot states: `0` qualified lead · `1` **pending** (booked, valid, notified) · `2` **confirmed** (the person said they will attend) · `3` cancelled · `4` waitlisted. ``` Book a new showing Manage an existing one ------------------------------------ --------------------------------------- 1. GET /showings/lookup → event_id 1. GET /showings?email= or ?phone= 2. GET /showings/questions → what to → the person's active showings ask 2. (caller picks one → slot_id) 3. GET /showings/availability → real 3. POST /showings/{slot_id}/confirm open POST /showings/{slot_id}/cancel times PUT /showings/{slot_id}/reschedule 4. POST /showings → slot_id (after a fresh availability read) (waitlist unit? send date_options) 4. GET /showings/{slot_id} → status ``` > **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. ### 4.2 Find bookable units — GET /showings/lookup ``` GET /showings/lookup?company_id=128&query=Maple%20Court Authorization: Bearer X-Company-Password: ``` Query params: `company_id` (required); optionally **`query`** (free text) or **`unit_id`** (exact match); `page` / `page_size` (default 50, max 100). `query` is a partial, case-insensitive match (`%text%`) across **seven** fields at once — property name, property address, URL slug, suite/unit number, building name, building address, event name — always scoped to your account and to published units only. "East 2nd" matches "275 East 2nd Street"; casing is ignored. **Response — `200 OK`** (real example; test unit renamed) ```json { "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** — book it with §4.6, not §4.5. **Whole catalog**: 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. Real envelope from an account with 190 units: ```json { "success": true, "count": 2, "total": 190, "page": 1, "page_size": 2, "has_more": true, "events": [ … ] } ``` A waitlist unit looks like this in the catalog (real): ```json { "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 } ``` ### 4.3 Pre-qualification questions — GET /showings/questions ``` GET /showings/questions?company_id=47&event_id=1599 Authorization: Bearer X-Company-Password: ``` 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. **Response — `200 OK`** (real) ```json { "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 (real: event 4067 above). Required questions are **enforced** on `POST /showings` — a missing answer fails with `400` and a `missing_questions` list. The answers are stored with the booking. `/reports/leads` and `/reports/showings` return the Qualified / Not Qualified result on every row and, with `include_answers=true`, the answers themselves (§5.3). ### 4.4 Availability — GET /showings/availability ``` GET /showings/availability?company_id=47&event_id=1409&date_from=2026-09-01&date_to=2026-09-14 Authorization: Bearer X-Company-Password: ``` 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. Times are in the event's local time zone. Optional `date_from` / `date_to` narrow the window; at most 30 days are scanned per call. **Response — `200 OK`** (real, trimmed to three slots) ```json { "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 } ] } ] } } ``` **How far ahead the calendar goes** depends on how the event is configured in Leasey: a *rolling* event (the most common) offers **today + N days**, where 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. Ask for a narrower span with `date_from` / `date_to` — those narrow the window, they never extend it past what the event allows. **Only published events are bookable.** An event that is not published returns `404` (`"Event not found, not published, or not yours."`), never a `200` with an empty list — so `404` here means "this unit isn't open for booking", not "no times available". Use `/showings/lookup` (§4.2) as the source of truth for what is bookable: it lists published events only. An event that is published but has no availability configured (or whose slots are all taken) returns the normal envelope with `dates: []` — offer the waitlist or a callback. `is_waitlist: true` with no dates means the event only accepts waitlist requests (§4.6). Always re-read availability right before booking: slots are first-come, first-served. ### 4.5 Schedule a showing — POST /showings ``` POST /showings Authorization: Bearer Content-Type: application/json ``` ```json { "company_id": 128, "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" } ] } ``` | Field | Required | Notes | |---|---|---| | `event_id` | yes | From §4.2 | | `firstname`, `lastname` | yes | The person visiting | | `email` | **yes** | The lead's identity and the key to find the showing later (`GET /showings?email=`). Capture it as accurately as possible | | `phone` | **yes** | Without it the API answers `400` with `missing_fields: ["phone"]`. It also works for lookups | | `dial_code` | no | Defaults to `+1` | | `date`, `time` | yes (normal events) | `YYYY-MM-DD` and 24h `HH:MM`, from §4.4 | | `unit_id` | building-level events only | When the person chose a specific unit of a building (`bookshowingby: 1`) | | `answers` | if the event has required questions | `{question_id, value}`; Dropdown values must be one of `options` | The team member is assigned **automatically** — never send one. (`answers` is shown for illustration — event 4067 has no questions, so the real call below omitted it; use the `question_id`s from §4.3 for your event.) **Response — `201 Created`** (real) ```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 will 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 (§4.12). - `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`). Failure cases: `400` with `missing_questions` (a required answer is missing), `400` `time_slot_unavailable` (someone took the time first — re-read §4.4), `400` with `is_waitlist: true` (this event needs §4.6). ### 4.6 Join the waitlist — POST /showings with date_options 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 as §4.5 — swap `date` + `time` for `date_options`: ```json { "company_id": 128, "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}` — the time is optional. One to three entries; past dates are rejected with `400`. The response has the same shape as §4.5 with `lead_status: "waitlisted"` (state `4`), no team member and no time held. The two modes are strictly separated: `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. ### 4.7 List a person's showings — GET /showings ``` GET /showings?company_id=128&email=jane.prospect%40example.com Authorization: Bearer X-Company-Password: ``` Use this **before** confirm / cancel / reschedule: 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). Optional `include_waitlist=false` omits waitlist entries. **Response — `200 OK`** (real; test unit renamed) ```json { "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; it can be cancelled like any other showing. `virtual`: `0` in person · `1` virtual · `2` either. ### 4.8 Showing status — GET /showings/{slot_id} ``` GET /showings/54949?company_id=128 Authorization: Bearer X-Company-Password: ``` **Response — `200 OK`** (real; test unit renamed) ```json { "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" } } ``` Re-check status at any point — for example after a write timed out — before retrying an action. ### 4.9 Reschedule — PUT /showings/{slot_id}/reschedule ``` PUT /showings/54949/reschedule Authorization: Bearer Content-Type: application/json ``` ```json { "company_id": 128, "password": "", "date": "2026-09-01", "time": "15:30" } ``` Read §4.4 first for the new time. Re-runs team-member assignment and re-sends notifications; the slot returns to `pending` (state `1`) and — like a fresh booking — does **not** need a follow-up `/confirm` to be valid. **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`). **Response — `200 OK`** (real) ```json { "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 test event above has none configured) — the showing still exists and the team is notified; pick a time from §4.4 to get an assigned member. ### 4.10 Cancel — POST /showings/{slot_id}/cancel ``` POST /showings/54949/cancel Authorization: Bearer Content-Type: application/json ``` ```json { "company_id": 128, "password": "" } ``` Sets the showing to cancelled (state `3`) and notifies the team. Works on waitlist entries too. **Response — `200 OK`** (real): ```json { "success": true, "slot_id": 54949, "state": 3, "message": "Slot cancelled successfully", "language": "en" } ``` A cancelled slot no longer appears in `GET /showings` (§4.7) but `GET /showings/{slot_id}` still returns it with `status: "cancelled"`. ### 4.11 Confirm — POST /showings/{slot_id}/confirm ``` POST /showings/54949/confirm Authorization: Bearer Content-Type: application/json ``` ```json { "company_id": 128, "password": "" } ``` Sets the showing to confirmed (state `2`) and notifies the team. **Optional** — a showing is already valid in `pending` after schedule or reschedule. Call it only when the person explicitly says they will attend; it does not have to happen in the same conversation as the booking. **Response — `200 OK`** (real): ```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. ### 4.12 Idempotency, errors and 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` in the response). An accidental retry never double-books — still, only call it once the person has chosen the time. - Reschedule / cancel / confirm are state transitions: repeating them lands on the same final state. - If a write times out with no response, read the true state with `GET /showings/{slot_id}` (or `GET /showings` by contact) before retrying, or retry with the same `Idempotency-Key` (§1.7). **Errors specific to showings** | Status | When | What to do | |---|---|---| | `400` | Missing fields; missing required questions (`missing_questions` lists each with `question`, `type`, `options`); booking conflict (`time_slot_unavailable`); wrong mode (`date`/`time` on a waitlist event or `date_options` on a normal one — the body carries `is_waitlist`) | Ask for the missing info, or re-read availability and offer another time | | `401` | Bad token or company credentials | Configuration error on your side — do not retry | | `404` | Event or showing not found, not published, or not owned by your account | Re-run lookup / list; tell the person you couldn't find it | | `500` | Unexpected server error | Retry once; then fall back to a human / callback | `time_slot_unavailable` means someone took that time between your availability read and the booking — fetch availability again and offer a new time. **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. **Never act on a slot the caller did not clearly choose**, and always confirm the specific appointment verbally before a cancel or reschedule. - Map spoken answers to one of a question's `options` before sending. ## Part 5 — Reports reference ### 5.1 Consolidated syndication report — GET /reports/syndication ``` GET /reports/syndication?company_id=128 Authorization: Bearer X-Company-Password: ``` The **admin view** across your whole account: one row per unit × marketplace with live status, publish date, days billed and amounts split by billing status — plus an executive summary. Optional `start_date` / `end_date` filter the billing periods included. **Response — `200 OK`** (real example, truncated) ```json { "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`. ### 5.2 Billing ledger — GET /reports/billing Money deserves its own report. `/reports/syndication` tells you *what is live and what it accumulates*; this one is 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). ``` GET /reports/billing?company_id=47 Authorization: Bearer X-Company-Password: ``` Optional filters: `start_date` / `end_date` (on `Created_At` — when the charge was generated; each row's `Period_Start`/`Period_End` shows the days it covers), `status`, `property_id`, plus `page`/`page_size` like every report. An unknown `status` returns `400 — status must be one of: failed, invoiced, paid, pending.` Real response (trimmed to one row): ```json { "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 } ] } ``` 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. ### 5.3 Properties, leads and showings reports — GET /reports/{properties|leads|showings} Same auth as §5.1 (`X-Company-Password` header). **Date filters.** `start_date` and `end_date` are optional on showings and leads, and each report filters on a different date: | Report | `start_date` / `end_date` filter on | Without `start_date` | |---|---|---| | `/reports/leads` | When the lead was created (`Lead_Date_Created`) | January 1 of the current year | | `/reports/showings` | When the showing takes place (`Date_start`, in the company's local time) | January 1 of the current year | Both accept a date (`2026-11-03`) or a date-time (`2026-11-03T14:00:00`). A bare date means midnight at the **start** of that day: `end_date=2026-11-03` stops at 00:00 on November 3, so send `2026-11-04` or `2026-11-03T23:59:59` to include the whole day. A change of stage or status (a lead that books, a showing that is confirmed or cancelled, feedback after a visit) does not move these dates. To pick up changes, use `updated_since`. **Incremental sync: `updated_since`** (since 3.12). Returns only the rows that changed since that moment, and every row carries `Updated_At` (UTC): | Report | A row counts as changed when… | |---|---| | `/reports/leads` | The lead's application (stage, archive, approval), its applicant record or its contact details change | | `/reports/showings` | The showing (time, status, confirmation, feedback) or its lead changes | | `/reports/properties` | The unit changes | | `/reports/billing` | A charge is created, invoiced, paid or retried | | `/reports/syndication` | Not supported: it is a snapshot of right now (`400`, `updated_since_not_supported`) | ``` GET /reports/leads?company_id=128&updated_since=2026-11-03T14:00:00Z&page=1&page_size=500 ``` ```json { "Application_ID": 128401, "Application_Status": "Booked", "Unit_ID": "9f3c1a52-…", "Lead_Date_Created": "2026-10-28 16:02:11", "Updated_At": "2026-11-03 14:22:05" } ``` - Store the newest `Updated_At` you received and send it as `updated_since` next time. A small overlap (a few minutes) is safe: de-duplicate by `Application_ID`, `Showing_ID`, `Property_ID` or `Billing_ID`. - With `updated_since` and no `start_date`, the January 1 default does not apply: you get every change, whatever the creation date. - Archiving a lead or deactivating a unit is a change: the row comes back with its new status. - Records not modified since this was introduced (November 2026) report their creation date as `Updated_At`. - New leads can also be pushed to you with the `lead.received` webhook (§6). **Pagination** (every `/reports/*` endpoint except `/reports/syndication`): add `page` and `page_size` (max 5000). Without them the full report is returned — subject to a 6 MB response cap: beyond it the API answers `413` with a suggested page size instead of failing opaquely. Paginated responses include: ```json "count": 1000, "total_rows": 5005, "pagination": { "page": 1, "page_size": 1000, "total_pages": 6, "has_more": true } ``` **`GET /reports/properties`** — your full unit portfolio (the "property list"). ``` GET /reports/properties?company_id=128 Authorization: Bearer X-Company-Password: ``` Real response: ```json { "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 } ] } ``` **`GET /reports/leads`** — prospect/applicant detail across the account (richer than §3.7: includes rating, tenancy-application and approval status). ``` GET /reports/leads?company_id=128&start_date=2026-08-01&end_date=2026-08-31 Authorization: Bearer X-Company-Password: ``` Real response row (personal details anonymized; the body envelope is the same as every report: `success`, `report_type`, `company_id`, `count`, `total_rows`, `data`): ```json { "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. **`GET /reports/showings`** — showing/viewing activity per unit, including which team member ran it and the lead source. ``` GET /reports/showings?company_id=128&page=1&page_size=500 Authorization: Bearer X-Company-Password: ``` Real response row: ```json { "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 } ``` Each row is **one showing**; `Showing_ID` is unique and is the key to use when you reconcile this report with your own records. `Event_ID` / `Event_Name` identify the booking link the showing was booked through. Real row from a building-level link (lead columns omitted): ```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" } ``` 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 `GET /reports/leads`: | `Booking_Scope` | What the booking link offers | Unit shown on the row | |---|---|---| | `unit` | One unit | That unit | | `building` | The whole building; the prospect picks a unit while booking | The unit the prospect picked | Until 3.7, rows from building-level links showed the building's first unit regardless of the unit the prospect picked, and when two showings shared the same time slot the lead columns of one could be copied onto the other. Both are fixed; if you cached the report before 2026-10-21, re-pull it. > 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. **Pre-qualification** (since 3.11). Every row of `/reports/leads` and `/reports/showings` carries `Prequalification_Result`, from the answers to the booking link's pre-qualification questions (§4.3): | `Prequalification_Result` | Meaning | |---|---| | `Qualified` | Every answer passed its question's rules | | `Not Qualified` | At least one answer did not pass. A visit may still have been booked: check `Showing_Status` | | `null` | No answers: the booking link has no questions, or the lead never went through it | Add `include_answers=true` to also get the answers. `Passed` is `false` on the answers that did not pass: ``` GET /reports/leads?company_id=128&start_date=2026-10-01&include_answers=true ``` ```json { "Application_ID": 128401, "Application_Status": "Booked", "Unit_ID": "9f3c1a52-…", "Prequalification_Result": "Not Qualified", "Prequalification_Answers": [ { "Question": "Do you own any pets?", "Answer": "Yes, 2 dogs", "Passed": false }, { "Question": "When are you looking to move in?", "Answer": "December 1", "Passed": true } ] } ``` - In `/reports/showings` the answers are the ones given for that showing. In `/reports/leads` they are the ones from the lead's most recent booking with answers. If a question was answered more than once, only the latest answer is returned. - **Leads screened out by the questions show up only in `/reports/leads`.** No visit was booked for them, so their showing row carries a placeholder `Date_start` (`1900-01-01` or `2000-10-01`) and falls outside the showings date filter. **Which ID is which.** Use these to join the reports with each other and with your own records: | What | `/reports/showings` | `/reports/leads` | `/reports/properties` | `/properties/leads`, `lead.received` | |---|---|---|---|---| | The unit | `Unit_ID` | `Unit_ID` | `Property_ID` | `property_id` | | The building | `Property_ID` | — (`Property_Name` only) | — (`Property_Name` only) | — | | The lead | `Application_ID` | `Application_ID` | — | `application_id` | | The person | `Person_ID` | `Person_ID` | — | `person_id` | | The showing | `Showing_ID` | — | — | — | `Property_ID` means the **building** in the showings report and the **unit** in the properties report. ### 5.4 Field values in the reports (real, from production data) Use these for filters, badges and validation. They come from the live data, not from a spec — if a new value appears it will be additive. **`/reports/showings`** | Field | Values | |---|---| | `Unit_Type` | `Apartment`, `House`, `Basement suite`, `Duplex/triplex`, `Townhouse`, `Other` (a raw numeric code may appear on legacy units) | | `Unit_status` | `Vacant`, `Occupied`, `On Hold`, `On Notice`, `Coming Soon` | | `Unit_publish_status` | `Published`, `Unpublished` | | `Showing_Status` | `Scheduled`, `Confirmed`, `Cancelled`, `Waitlisted`, `Disqualified` (the lead went through the booking link but no visit time was booked; these rows carry a placeholder `Date_start`, so follow them in `/reports/leads` with `Prequalification_Result`) | | `Prequalification_Result` | `Qualified`, `Not Qualified`, `null` (§5.3) | | `Application_Status` | The lead's stage, with the dashboard's names (table below) | | `TeamMemberRol` | `Member`, `Admin` | | `Lead_Source` | Where the lead came from: the applicant's origin, or the showing's own origin when the applicant has none. `Manual`, `Leasey`, `Kijiji`, `Rentsync`, `Zillow`, `Facebook`, `Apartments`, `Zumper`, `CompanyWebsite`, `PadMapper`, `NotIdentified`, `null`. Same name as in `/reports/leads` | | `Lead_source` | **Deprecated**, kept so existing integrations keep working: the applicant's origin only, without the fallback, so it can be `null` where `Lead_Source` is not. Use `Lead_Source` | **`/reports/leads`** | Field | Values | |---|---| | `Application_Status` | The lead's stage (table below) | | `Applicant_Rating` | `0`–`5` (0 = not rated) or `null` | | `Prequalification_Result` | `Qualified`, `Not Qualified`, `null` (§5.3) | | `Tenancy_Application_Status` | `Not_Completed`, `Completed` | | `Lead_Approval_Status` | `In review`, `Approved`, `Denied`, `Cancelled` | | `Unit_Publish_Status` | `Published`, `Unpublished` | | `Vacancy_Status` | `Vacant`, `Occupied` | **`/reports/properties`** | Field | Values | |---|---| | `Unit_Type` | `Apartment`, `House`, `Basement suite`, `Townhouse`, `Duplex/triplex`, `Other`, `Unknown` | | `Unit_Status` | `Active`, `Archived` | | `Unit_Publish_Status` | `Published`, `Unpublished` | | `Rental_Space` | `Entire place`, `Partial place`, `Single room` | | `Lease_Term` | `Fixed term, renewing month-to-month`, `Fixed term`, `Month to month`, `Unknown` | | `Country` | `Canada`, `US` | **Lead stages.** The same stage has two sets of names: the lead endpoints use the first one and the showings report uses the dashboard's names. | Stage | `/reports/leads` `Application_Status`, `/properties/leads` and `lead.received` `status` | `/reports/showings` `Application_Status` | |---|---|---| | New lead, nothing else yet | `New Lead` | `Lead` | | Went through your booking link: booked a showing, joined the waitlist or answered the pre-qualification questions (`/reports/showings` says which) | `Booked` | `Booked` | | Application started | `Application In Progress` | `Applying` | | Application in review | `Application In Review` | `Checking References` | | Signing the lease | `Signing Lease` | `Signing Lease` | | Tenant | `Active Tenants` | `Active Tenant` | | Archived, from any stage | `Archived Lead` | `Archived Lead` | Until 3.10, `/reports/leads` returned the raw value `3` instead of `Booked`, and `/properties/leads` and `lead.received` called that stage `Application Received`. ### 5.5 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` (table in §5.4). - 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++@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 ` 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. | ## Part 6 — Webhooks reference ### 6.1 Events Push delivery for syndication, leads, and the leasing pipeline that follows a lead. Polling (§3.7, `updated_since` in Part 10) still works — webhooks are optional and complementary. | Event | Payload core | Fires when | |---|---|---| | `unit.published` | property_id, marketplace, active, **charge** | A marketplace activation completes (charge included — no separate event) | | `unit.unpublished` | property_id, marketplace, active | A deactivation completes | | `lead.received` | full lead (same fields as `/properties/leads`) | A new lead is created for your company (delivered within ~5 min) | | `application.stage_changed` | application_id, person_id, property_id, `stage`, `previous_stage`, `archived`, `previous_archived`, changed_at | An application changes stage (`booked` → `application_in_progress` → … → `active_tenant`) or is archived or unarchived. A new lead at New Lead is announced by `lead.received`, not by this event | | `application.decision_changed` | application_id, person_id, `decision`, `previous_decision`, changed_at | The application is approved, denied, cancelled, or back to `in_review` | | `screening.status_changed` | check_id, application_ids, type, provider, `status`, `previous_status`, report_available, completed_at | A screening check is ordered or changes status (for example `in_progress` → `completed`). Never the report itself | | `lease.status_changed` | lease_id, property_id, `status`, `previous_status`, archived, tenant_person_ids, sent_at, signed_at | A lease is created or changes status (`draft` → `sent_for_signatures` → `active`) or is archived | | `document.status_changed` | document_id, lease_id, property_id, `status`, `previous_status`, signer_count, signed_count, sent_at, completed_at, changed_at | An e-signature document is created or changes status (`draft` → `waiting_signatures` → `completed`, or `void` / `archived`). One signer signing does not change the status; `signed_count` says how many have signed. Never templates | The five leasing events use the same names and values as Part 10, 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 Part 10), 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 } } ``` ### 6.2 Register your endpoint — PUT /webhooks and GET /webhooks **`PUT /webhooks`** — creates or updates your single webhook (one per company). ```json { "company_id": 128, "password": "", "url": "https://partner.example.com/leasey/webhooks", "events": ["unit.published", "unit.unpublished", "lead.received"] } ``` Response (the `secret` appears **only** when it is first created or when you send `"regenerate_secret": true` — store it, it cannot be read back): ```json { "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`** (with an API key; legacy credentials go as `company_id` and `password` query parameters) — current config plus the last 20 deliveries so you can debug your receiver without asking us. Real response: ```json { "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): ```json { "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. ### 6.3 Delivery format and signature Events are HTTPS POSTs (JSON), signed per the [Standard Webhooks](https://www.standardwebhooks.com) spec: ``` POST 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` (§3.7), so one parser serves both the pull and the push path: ```json { "id": "msg_902", "event": "lead.received", "created_at": "2026-08-26T21:15:02Z", "data": { "application_id": "117278", "application_id_number": 117278, "property_id": "80240cf8-0666-484a-af80-02d6a2133522", "source": "kijiji", "first_name": "Maria", "last_name": "Prospect", "email": "maria.prospect@example.com", "phone": "+15551234567", "status": "Lead", "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 } } ``` An `unit.unpublished` delivery has the same `data` shape as `unit.published` minus the `charge` (`active: false`). Verify: compute `HMAC-SHA256(secret_after_whsec_, "{id}.{timestamp}.{raw_body}")`, base64-encode it, and compare with the value after `v1,`: ```python 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]) ``` ### 6.4 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). - Events are delivered by a job that runs **every 5 minutes**, so expect up to ~5 minutes between the action and the delivery — not instant. - `lead.received` starts with the **first delivery run after you register** (within ~5 minutes): leads created before that run are not sent, and history is never replayed — use `/properties/leads` for backfill. ### 6.5 Testing your webhook Before your own receiver is ready, point the webhook at a request inspector such as [webhook.site](https://webhook.site): it gives you a public HTTPS URL instantly and shows every request it receives — headers, body and timing — with no server to build. It is the usual way to see real deliveries while you develop. To test your own receiver from your machine, expose it with a tunnel such as `ngrok` instead. 1. Open webhook.site and copy **your unique URL**. 2. `PUT /webhooks` with that URL and the events you want. **Store the `secret`** from the response — it is shown only once. 3. Trigger an event: publish a free marketplace on a unit (for example `zillow`) → within ~5 minutes you see a `POST` with the `unit.published` body and the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers (§6.3). Unpublish it to see `unit.unpublished`. 4. For `lead.received`, wait ~5 minutes after registering (one delivery run), then create a new lead on one of your units. 5. `GET /webhooks` lists each delivery with its status (`delivered`, `pending`, `failed`) and the response code your URL returned. 6. When you're done, point the webhook at your real receiver (`PUT /webhooks` again) or pause it with `"is_active": false`. > ⚠️ **Sandbox only.** Anyone who has a webhook.site URL can read everything > sent to it, and `lead.received` carries the lead's name, email and phone. > Use inspectors only in the sandbox (§1.3) with test data — never register a > public inspector URL on a production account. ## Part 7 — Field catalogs (the `property` object) Fields accepted in the `property` object (create and update). Your side maps its own model into these; Leasey maps them onward into each marketplace feed. **Required on create** | Field | Type | Notes | |---|---|---| | `name` | string | Unit display name | | `address` | string | Street address | | `city` | string | City name (resolved against Leasey's city catalog) | | `province_state` | string | e.g. `"BC"`, `"Ontario"`, `"FL"` | | `country` | string | `"Canada"` or `"United States"` | **Optional — identity & location**: `suite_unit`, `postal_code`. **Optional — building & floorplan** (for multi-unit properties): - `building_name` + `number_of_units` — groups the unit into a building (created if it doesn't exist; the response echoes `building_id`). The building **inherits the unit's location** — address, city, province, country, postal code and coordinates — so building-level showings, feeds and emails always have an address. Send the same `building_name` on every unit of that building and they all land in the same `building_id`; a building that already exists with blank location fields is completed from the next unit you create (existing values are never overwritten). - `building_id` — attach to a building you already created (from a previous response) instead of matching by name. - `building_amenities` — array of `{id, value}` (value 1 included · 2 available · 3 not available); amenities that belong to the building rather than the unit. Requires `building_id`, `building_name` or `number_of_units` in the same request. Id catalog: 1 Bike storage · 2 Concierge · 3 Electrical car charging station · 4 Fitness center/gym · 5 Hot tub · 6 Sauna · 7 Steam room · 8 Swimming pool · 9 Common areas · 10 Community laundry · 11 Elevator · 12 On-site manager. - `floorplan_id` — integer id of an existing floorplan, **or** `floorplan` — a string name or an object `{name, bedrooms, bathrooms, size_sqft}`; the floorplan is created under the building if it doesn't exist and the response echoes `floorplan_id`. **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` (number), `bathrooms` (number), `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; value 1 included in rent · 2 available (extra) · 3 not available), `restrictions` (array of ids: 1 no dogs · 2 no smoking · 3 no short-term · 4 no subletting · 5 no cats). Feature id catalog (unit amenities & utilities): | id | Feature | Type | id | Feature | Type | |---|---|---|---|---|---| | 1 | Air conditioning | Amenity | 20 | Electricity | Utility | | 2 | Balcony / patio | Amenity | 21 | Internet | Utility | | 3 | Basement | Amenity | 22 | Natural gas | Utility | | 4 | Bike storage | Amenity | 23 | Water | Utility | | 5 | Concierge | Amenity | 24 | Common areas | Amenity | | 6 | Dishwasher | Amenity | 25 | Community laundry | Amenity | | 7 | Electrical car charging station | Amenity | 26 | Elevator | Amenity | | 8 | Fireplace | Amenity | 27 | On-site manager | Amenity | | 9 | Fitness center / gym | Amenity | 28 | In-suite dryer | Amenity | | 10 | Furnished | Amenity | 29 | Food waste disposal | Amenity | | 11 | Heating | Amenity | 30 | Freezer | Amenity | | 12 | Hot tub | Amenity | 31 | Microwave | Amenity | | 13 | In-suite washer | Amenity | 32 | Oven | Amenity | | 14 | Parking | Amenity | 33 | Refrigerator | Amenity | | 15 | Sauna | Amenity | 34 | Stove | Amenity | | 16 | Steam room | Amenity | 35 | Garbage disposal | Utility | | 17 | Storage locker | Amenity | 36 | Satellite TV | Utility | | 18 | Swimming pool | Amenity | 37 | Sewage | Utility | | 19 | Cable | Utility | | | | Example: `{"id": 6, "value": 1}` = dishwasher included in the rent; `{"id": 20, "value": 2}` = electricity available but paid separately. **Optional — people**: - `owner_contact` — object with `first_name`, `last_name`, `email`, `phone`, `dial_code`, `notify_email` (bool), `notify_sms` (bool); the notification recipient for this unit. **Every notification the platform sends about this unit — publish/unpublish events, showing cancellations on deactivation, lead-related notices — uses this stored contact (when `notify_email` is true), never anything from a later request.** The enable/disable payloads (§3.6) carry no email field at all, and any unexpected field in them is ignored — so set `owner_contact` correctly at creation (or update it via `PUT`) and notifications are handled. - `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: ```json "team_members": [ { "team_member_id": 45 }, { "email": "agent@acmepm.com", "first_name": "Ana", "last_name": "Ruiz" } ] ``` **Role — you don't need to send one.** A member created through the API is an **Admin** by default, because the people you create here operate your own portfolio. To set a different role, use any of these (in order of precedence): `role_id` (a numeric id of your account's roles), **`role` by name** (`"Admin"`, `"Member"`, `"Showing Coordinator"`, or any custom role of your account), or `is_admin: false` (creates the member with the account's default non-admin role). What the role means in practice: **Admin sees the whole account's portfolio** — every unit, lead, showing and lease; a non-admin sees only what is assigned to them (members created here are always assigned to the unit in the same call). The role and the admin flag are always stored consistently, so the role shown in the Leasey **Team** screen is exactly what you asked for. An unknown `role` or a `role_id` that doesn't belong to your account fails that entry (listed in `skipped`) without aborting the rest. `timezone` is optional. Changing the role of a member who **already exists** is not done through this endpoint (existing members are only assigned to the unit) — edit them in the Leasey dashboard. The response echoes what happened to every entry: ```json "team_members": { "assigned": [45, 61], "created": [ { "team_member_id": 61, "user_id": 5120, "person_id": 131044, "email": "agent@acmepm.com", "role_id": 2, "role": "Admin", "is_admin": true, "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` (§3.6). Marketplace-specific formatting (photo sizes, description length limits, feed-specific enums) is handled by Leasey — you send the canonical fields above once. ## Part 8 — Appendices ### 8.1 FAQ **How do I know the exact cost before publishing?** `GET /properties/{id}/marketplaces` (§3.5) 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 §3.7 and push §6) 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** (§1.3): its billing runs on Stripe test mode, so you can exercise paid marketplaces with a Stripe test card and nothing is ever charged for real, and its listings never reach live marketplaces (except Craigslist — keep it off in the sandbox). Free marketplaces (`zillow`, `facebook`, …) never generate charges in any environment. **Leads: should I poll or use webhooks?** Both work and can run together. Webhooks (`lead.received`) deliver within ~5 minutes with retries; polling §3.7 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`. **Why does a paid marketplace come back `payment_method_required`?** Paid activations need a Stripe billing profile with a saved card on the account. Without one, Leasey could not invoice the charge, so the API refuses the paid entry (nothing is changed) and tells you exactly what is missing. Read `billing.ready` and `billing.message` from `GET /properties/{id}/marketplaces` (§3.5) before showing paid toggles; free marketplaces always work. The sandbox account behaves the same way — it has no card, so you can test the refusal path there and the full paid path once a card is on file. **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. ### 8.2 Glossary | Term | Meaning | |---|---| | **Unit / Listing / Property** | The rental unit being syndicated (one `property_id`) | | **Marketplace** | External portal: Zillow, Kijiji, Facebook Marketplace, Apartments.com, Zumper, etc. | | **Syndication** | Publishing a unit to one or more marketplaces via Leasey's feeds | | **Activation** | Turning a marketplace on for a unit (may create a charge) | | **Daily rate** | Per-day price of a paid marketplace, set per partner account | | **First month** | `daily_rate × 30`, billed in full on activation | | **Proration** | Partial-period billing at the daily rate after deactivation | | **Partner account** | Your single Leasey account under which all your clients' units live | ### 8.3 Integration checklists Use this to track your integration end to end: - [ ] Credentials received (API token + company password) and stored server-side - [ ] `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 (§6.3); secret stored on first response - [ ] Webhook receiver answers 2xx within 8 s; delivery states monitored via `GET /webhooks` Booking integrations (voice agents, chatbots — §4): - [ ] `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 - [ ] Load test with your expected unit volume ### 8.4 Best practices **Security** - Call the API only from your backend. Never expose the token or company password in browser or mobile code. - On `GET`, prefer the `X-Company-Password` header over the query string so the password does not land in access logs, proxies or browser history. - Keep the token in environment variables, never in source control. Rotate the company API password if you suspect exposure. - Verify webhook signatures (§6.3) 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 rather than fetching per page view. - Design for a 60-second client timeout — reports on large accounts and cold starts can take several seconds. - Batch marketplace toggles into one `PUT`; load the showings catalog once instead of searching per call. **Reliability** - Retry `5xx` with exponential backoff (2 s, 4 s, 8 s — three attempts). Do not retry `4xx`: fix the request. - After a write that timed out, read the state (`GET /showings/{slot_id}`, `GET /properties/{id}/marketplaces`) before retrying — writes are idempotent, but confirm rather than assume. - Persist `property_id` on `201` immediately; persist `slot_id` and `application_id` when you book. - Treat every field as nullable. **Data consistency** - `photos`, `features`, `restrictions` are **replaced** on update — send the full current set, not the delta. - `team_members` on update is **additive**; remove assignments from the Leasey dashboard. - Marketplace names are canonical (`zillow`, `apartmentsPremium`, `lq`, `lespac`, `immediateRent`); aliases are accepted but normalized. ### 8.5 Support Contact your Leasey technical contact with: - the `request_id` of the response (in the body of every error, and in the `X-Request-Id` header of every response) - your `company_id` - the full request (credentials removed) and the exact response (`code`, `message`, status code) API access is included with every Leasey account, with no usage quota. Sandbox credentials and the `openapi.yaml` file go through the same channel. ### 8.6 Changelog | Version | Date | Changes | |---|---|---| | 3.18 | 2026-11-06 | **New:** every value now has one type across the API. The older fields sent as text get a twin with the same value as a number: `application_id_number`, `person_id_number` and `rent_amount_number` in `/properties/leads` and `lead.received`; `price_number`, `bedrooms_number` and `bathrooms_number` in `/showings/lookup`; `slot_id_number` in the reschedule response; `Parking_Stalls_Number` in `/reports/showings`. The text fields stay and are deprecated (§1.4 "Data types"). `rent_amount` in the property object is documented as a number, which it always accepted | | 3.17 | 2026-11-06 | **New:** leasing writes (§10.7–10.10). `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 §10.10 | | 3.16 | 2026-11-06 | **New:** `document.status_changed` webhook: an e-signature document is created or changes status (sent for signatures, completed, voided, archived), with how many signers have signed (§6.1, `DocumentStatusChangedEvent`) | | 3.15 | 2026-11-06 | **New:** read applications, screening checks, leases and e-signature documents (Part 10): `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` (§6.1). API keys can be limited to some resources with `resources` (§1.2); keys without it keep working everywhere | | 3.14 | 2026-11-05 | **New:** `GET /health`, with no credentials, for your own monitoring (§1.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 (§5.5), with the same row schemas in `openapi.yaml`, which now types every response and accepts an API key alone on every company route (`company_id` and `password` are legacy and optional); `Lead_source` in `/reports/showings` is deprecated in favor of `Lead_Source`; `phone` is required in `POST /showings`; the request timeout is 29 s; `/reports/syndication` is not paginated | | 3.13 | 2026-11-05 | **New:** Leasey's own domains with the version in the path: `https://api.leasey.ai/v1` (production) and `https://sandbox.api.leasey.ai/v1` (sandbox), TLS 1.2+; versioning policy (§1.3). Every response carries `Strict-Transport-Security`, organization routes included. The previous `execute-api` base URLs keep working unchanged | | 3.12 | 2026-11-04 | **New:** every error carries a stable machine-readable `code` and the `request_id`, and every response the `X-Request-Id` header (§1.5); errors answered by the gateway itself (unknown route, throttling, timeout) use the same JSON format; protective throttle with `429` and `Retry-After` (§1.4); `Idempotency-Key` on every write (§1.7); `updated_since` and `Updated_At` on `/reports/leads`, `/reports/showings`, `/reports/properties` and `/reports/billing` for incremental sync (§5.3); the order of every report is documented and stable (§1.4); 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` (§1.2). Additive: `error`, `message` and every existing field keep their values | | 3.11 | 2026-11-03 | **New:** `Prequalification_Result` (`Qualified` / `Not Qualified`) on every row of `/reports/leads` and `/reports/showings`, and the answers themselves with `include_answers=true` (§5.3). Leads screened out by the questions can now be followed in `/reports/leads`. Additive | | 3.10 | 2026-11-03 | **Fixed:** `/reports/leads` returned the raw value `3` for leads that went through the booking link; it now returns `Booked`. `/properties/leads` and `lead.received` called that stage `Application Received`; they now return `Booked` too. **New:** `Unit_ID` on `/reports/leads`. **Docs:** lead stages with both sets of names (§5.4), which ID is which across reports and what each date filter applies to (§5.3), a reschedule keeps the showing's id (§4.9), pre-qualification answers are not returned by the API (§4.3), `Disqualified` showing status; removed the mention of `/reports/applications`, which does not exist | | 3.9 | 2026-10-29 | **New:** per-company API keys (`lsy_live_…`) accepted by every endpoint in place of the shared token + company password; *Full access* and *Read only* keys; created and revoked by company administrators in the dashboard (Profile → Developers) or rotated through `GET/POST/DELETE /api-keys`; email to administrators on every new key. Responses carry `Strict-Transport-Security`. Legacy credentials keep working unchanged | | 3.8 | 2026-10-29 | **New:** Organizations (Part 9) — 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 now carry `company_id`, `source`, `billing_item_id` and, for organization companies, `organization_id` metadata. Additive — nothing existing changed | | 3.7 | 2026-10-21 | **Fix:** `GET /reports/showings` — on building-level booking links the unit columns showed the link's placeholder unit (the building's first unit) instead of the unit the prospect booked, so showings and leads for the same person pointed at different units. Lead columns could also 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 — no existing column changed name or type | | 3.6 | 2026-09-22 | **Fix:** `GET /showings/availability` returned zero dates for rolling events (the majority) because it read the event's stored `dateto` as the end of the window instead of `today + rolling days`; it now matches the booking widget exactly. Documented how far ahead each event type offers, and that unpublished events return `404` rather than an empty list | | 3.5 | 2026-09-18 | Team members created through the API are **Admin by default** — no `role` needed in the request. Send `role`, `role_id` or `is_admin: false` for anything else | | 3.4 | 2026-09-11 | Team members: `role` by name (`Admin`, `Member`, `Showing Coordinator`) and `is_admin` now set the role actually shown in the platform — previously `is_admin: true` left the member as `Member`; the response echoes `role`/`role_id`/`is_admin`. The paid-marketplace payment check no longer has exempt accounts: it applies to every account, test ones included (`billing.exempt` removed from the catalog response). Marketplace directory now 23 with `streetEasy` | | 3.3 | 2026-08-27 | API-triggered publish/unpublish now fires the same notification flow as the platform UI (internal alert; on deactivation, showing cancellations and lead/owner/tenant/team notices per the unit's settings); billing-parity and notification-source notes added to the docs | | 3.2 | 2026-08-27 | Buildings created via `building_name` now inherit the unit's address/city/province/country/postal/coordinates (existing buildings with blank location are completed); duplicate unit `name` returns `409 Conflict` with the existing `property_id` instead of a 500 | | 3.1 | 2026-08-27 | **Payment-method guard**: paid marketplace activations are refused with `code: payment_method_required` when the account has no Stripe billing profile / saved card; catalog exposes `billing.ready` + `message` to check beforehand. Free marketplaces unaffected | | 2.0 | 2026-08-26 | **Complete guide**: Showings API integrated (find, questions, availability, schedule, waitlist, list, status, reschedule, cancel, confirm) with real responses; single document for every external endpoint | | 1.2 | 2026-08-26 | Money-flow diagram, `lead.received` webhook example, operational limits table, FAQ, PII anonymized in examples, full field catalogs | | 1.1 | 2026-08-26 | Billing ledger (`/reports/billing`), webhooks implemented (§6), pagination + 413, real 422 and `GET /webhooks` examples, §7 completed against the code | | 1.0 | 2026-08-26 | Initial partner guide: catalog with prices, charge confirmation, consolidated report, leads with `property_id`/`source` | --- ## Part 9 — Organizations reference (one key, many companies) ### 9.1 What an organization is An **organization** is a platform that manages several Leasey companies on behalf of its own clients — for example a property-management system whose customers are each a separate company in Leasey. With one organization key it can create companies, collect each company's card, manage their units and showings, switch marketplaces on and off in bulk, and read their reports. - Every company stays a normal Leasey company: its own units, leads, showings, team, Stripe customer and invoices. Nothing about a company changes because it belongs to an organization. - A company belongs to **at most one** organization. - **Each company pays with its own card.** Marketplace charges of a company are invoiced to that company's Stripe customer, exactly as for a direct client. - Companies created through the organization API start as **external clients**: their users see the reduced dashboard (Properties, Syndication, Screening, Team) because the operational work happens in your platform. - An organization can only work with companies it created or that Leasey linked to it. It can never claim an existing Leasey company by itself; ask your Leasey contact to link one (§9.10). ### 9.2 Getting access and authenticating Leasey creates the organization and hands you its first key. Keys look like this and are shown **once**: | Environment | Base URL | Key prefix | |---|---|---| | Sandbox | `https://sandbox.api.leasey.ai/v1` | `lsy_org_test_` | | Production | `https://api.leasey.ai/v1` | `lsy_org_live_` | The previous base URLs (§1.3) keep working for organizations too. Every organization request sends only the key — no `company_id` or company password: ``` GET /org Authorization: Bearer lsy_org_live_<40 characters> ``` ```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 } } ``` Leasey stores only a SHA-256 hash of the key. If you lose it, create a new one (§9.3) — nobody can recover it. ### 9.3 Rotating keys without downtime An organization can hold several active keys. To rotate: 1. `POST /org/keys` — create the new key (shown once). 2. Deploy the new key in your system. 3. `GET /org/keys` — check `last_used_at` of the old key stops moving. 4. `DELETE /org/keys/{key_id}` — revoke the old key. ``` POST /org/keys Authorization: Bearer lsy_org_live_... Content-Type: application/json { "label": "production 2026-Q4" } ``` ```json { "success": true, "key": { "id": 12, "label": "production 2026-Q4", "key_prefix": "lsy_org_live_", "key_last4": "Q8x2", "created_at": "2026-10-29T18:10:00", "api_key": "lsy_org_live_3fX…Q8x2" }, "warning": "Store api_key now; it is shown only once. Revoke the old key after you switch." } ``` `GET /org/keys` lists every key without the secret (`key_last4`, `created_at`, `last_used_at`, `last_used_ip`, `revoked_at`) and tells you which one made the call (`current_key_id`). The last active key cannot be revoked (`409`): create its replacement first. ### 9.4 Create a company — POST /org/companies One call creates everything a company needs to work in Leasey: - the company, with the denial and cancellation reasons its team uses; - its **administrator** user (Admin role) — no password is set and **no email is sent**; the administrator signs in with *Forgot password* on the Leasey login page whenever you decide to give them access; - its **Stripe customer**, labelled with your organization (§9.9); - the link to your organization. ``` POST /org/companies Authorization: Bearer lsy_org_live_... Content-Type: application/json { "company": { "company_name": "Maple Street Rentals", "email": "office@maplestreet.example", "time_zone": "Eastern Time (ET)", "currency": "CAD", "language": "en", "homepage": "https://maplestreet.example" }, "admin": { "email": "jane.doe@maplestreet.example", "first_name": "Jane", "last_name": "Doe" } } ``` | Field | Required | Values | |---|---|---| | `company.company_name` | yes | text | | `company.url_slug` | no | defaults to the name in lowercase with hyphens; send it only if the name is taken | | `company.email` | no | billing and contact email (used on the Stripe customer) | | `company.time_zone` | no | `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)` | | `company.currency` | no | `CAD` (default) or `USD` | | `company.language` | no | `en` (default) or `fr` — language of emails and SMS to prospects | | `company.external_client` | no | `true` (default) shows the reduced dashboard; `false` the full one | | `admin.email` | yes | must not already have a Leasey login | | `admin.first_name` / `admin.last_name` | first name yes | text | Response `201`: ```json { "success": true, "company": { "company_id": 412, "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": 2210, "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_R2x…", "readiness": { "ready": false, "checks": { "admin_user": true, "stripe_customer": true, "payment_method": false, "reasons_seeded": true, "api_credentials": true, "time_zone": true }, "missing": ["payment_method"], "payment_method": null, "external_client": true }, "next_steps": [ "POST /org/companies/412/payment-setup-link to collect the company's card.", "POST /org/companies/412/properties to add units." ] } ``` | Status | When | |---|---| | `201` | Created | | `409` | `url_slug` already taken in Leasey (send a different `company.url_slug`), or `admin.email` already has a Leasey login | | `422` | Validation failed; `details` lists every field problem | If Stripe is unavailable the company is still created and the response carries a `warnings` entry; call `POST /org/companies/{id}/onboarding` to finish it (§9.6). ### 9.5 Collect the company's card — POST /org/companies/{company_id}/payment-setup-link Card numbers never pass through your system or Leasey. This returns a Stripe page where the company (or you, on its behalf) saves the card: ``` POST /org/companies/412/payment-setup-link Authorization: Bearer lsy_org_live_... Content-Type: application/json { "success_url": "https://app.acme.example/clients/412/billing?done=1", "cancel_url": "https://app.acme.example/clients/412/billing" } ``` ```json { "success": true, "company_id": 412, "url": "https://checkout.stripe.com/c/pay/cs_live_…", "expires_at": 1761850000, "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." } ``` - Both URLs are optional and must be `https`. Without them the page returns to the Leasey dashboard. - The link expires after 24 hours (`expires_at`, Unix time). Ask for a new one any time; it also replaces a saved card. - When the card is saved, Leasey stores it on that company and marks it as the default for its invoices. From then on `readiness.payment_method` is `true` and the company can activate paid marketplaces. - Leasey also checks Stripe every time you read the company (`GET /org/companies/{id}` or `/readiness`), create or update one of its units, or run a bulk job. A card saved a moment ago counts right away, even if Stripe's notification has not arrived. ### 9.6 Company status — list, detail, readiness, edit | Method | Path | What it does | |---|---|---| | `GET` | `/org/companies` | Every company of the organization with its `readiness` | | `GET` | `/org/companies/{id}` | Company profile, `stripe_customer_id` and `readiness` | | `GET` | `/org/companies/{id}/readiness` | Only the readiness block | | `PATCH` | `/org/companies/{id}` | Edit `company_name`, `email`, `homepage`, `time_zone`, `currency`, `language`; `external_client` only on companies you created | | `POST` | `/org/companies/{id}/onboarding` | Finishes whatever is missing (Stripe customer; administrator if you send `admin`). Safe to repeat | `readiness.ready` is `true` when the company has an administrator, a Stripe customer and a card — that is, when it can create units and activate paid marketplaces. `missing` lists what blocks it. ``` PATCH /org/companies/412 Authorization: Bearer lsy_org_live_... Content-Type: application/json { "company": { "time_zone": "Pacific Time (PT)", "language": "fr" } } ``` ```json { "success": true, "updated": ["time_zone", "language"] } ``` A company that does not belong to your organization always answers `404`, whether it exists or not. ### 9.7 Working inside a company — the same endpoints you already know Everything in Parts 3 to 6 works for each of your companies under `/org/companies/{company_id}/…`, with the same bodies and responses. The only difference: you send the organization key and **no** `company_id` or company password — Leasey fills them in for the company in the path (and overwrites any you send). | Organization path | Same as | Reference | |---|---|---| | `POST /org/companies/{id}/properties` | `POST /properties` | §3.3 | | `PUT /org/companies/{id}/properties/{property_id}` | `PUT /properties/{property_id}` | §3.4, §3.6 | | `GET /org/companies/{id}/properties/{property_id}/marketplaces` | `GET /properties/{property_id}/marketplaces` | §3.5 | | `GET /org/companies/{id}/properties/leads` | `GET /properties/leads` | §3.7 | | `GET /org/companies/{id}/showings/lookup` · `/questions` · `/availability` | `GET /showings/…` | §4.2–4.4 | | `GET` · `POST /org/companies/{id}/showings` | `GET` · `POST /showings` | §4.5–4.7 | | `GET /org/companies/{id}/showings/{slot_id}` | `GET /showings/{slot_id}` | §4.8 | | `PUT /org/companies/{id}/showings/{slot_id}/reschedule` | `PUT /showings/{slot_id}/reschedule` | §4.9 | | `POST /org/companies/{id}/showings/{slot_id}/cancel` · `/confirm` | `POST /showings/{slot_id}/cancel` · `/confirm` | §4.10–4.11 | | `GET /org/companies/{id}/reports/{showings,leads,properties,syndication,billing}` | `GET /reports/…` | Part 5 | | `GET` · `PUT /org/companies/{id}/webhooks` | `GET` · `PUT /webhooks` | Part 6 | Example — publish one unit of company 412 on Kijiji: ``` PUT /org/companies/412/properties/39e00f97-c0a0-48f0-9ebb-38f1a50e3f5b Authorization: Bearer lsy_org_live_... Content-Type: application/json { "property": { "marketplaces": [ { "name": "kijiji", "enabled": true } ] } } ``` The response is exactly the one in §3.6, including the charge confirmation and the `payment_method_required` refusal when the company has no card yet. Webhooks are configured per company: register your URL on each company you want events from, and each company gets its own signing secret. The event body does not name the company, so put it in the URL you register — for example `https://hooks.acme.example/leasey?company_id=412` — and verify the signature with that company's secret. ### 9.8 Marketplaces in bulk — POST /org/marketplaces/bulk Switches marketplaces on or off for many units across your companies in one request. It runs in the background: the call answers `202` with a job, and `GET /org/jobs/{job_id}` shows progress and the result of every unit. Each unit goes through the same rules as §3.6: a marketplace already in the requested state is not charged again, a paid marketplace is refused for a company without a card, and every change sends the same notifications and webhooks as a single call. ``` POST /org/marketplaces/bulk Authorization: Bearer lsy_org_live_... Content-Type: application/json { "marketplaces": [ { "name": "kijiji", "enabled": true }, { "name": "zumper", "enabled": true } ], "company_ids": [412, 418], "published_only": true, "dry_run": true } ``` | Field | Required | Meaning | |---|---|---| | `marketplaces` | yes | Same list as §3.6 | | `company_ids` | no | Limit to these companies; default all companies of the organization | | `building_ids` | no | Limit to these buildings | | `property_ids` | no | Limit to these units | | `published_only` | no | `true` (default): only published, non-archived units. `false` also includes unpublished units — a paid activation is charged even if the unit is not published | | `dry_run` | no | `true`: count the matching units per company and stop. Nothing is changed | Dry run response `200`: ```json { "success": true, "dry_run": true, "total": 57, "scope": { "company_ids": [412, 418], "building_ids": null, "published_only": true, "units_by_company": { "412": 31, "418": 26 } } } ``` The same request without `dry_run` answers `202`: ```json { "success": true, "job_id": 88, "status": "queued", "total": 57, "scope": { "company_ids": [412, 418], "published_only": true, "units_by_company": { "412": 31, "418": 26 } }, "status_url": "/org/jobs/88" } ``` ``` GET /org/jobs/88?page=1&page_size=200 Authorization: Bearer lsy_org_live_... ``` ```json { "success": true, "job": { "id": 88, "job_type": "marketplaces_bulk", "status": "completed", "total": 57, "next_index": 57, "succeeded": 55, "failed": 2, "results": [ { "company_id": 412, "property_id": "39e00f97-c0a0-48f0-9ebb-38f1a50e3f5b", "status_code": 200, "ok": true, "marketplaces": [ { "name": "kijiji", "status": "published", "active": true }, { "name": "zumper", "status": "no_change", "active": true } ] }, { "company_id": 418, "property_id": "8ae7647d-24d9-4c91-b271-4a3331a53ed6", "status_code": 200, "ok": false, "marketplaces": [ { "name": "kijiji", "status": "error", "code": "payment_method_required", "active": false } ] } ], "pagination": { "page": 1, "page_size": 200, "total_rows": 57, "has_more": false } } } ``` - `status`: `queued` → `running` → `completed` (or `failed` with `error`). - A unit is `ok: false` when any of its marketplaces came back `error`; fix the cause (for example add the company's card, §9.5) and send a new job for those units — units already in the requested state are skipped without charge. - Up to 5,000 units per job. `GET /org/jobs` lists the last 50 jobs. ### 9.9 Money: billing report and Stripe labels `GET /org/reports/billing` returns every marketplace charge of your companies, with the same rows as §5.2 plus `Company_ID` and `Company_Name`, totals by currency and status, and a breakdown per company. Filters: `start_date`, `end_date`, `status`, `company_id`; paginate with `page` and `page_size` (max 2000 here; the `pagination` object carries `total_rows` and `has_more`). ``` GET /org/reports/billing?start_date=2026-10-01&end_date=2026-10-31&page=1&page_size=500 Authorization: Bearer lsy_org_live_... ``` ```json { "success": true, "report_type": "organization_billing", "organization_id": 7, "companies": 2, "totals_by_currency": { "CAD": { "invoiced": 1260.0, "pending": 90.0 } }, "by_company": [ { "company_id": 412, "company_name": "Maple Street Rentals", "charges": 31, "amount_by_currency": { "CAD": 930.0 }, "summary": { "…": "same as §5.2" } } ], "count": 42, "data": [ { "Company_ID": 412, "Company_Name": "Maple Street Rentals", "Billing_ID": 5120, "Property_Name": "Unit 1 - 93 Grant Ave", "Marketplace": "kijiji", "Amount": 30.0, "Currency": "CAD", "Status": "invoiced", "Stripe_Invoice_ID": "in_1UV…" } ], "pagination": { "page": 1, "page_size": 500, "total_rows": 42, "has_more": false } } ``` In Stripe, the customer, every invoice and its payment carry metadata, so Leasey can reconcile each organization's money directly in Stripe: `company_id`, `organization_id`, `organization_name`, `account_type=organization_member`, `source` and `billing_item_id`. Invoices and receipts that your companies receive are unchanged. ### 9.10 Security, audit and what needs Leasey - **Isolation:** every company route checks that the company belongs to your organization before doing anything; otherwise `404`. - **Audit:** `GET /org/audit?limit=100` returns the latest actions made with your keys (route, company, status, key, IP, time). Request bodies are never logged. - **Suspension:** a suspended organization receives `403` on every call. - **Needs Leasey:** creating the organization and its first key, linking an existing Leasey company to your organization, unlinking a company (its data stays; your access ends) and suspending or reactivating the organization. | Status | Meaning | |---|---| | `401` | Missing, wrong, revoked or expired organization key | | `403` | Organization suspended | | `404` | Unknown route, or the company is not in your organization | | `409` | Conflict (slug or admin email taken, last active key) | | `422` | Validation failed; see `details` | | `502` | Stripe did not answer; retry | | `503` | Organizations not enabled in that environment yet | --- ## Part 10 — Leasing reference (applications, screening, leases, e-signature) What happens after the lead: the application, the screening checks ordered for the applicant, the lease and its signatures. The reads (§10.2–10.6) work with any key, a read-only one included; the writes (§10.7–10.9) need a full-access key. A key limited to resources needs `applications`, `screening`, `leases` or `documents` (§1.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. ### 10.1 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 §10.7–10.9 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. ### 10.2 Applications — GET /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 (§5.3) | ```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. ### 10.3 Screening checks — GET /screening 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. ### 10.4 Leases — GET /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 of §10.2 (and `lease_ids` there points back). - Money is a number; a value that was never entered is `null`, an explicit zero is `0`. ### 10.5 E-signature documents — GET /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 (§10.4). `document.status_changed` (§6.1) 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. ### 10.6 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` (§6.1) 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. ### 10.7 Changing applications — POST /applications, PATCH, /send 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`, §1.2). Every write accepts `Idempotency-Key` (§1.7) 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 §10.2; 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` (§6.1) 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`). ### 10.8 Ordering a screening check — POST /applications/{application_id}/screening 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`, §6.1). 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 §10.3 (`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. ### 10.9 Leases and e-signature — POST /leases, PATCH, /send, /void | 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 §10.4. 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 §10.4. - 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 (§10.5) 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` (§6.1). - `/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. ### 10.10 Error codes of the leasing writes Besides the codes of §1.5: | `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 |