# Finding a lease

Source: https://api-docs.magicdoor.com/walkthrough

How the portfolio, property, unit and lease records fit together, how to walk from one to the next, and what to read once you have the lease you wanted.

Almost everything in MagicDoor hangs off a lease, and almost every integration starts by finding one. This page walks the whole path with real calls, then shows what to read from a lease once you have it.

## How the records nest

```text
portfolio            a book of business — one owner group, one set of books
 └── property        a building or a house
      └── unit       a rentable space; a single-family house still has one
           └── lease an agreement over that unit for a period of time
```

Each record carries the ids of everything above it, which is what makes the shortcut in the next section possible. A lease knows its `unitId`, `propertyId` **and** `portfolioId` — you never have to walk back up.

> **Note: A unit can have more than one lease**
>
> Past leases stay on the unit, and a lease signed for a future move-in exists alongside the current one. "The lease for this unit" is a question about dates, not a lookup — filter by `ended` and the lease-date range rather than assuming a unit has exactly one.

## The direct route

If you already know which portfolio, property or unit you care about, **do not walk the tree**. `GET /company-app/leases` filters on any level directly:

**By portfolio**

```bash
curl --request GET \
  --url 'https://api.portal.magicdoor.com/company-app/leases?portfolioId=1830472847398404096&ended=false' \
  --header "Authorization: Bearer $MAGICDOOR_TOKEN"
```

**By property**

```bash
curl --request GET \
  --url 'https://api.portal.magicdoor.com/company-app/leases?propertyId=1830472847398404097&ended=false' \
  --header "Authorization: Bearer $MAGICDOOR_TOKEN"
```

**By unit**

```bash
curl --request GET \
  --url 'https://api.portal.magicdoor.com/company-app/leases?unitId=1832019384756201984' \
  --header "Authorization: Bearer $MAGICDOOR_TOKEN"
```

There are plural forms too — `portfolioIds`, `propertyIds`, `unitIds` and `tenantIds` — so one call can cover several at once. See [List leases](https://api-docs.magicdoor.com/api-reference/leases/list-leases.md) for the full filter set, which also covers balances, expiry dates, eviction and month-to-month.

## Walking down from the top

When you are exploring rather than looking something up, walk down a level at a time.

**Step 1. List the portfolios**

This one is **not paginated** — it returns a plain JSON array, and each portfolio already has its properties expanded. For a small book of business this single call may be all you need.

```bash
curl --request GET \
  --url 'https://api.portal.magicdoor.com/company-app/portfolios?active=true' \
  --header "Authorization: Bearer $MAGICDOOR_TOKEN"
```

**200 OK (trimmed)**

```json
[
  {
    "id": "1830472847398404096",
    "name": "Riverside Holdings",
    "active": true,
    "propertyCount": 2,
    "activePropertyCount": 2,
    "properties": [
      { "id": "1830472847398404097", "displayName": "Evergreen Terrace" }
    ]
  }
]
```

**Step 2. List that portfolio's properties**

If you need the full property record — unit counts, rent roll, balances — rather than the summary embedded above, ask for it directly. This endpoint **is** paginated.

```bash
curl --request GET \
  --url 'https://api.portal.magicdoor.com/company-app/properties?portfolioId=1830472847398404096&page=1&pageSize=50' \
  --header "Authorization: Bearer $MAGICDOOR_TOKEN"
```

**Step 3. List the property's units**

Filter by `propertyId`. A single-family house has exactly one unit — there is no special case for it in the data model.

```bash
curl --request GET \
  --url 'https://api.portal.magicdoor.com/company-app/units?propertyId=1830472847398404097&page=1&pageSize=50' \
  --header "Authorization: Bearer $MAGICDOOR_TOKEN"
```

**Step 4. List the unit's leases**

Add `ended=false` for the ones still running. Leave it off to see the unit's whole history.

```bash
curl --request GET \
  --url 'https://api.portal.magicdoor.com/company-app/leases?unitId=1832019384756201984&ended=false' \
  --header "Authorization: Bearer $MAGICDOOR_TOKEN"
```

> **Tip: List leases already embeds what you walked for**
>
> `GET /company-app/leases` returns each lease with its unit, property and tenants filled in. So the walk above is worth doing when you want the intermediate records themselves — a property's rent roll, a unit's details — and worth skipping when you only want leases.

## Reading the lease

With a lease id, [Get a lease](https://api-docs.magicdoor.com/api-reference/leases/get-lease.md) returns the record itself:

**GET /company-app/leases/1830472847398404096**

```json
{
  "id": "1830472847398404096",
  "portfolioId": "1827741029384756224",
  "propertyId": "1829933847562019840",
  "unitId": "1832019384756201984",
  "start": "2026-03-01",
  "end": "2027-02-28",
  "ended": false,
  "monthToMonth": false,
  "eviction": false,
  "draft": false,
  "currentRent": 1850.00,
  "balance": 1850.00,
  "postedBalance": 1850.00,
  "securityDepositRequested": 1850.00,
  "securityDepositPaid": 1850.00,
  "securityDepositDue": 0.00,
  "lastPaymentDate": "2026-08-01",
  "earliestUnpaidBillDueDate": "2026-09-01",
  "pastDueDays": 0
}
```

### The three states that are easy to confuse

| Field | Means |
| --- | --- |
| `draft` | Not a real agreement yet. It is being prepared and has not been activated |
| `ended` | Deliberately terminated. A lease past its `end` date is **not** automatically ended |
| `monthToMonth` | Continues past `end` a month at a time rather than expiring |

> **Warning: An expired lease is a normal, ongoing state**
>
> If `end` is in the past and `ended` is `false`, the lease is still live as far as MagicDoor is concerned — the tenant is still there and rent is still owed. Nothing in the system turns expiry into termination on its own.
>
> So do not treat `end < today` as "finished". To find leases that need attention, use [List expiring leases](https://api-docs.magicdoor.com/api-reference/leases/list-expiring-leases.md) rather than comparing dates yourself.

### Balance and postedBalance

`balance` is everything owed. `postedBalance` counts only charges that have actually been posted to the ledger — future-dated charges are excluded. When you want "what does this tenant owe **right now**", `postedBalance` is usually the honest answer.

`pastDueDays` is days since the oldest unpaid bill came due, and is absent when nothing is overdue.

## Going deeper from a lease

The lease record is a summary. Each of these reads one part of it in full, all keyed on the same `leaseId`.

- [Charge ledger](https://api-docs.magicdoor.com/api-reference/lease-charges/get-lease-charge-ledger.md) — Every charge, payment, credit and late fee, with running balances and an asOf date.
- [Deposit ledger](https://api-docs.magicdoor.com/api-reference/lease-deposits/get-lease-deposit-ledger.md) — Security deposit held, paid and refunded, kept separate from rent.
- [Recurring charges](https://api-docs.magicdoor.com/api-reference/lease-charges/list-lease-recurring-charges.md) — The standing charges that generate rent each period.
- [Renewal offers](https://api-docs.magicdoor.com/api-reference/lease-renewals/list-lease-renewal-offers.md) — Offers made on this lease and where each one stands.
- [Documents](https://api-docs.magicdoor.com/api-reference/lease-documents/list-lease-documents.md) — Signed agreements and attachments. Filter the list by leaseId.
- [Automatic payments](https://api-docs.magicdoor.com/api-reference/leases/list-lease-auto-pays.md) — Whether the tenant has autopay set up on this lease.

> **Note: Tenants come with the lease**
>
> There is no "list this lease's tenants" endpoint, because you already have them — both `GET /company-app/leases` and `GET /company-app/leases/{leaseId}` return the lease's tenants inline. The [lease tenants](https://api-docs.magicdoor.com/api-reference/lease-tenants/move-in-tenant.md) endpoints are for changing who is on the lease — moving someone in or out — not for reading them.

## A worked example

Every active lease in one portfolio, with what each tenant currently owes:

**TypeScript**

```typescript
const PORTAL = "https://api.portal.magicdoor.com";

async function arrearsFor(portfolioId: string, token: string) {
  const rows: { lease: string; unit: string; owed: number }[] = [];
  let page = 1;
  let seen = 0;
  let total = Infinity;

  while (seen < total) {
    const url = new URL(`${PORTAL}/company-app/leases`);
    url.searchParams.set("portfolioId", portfolioId);
    url.searchParams.set("ended", "false");
    url.searchParams.set("page", String(page));
    url.searchParams.set("pageSize", "100");

    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!response.ok) throw new Error(`Listing leases failed: ${response.status}`);

    const body = await response.json();
    total = body.totalCount;
    if (body.items.length === 0) break;

    for (const lease of body.items) {
      // postedBalance excludes charges dated in the future.
      if (lease.postedBalance > 0) {
        rows.push({
          lease: lease.id,          // keep ids as strings
          unit: lease.unit?.name ?? lease.unitId,
          owed: lease.postedBalance,
        });
      }
    }

    seen += body.items.length;
    page += 1;
  }

  return rows;
}
```

**Python**

```python
import httpx

PORTAL = "https://api.portal.magicdoor.com"


def arrears_for(portfolio_id: str, token: str):
    rows, page, seen, total = [], 1, 0, None

    while total is None or seen < total:
        response = httpx.get(
            f"{PORTAL}/company-app/leases",
            params={
                "portfolioId": portfolio_id,
                "ended": "false",
                "page": page,
                "pageSize": 100,
            },
            headers={"Authorization": f"Bearer {token}"},
        )
        response.raise_for_status()

        body = response.json()
        total = body["totalCount"]
        if not body["items"]:
            break

        for lease in body["items"]:
            # posted_balance excludes charges dated in the future
            if lease["postedBalance"] > 0:
                rows.append({
                    "lease": lease["id"],          # keep ids as strings
                    "unit": lease.get("unit", {}).get("name") or lease["unitId"],
                    "owed": lease["postedBalance"],
                })

        seen += len(body["items"])
        page += 1

    return rows
```

Note the response field is `currentPage`, not `page` — see [Pagination](https://api-docs.magicdoor.com/pagination.md). The loop above counts records instead, which works either way.

## Next

- [List leases](https://api-docs.magicdoor.com/api-reference/leases/list-leases.md) — Every filter available on the lease list.
- [Pagination](https://api-docs.magicdoor.com/pagination.md) — The two envelopes, and how to walk past page one.
- [API stability](https://api-docs.magicdoor.com/stability.md) — What is and is not promised about this API.
