---
title: Property
description: Manage developments, plots, reservations and call-offs in Signal's optional Property module.
product: signal
section: api
order: 26
---

# Property

The Property module is an optional part of Signal for businesses that sell or supply homes on developments. Its records are:

- **Developments**: a site or project.
- **Plots**: the individual homes or lots on a development.
- **Reservations**: a buyer's hold on a plot.
- **Call-offs**: a client's order for a batch of plots on a development (or for bulk stock), which then moves through checking, dispatch, invoicing and payment.

All Property endpoints use the `property:read` and `property:write` scopes.

Developments and plots support [custom fields](../conventions.md#custom-fields): they include `custom_fields` in responses and accept custom field values on create and update.

---

## Developments

### The development object

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | UUID. |
| `name` | string | |
| `description` | string or null | |
| `location` | string or null | |
| `status` | string | Signal uses `active` (the default), `planning`, `launching` and `completed`. |
| `launch_date` | string or null | ISO 8601 timestamp. |
| `completion_date` | string or null | ISO 8601 timestamp. |
| `image_url` | string or null | |
| `order_number` | string or null | Your order number for the development, up to 100 characters. New call-offs on the development use it as their client order number unless given another. |
| `file_path` | string or null | Where the development's files live (a path or link), up to 1,000 characters. |
| `created_at` | string | ISO 8601 timestamp. |
| `updated_at` | string | ISO 8601 timestamp. |
| `custom_fields` | object | Custom fields for developments, keyed by internal name. |

```json
{
  "id": "1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37",
  "name": "Riverside Gardens",
  "description": "42 homes on the old mill site.",
  "location": "Portland, OR",
  "status": "active",
  "launch_date": "2026-04-01T00:00:00.000Z",
  "completion_date": null,
  "image_url": null,
  "order_number": "PO-7781",
  "file_path": null,
  "created_at": "2026-11-12T09:30:00.000Z",
  "updated_at": "2026-11-12T09:30:00.000Z",
  "custom_fields": { "phase": "2" }
}
```

### List developments

`GET /property/developments` · Scope: `property:read`

Newest first.

| Query parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `status` | string | No | Exact match, such as `active`. |
| `limit` | integer | No | 1 to 100. Default 20. |
| `cursor` | string | No | `meta.cursor` from the previous page. |

```bash
curl "https://crm.empiraa.app/api/v1/property/developments?status=active" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
const res = await fetch("https://crm.empiraa.app/api/v1/property/developments?status=active", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

**Response `200`:** an array of [development objects](#the-development-object) in `data`; `meta` has `cursor` and `hasMore`.

### Create a development

`POST /property/developments` · Scope: `property:write`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `name` | string | Yes | |
| `description` | string or null | No | |
| `location` | string or null | No | |
| `status` | string or null | No | Defaults to `active`. |
| `launch_date` | string or null | No | ISO 8601 date or timestamp. |
| `completion_date` | string or null | No | ISO 8601 date or timestamp. |
| `image_url` | string or null | No | |
| `order_number` | string or null | No | Up to 100 characters. Trimmed; blank means none. |
| `file_path` | string or null | No | Up to 1,000 characters. Trimmed; blank means none. |
| `custom_fields` | object | No | Other unrecognized top-level fields are also treated as custom fields. |

```bash
curl -X POST https://crm.empiraa.app/api/v1/property/developments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Riverside Gardens", "location": "Portland, OR", "order_number": "PO-7781"}'
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/developments", {
  method: "POST",
  headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Riverside Gardens", location: "Portland, OR", order_number: "PO-7781" }),
});
```

**Response `201`:** the new [development object](#the-development-object) in `data`.

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | `name` is missing; a date isn't valid; `order_number` or `file_path` isn't text or is too long. |

**Webhook events:** [`development.created`](../webhooks.md#event-catalog).

### Get a development

`GET /property/developments/{id}` · Scope: `property:read`

```bash
curl https://crm.empiraa.app/api/v1/property/developments/1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/developments/1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

**Response `200`:** the [development object](#the-development-object) in `data`. **`404 NOT_FOUND`** if it isn't in this workspace.

### Update a development

`PATCH /property/developments/{id}` · Scope: `property:write`

Send only the fields you want to change. Takes the same fields as [create](#create-a-development).

```bash
curl -X PATCH https://crm.empiraa.app/api/v1/property/developments/1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "completed", "completion_date": "2026-11-30"}'
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/developments/1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37", {
  method: "PATCH",
  headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ status: "completed", completion_date: "2026-11-30" }),
});
```

**Response `200`:** the updated [development object](#the-development-object) in `data`.

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | A date isn't valid; `order_number` or `file_path` isn't text or is too long. |
| 404 | `NOT_FOUND` | The development isn't in this workspace. |

**Webhook events:** [`development.updated`](../webhooks.md#event-catalog).

### Delete a development

`DELETE /property/developments/{id}` · Scope: `property:write`

A development that has call-offs keeps its billing history, so it can't be deleted (`409 CONFLICT`). Archive it in Signal instead.

```bash
curl -X DELETE https://crm.empiraa.app/api/v1/property/developments/1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/developments/1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37", {
  method: "DELETE",
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

**Response `204`:** no body.

| Status | Code | When |
| --- | --- | --- |
| 404 | `NOT_FOUND` | The development isn't in this workspace. |
| 409 | `CONFLICT` | The development has call-offs. |

---

## Plots

### The plot object

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | UUID. |
| `development_id` | string | |
| `plot_number` | string | Text, so `12A` works. |
| `property_type` | string or null | |
| `bedrooms` | integer or null | |
| `bathrooms` | number or null | |
| `sqft` | number or null | Floor area in square feet. |
| `price` | number | In the workspace's currency. Defaults to 0. |
| `status` | string | Signal uses `available` (the default), `reserved`, `sold` and `unavailable`. |
| `notes` | string or null | |
| `image_url` | string or null | |
| `website_url` | string or null | |
| `created_at` | string | ISO 8601 timestamp. |
| `updated_at` | string | ISO 8601 timestamp. |
| `custom_fields` | object | Custom fields for plots, keyed by internal name. |

```json
{
  "id": "5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51",
  "development_id": "1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37",
  "plot_number": "12",
  "property_type": "Townhouse",
  "bedrooms": 3,
  "bathrooms": 2,
  "sqft": 1450,
  "price": 489000,
  "status": "available",
  "notes": null,
  "image_url": null,
  "website_url": null,
  "created_at": "2026-11-12T09:30:00.000Z",
  "updated_at": "2026-11-12T09:30:00.000Z",
  "custom_fields": {}
}
```

### List plots

`GET /property/plots` · Scope: `property:read`

Newest first.

| Query parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `development_id` | string | No | Only plots on this development. |
| `status` | string | No | Exact match, such as `available`. |
| `limit` | integer | No | 1 to 100. Default 20. |
| `cursor` | string | No | `meta.cursor` from the previous page. |

```bash
curl "https://crm.empiraa.app/api/v1/property/plots?development_id=1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37&status=available" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
const url = new URL("https://crm.empiraa.app/api/v1/property/plots");
url.searchParams.set("development_id", "1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37");
url.searchParams.set("status", "available");
const res = await fetch(url, { headers: { Authorization: "Bearer YOUR_API_KEY" } });
```

**Response `200`:** an array of [plot objects](#the-plot-object) in `data`; `meta` has `cursor` and `hasMore`.

### Create a plot

`POST /property/plots` · Scope: `property:write`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `development_id` | string | Yes | A development in this workspace. |
| `plot_number` | string | Yes | Send as a string, even for numbers. |
| `property_type` | string or null | No | |
| `bedrooms` | integer or null | No | |
| `bathrooms` | number or null | No | |
| `sqft` | number or null | No | |
| `price` | number | No | Defaults to 0. |
| `status` | string or null | No | Defaults to `available`. |
| `notes` | string or null | No | |
| `image_url` | string or null | No | |
| `website_url` | string or null | No | |
| `custom_fields` | object | No | Other unrecognized top-level fields are also treated as custom fields. |

```bash
curl -X POST https://crm.empiraa.app/api/v1/property/plots \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"development_id": "1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37", "plot_number": "12", "bedrooms": 3, "price": 489000}'
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/plots", {
  method: "POST",
  headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    development_id: "1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37",
    plot_number: "12",
    bedrooms: 3,
    price: 489000,
  }),
});
```

**Response `201`:** the new [plot object](#the-plot-object) in `data`.

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | `development_id` or `plot_number` is missing or not a string. |
| 404 | `NOT_FOUND` | The development isn't in this workspace. |

**Webhook events:** [`plot.created`](../webhooks.md#event-catalog).

### Get a plot

`GET /property/plots/{id}` · Scope: `property:read`

```bash
curl https://crm.empiraa.app/api/v1/property/plots/5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/plots/5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

**Response `200`:** the [plot object](#the-plot-object) in `data`. **`404 NOT_FOUND`** if it isn't in this workspace.

### Update a plot

`PATCH /property/plots/{id}` · Scope: `property:write`

Send only the fields you want to change. Takes the same fields as [create](#create-a-plot), except `development_id`, which can't be changed.

```bash
curl -X PATCH https://crm.empiraa.app/api/v1/property/plots/5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "sold"}'
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/plots/5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51", {
  method: "PATCH",
  headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ status: "sold" }),
});
```

**Response `200`:** the updated [plot object](#the-plot-object) in `data`. **`404 NOT_FOUND`** if it isn't in this workspace.

**Webhook events:** [`plot.updated`](../webhooks.md#event-catalog), plus [`plot.status_changed`](../webhooks.md#event-catalog) when `status` changes.

### Delete a plot

`DELETE /property/plots/{id}` · Scope: `property:write`

A plot on a call-off can't be deleted (`409 CONFLICT`).

```bash
curl -X DELETE https://crm.empiraa.app/api/v1/property/plots/5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/plots/5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51", {
  method: "DELETE",
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

**Response `204`:** no body.

| Status | Code | When |
| --- | --- | --- |
| 404 | `NOT_FOUND` | The plot isn't in this workspace. |
| 409 | `CONFLICT` | The plot is on a call-off. |

---

## Reservations

### The reservation object

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | UUID. |
| `plot_id` | string | |
| `contact_id` | string or null | The buyer's Contact, if any. |
| `account_id` | string or null | |
| `buyer_name` | string | |
| `deposit_amount` | number | In the workspace's currency. |
| `total_price` | number | In the workspace's currency. |
| `reserved_at` | string | When the reservation was made. |
| `expires_at` | string | When the hold lapses. |
| `status` | string | `active` for a new reservation, `cancelled` once canceled. |
| `notes` | string or null | |
| `created_at` | string | ISO 8601 timestamp. |
| `updated_at` | string | ISO 8601 timestamp. |

```json
{
  "id": "7f9b1d3e-5a6c-4e8f-8b0d-4c6e8a0c2e73",
  "plot_id": "5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51",
  "contact_id": "a1d4e7b2-5c3f-4e8a-b9d6-0f2c7e1a3b54",
  "account_id": null,
  "buyer_name": "Maria Garcia",
  "deposit_amount": 5000,
  "total_price": 489000,
  "reserved_at": "2026-11-12T09:30:00.000Z",
  "expires_at": "2026-12-12T00:00:00.000Z",
  "status": "active",
  "notes": null,
  "created_at": "2026-11-12T09:30:00.000Z",
  "updated_at": "2026-11-12T09:30:00.000Z"
}
```

### List reservations

`GET /property/reservations` · Scope: `property:read`

Newest first.

| Query parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `plot_id` | string | No | |
| `status` | string | No | Such as `active` or `cancelled`. |
| `contact_id` | string | No | |
| `limit` | integer | No | 1 to 100. Default 20. |
| `cursor` | string | No | `meta.cursor` from the previous page. |

```bash
curl "https://crm.empiraa.app/api/v1/property/reservations?status=active" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/reservations?status=active", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

**Response `200`:** an array of [reservation objects](#the-reservation-object) in `data`; `meta` has `cursor` and `hasMore`.

### Create a reservation

`POST /property/reservations` · Scope: `property:write`

Reserves a plot. The plot's `status` changes to `reserved`.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `plot_id` | string | Yes | A plot in this workspace. |
| `buyer_name` | string | Yes | |
| `expires_at` | string | Yes | ISO 8601 date or timestamp. |
| `contact_id` | string or null | No | |
| `account_id` | string or null | No | |
| `deposit_amount` | number | No | Defaults to 0. |
| `total_price` | number | No | Defaults to 0. |
| `notes` | string or null | No | |

```bash
curl -X POST https://crm.empiraa.app/api/v1/property/reservations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"plot_id": "5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51", "buyer_name": "Maria Garcia", "expires_at": "2026-12-12", "deposit_amount": 5000, "total_price": 489000}'
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/reservations", {
  method: "POST",
  headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    plot_id: "5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51",
    buyer_name: "Maria Garcia",
    expires_at: "2026-12-12",
    deposit_amount: 5000,
    total_price: 489000,
  }),
});
```

**Response `201`:** the new [reservation object](#the-reservation-object) in `data`.

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | `plot_id`, `buyer_name` or `expires_at` is missing, or `expires_at` isn't a date. |
| 404 | `NOT_FOUND` | The plot isn't in this workspace. |

**Webhook events:** none.

### Get a reservation

`GET /property/reservations/{id}` · Scope: `property:read`

```bash
curl https://crm.empiraa.app/api/v1/property/reservations/7f9b1d3e-5a6c-4e8f-8b0d-4c6e8a0c2e73 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/reservations/7f9b1d3e-5a6c-4e8f-8b0d-4c6e8a0c2e73", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

**Response `200`:** the [reservation object](#the-reservation-object) in `data`. **`404 NOT_FOUND`** if it isn't in this workspace.

### Update a reservation

`PATCH /property/reservations/{id}` · Scope: `property:write`

Send only the fields you want to change: `buyer_name`, `contact_id`, `account_id`, `deposit_amount`, `total_price`, `expires_at` (can't be cleared), `status` and `notes`. To cancel, use [`DELETE`](#cancel-a-reservation), which also frees the plot.

```bash
curl -X PATCH https://crm.empiraa.app/api/v1/property/reservations/7f9b1d3e-5a6c-4e8f-8b0d-4c6e8a0c2e73 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expires_at": "2027-01-12", "deposit_amount": 7500}'
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/reservations/7f9b1d3e-5a6c-4e8f-8b0d-4c6e8a0c2e73", {
  method: "PATCH",
  headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ expires_at: "2027-01-12", deposit_amount: 7500 }),
});
```

**Response `200`:** the updated [reservation object](#the-reservation-object) in `data`.

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | `expires_at` is empty or isn't a date. |
| 404 | `NOT_FOUND` | The reservation isn't in this workspace. |

### Cancel a reservation

`DELETE /property/reservations/{id}` · Scope: `property:write`

Sets the reservation's `status` to `cancelled` and the plot's `status` back to `available`. The reservation is kept.

```bash
curl -X DELETE https://crm.empiraa.app/api/v1/property/reservations/7f9b1d3e-5a6c-4e8f-8b0d-4c6e8a0c2e73 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/reservations/7f9b1d3e-5a6c-4e8f-8b0d-4c6e8a0c2e73", {
  method: "DELETE",
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

**Response `204`:** no body. **`404 NOT_FOUND`** if it isn't in this workspace.

---

## Call-offs

A call-off is a client's order for a batch of plots on a development, quoted by its reference (such as `CO-0042`). A call-off moves through these statuses in Signal:

`booked` → `checked` → `scheduled` → `dispatched` → `invoiced` → `paid` (or `canceled`)

Each plot can be on one live call-off per **stage**: `reservation` or `completion`.

A **bulk** call-off is for stock that isn't tied to a development. It's booked for the client's Account with a description and a value you enter, and has no development, plots or stage.

Through the API you can list and read call-offs, book new ones, and record their invoice and payment. Checking, scheduling and dispatching are done in Signal.

### The call-off object

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | UUID. |
| `reference` | string | Such as `CO-0042`. |
| `number` | integer | The number in `reference`. |
| `development_id` | string or null | `null` for a bulk call-off. |
| `development_name` | string or null | |
| `account_id` | string or null | The client Account: the development's developer, or a bulk call-off's own Account. |
| `account_name` | string or null | |
| `developer_account_id` | string or null | Same as `account_id`. |
| `developer_account_name` | string or null | Same as `account_name`. |
| `description` | string or null | What a bulk call-off is for. `null` on a development call-off. |
| `client_order_number` | string or null | The client's purchase order number. |
| `requested_by_contact_id` | string or null | The Contact who requested it. |
| `requested_by_name` | string or null | |
| `requested_by_email` | string or null | |
| `requested_by_phone` | string or null | Their phone, or their mobile when they have no phone. |
| `requested_at` | string | ISO 8601 timestamp. |
| `required_by` | string or null | Calendar day, `YYYY-MM-DD`. |
| `status` | string | `booked`, `checked`, `scheduled`, `dispatched`, `invoiced`, `paid` or `canceled`. |
| `stage` | string or null | `reservation` or `completion`. `null` for a bulk call-off. |
| `value` | number | Plot prices when booked (or the bulk value), in the workspace's currency. Later price changes don't alter it. |
| `notes` | string or null | |
| `plots` | array | Plots currently on the call-off: `id`, `plot_number`, `price`. Empty once canceled. |
| `booked_plot_numbers` | array of strings | Plot numbers as booked. Kept after a cancel. |
| `created_by_user_id` | string or null | `null` when booked through the API. |
| `checked_by_user_id` | string or null | |
| `checked_at` | string or null | |
| `dispatch_date` | string or null | Calendar day, `YYYY-MM-DD`. |
| `dispatched_at` | string or null | |
| `invoice_number` | string or null | |
| `invoiced_at` | string or null | |
| `paid_at` | string or null | |
| `canceled_at` | string or null | |
| `created_at` | string | ISO 8601 timestamp. |
| `updated_at` | string | ISO 8601 timestamp. |

```json
{
  "id": "3a5c7e9b-1d2f-4a4c-8e6b-5d7f9b1d3f84",
  "reference": "CO-0042",
  "number": 42,
  "development_id": "1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37",
  "development_name": "Riverside Gardens",
  "developer_account_id": "3f6c2a1e-8b4d-4c2a-9f1e-2b7d5a9c4e10",
  "developer_account_name": "Contoso Homes",
  "account_id": "3f6c2a1e-8b4d-4c2a-9f1e-2b7d5a9c4e10",
  "account_name": "Contoso Homes",
  "description": null,
  "client_order_number": "PO-7781",
  "requested_by_contact_id": "a1d4e7b2-5c3f-4e8a-b9d6-0f2c7e1a3b54",
  "requested_by_name": "Maria Garcia",
  "requested_by_email": "maria.garcia@contoso.example",
  "requested_by_phone": "+1 312 555 0187",
  "requested_at": "2026-11-12T09:30:00.000Z",
  "required_by": "2026-12-01",
  "status": "booked",
  "stage": "reservation",
  "value": 978000,
  "notes": null,
  "plots": [
    { "id": "5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51", "plot_number": "12", "price": 489000 },
    { "id": "6e8a0c2d-4f5b-4d7e-8a9c-3b5d7f9b1d62", "plot_number": "13", "price": 489000 }
  ],
  "booked_plot_numbers": ["12", "13"],
  "created_by_user_id": null,
  "checked_by_user_id": null,
  "checked_at": null,
  "dispatch_date": null,
  "dispatched_at": null,
  "invoice_number": null,
  "invoiced_at": null,
  "paid_at": null,
  "canceled_at": null,
  "created_at": "2026-11-12T09:30:00.000Z",
  "updated_at": "2026-11-12T09:30:00.000Z"
}
```

### List call-offs

`GET /property/call-offs` · Scope: `property:read`

Highest number (most recent) first.

| Query parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `status` | string | No | One status. |
| `statuses` | string | No | Several statuses, comma-separated, such as `invoiced,paid`. |
| `development_id` | string | No | |
| `dispatch_from` | string | No | `YYYY-MM-DD`. Only call-offs with a `dispatch_date` on or after this day. |
| `dispatch_to` | string | No | `YYYY-MM-DD`. Only call-offs with a `dispatch_date` on or before this day. |
| `limit` | integer | No | 1 to 100. Default 20. |
| `cursor` | string | No | `meta.cursor` from the previous page. |

```bash
curl "https://crm.empiraa.app/api/v1/property/call-offs?statuses=dispatched,invoiced" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
const url = new URL("https://crm.empiraa.app/api/v1/property/call-offs");
url.searchParams.set("statuses", "dispatched,invoiced");
const res = await fetch(url, { headers: { Authorization: "Bearer YOUR_API_KEY" } });
```

**Response `200`:** an array of [call-off objects](#the-call-off-object) in `data`; `meta` has `cursor` and `hasMore`.

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | A status isn't one of the values above, or a date isn't a real `YYYY-MM-DD` day. |

### Book a call-off

`POST /property/call-offs` · Scope: `property:write`

Send **either** a development call-off (`development_id` and `plots`) **or** a bulk call-off (`account_id`, `description` and `value`), not both. Fields not listed here are refused with `400`, so a mistyped field isn't silently ignored.

#### Development call-off

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `development_id` | string | Yes | A development in this workspace. |
| `plots` | string | Yes | Plot numbers as text, with ranges: `"10-25"` or `"10-25, 30"`. |
| `stage` | string | No | `reservation` (default) or `completion`. |
| `create_missing_plots` | boolean | No | Create plots in the list that the development doesn't have yet. Default `false`. |
| `requested_by_contact_id` | string or null | No | |
| `requested_at` | string | No | ISO 8601 date or date and time, such as `2026-11-12` or `2026-11-12T09:30:00Z`. A time with no offset is UTC. Defaults to now. |
| `required_by` | string | No | `YYYY-MM-DD`. |
| `client_order_number` | string or null | No | Up to 100 characters. Defaults to the development's `order_number`. |
| `notes` | string or null | No | Up to 2,000 characters. |

#### Bulk call-off

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `account_id` | string | Yes | The client's Account. |
| `description` | string | Yes | What is being called off, up to 500 characters. |
| `value` | number | Yes | Such as `1250.50`. |
| `requested_by_contact_id` | string or null | No | |
| `requested_at` | string | No | As above. |
| `required_by` | string | No | `YYYY-MM-DD`. |
| `client_order_number` | string or null | No | Up to 100 characters. |
| `notes` | string or null | No | Up to 2,000 characters. |

Dates must be real calendar days between the years 1900 and 2100; `2026-02-30` is refused rather than moved to March.

```bash
curl -X POST https://crm.empiraa.app/api/v1/property/call-offs \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "development_id": "1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37",
    "plots": "12-13",
    "stage": "reservation",
    "requested_by_contact_id": "a1d4e7b2-5c3f-4e8a-b9d6-0f2c7e1a3b54",
    "required_by": "2026-12-01"
  }'
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/call-offs", {
  method: "POST",
  headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    development_id: "1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37",
    plots: "12-13",
    stage: "reservation",
    requested_by_contact_id: "a1d4e7b2-5c3f-4e8a-b9d6-0f2c7e1a3b54",
    required_by: "2026-12-01",
  }),
});
```

**Response `201`:** the new [call-off object](#the-call-off-object) in `data`, with `status` `booked`.

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | A required field is missing; an unknown field was sent; development and bulk fields were mixed; a date or text field isn't valid; or the booking can't be made (for example a plot is already on a live call-off for that stage, or doesn't exist and `create_missing_plots` is `false`). `title` explains, using your workspace's name for a plot. |
| 404 | `NOT_FOUND` | The development or Account isn't in this workspace. |
| 409 | `CONFLICT` | The call-off changed while it was being booked. Retry. |

**Webhook events:** none.

### Get a call-off

`GET /property/call-offs/{id}` · Scope: `property:read`

```bash
curl https://crm.empiraa.app/api/v1/property/call-offs/3a5c7e9b-1d2f-4a4c-8e6b-5d7f9b1d3f84 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```js
await fetch("https://crm.empiraa.app/api/v1/property/call-offs/3a5c7e9b-1d2f-4a4c-8e6b-5d7f9b1d3f84", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
```

**Response `200`:** the [call-off object](#the-call-off-object) in `data`. **`404 NOT_FOUND`** if it isn't in this workspace.

### Record an invoice or payment

`PATCH /property/call-offs/{id}` · Scope: `property:write`

Records a call-off's invoice, its payment, or both, so your accounting tool (or Zapier, Make or n8n) can keep Signal up to date. This is the only change the API makes to a call-off.

| Field | Type | Notes |
| --- | --- | --- |
| `invoice_number` | string | Records the invoice. Up to 100 characters; can't be blank. A call-off has one invoice number. |
| `invoiced_at` | string | When it was invoiced. ISO 8601 date or date and time. Needs `invoice_number`. Defaults to now. |
| `paid_at` | string | Records the payment, at this time. ISO 8601 date or date and time. |
| `paid` | boolean | `true` records the payment now. Only `true` is accepted: a payment can't be taken back. |

Send `invoice_number` to record an invoice, `paid_at` or `paid: true` to record a payment, or both. When you send both, the invoice is recorded first. A payment can only be recorded once the call-off is invoiced. Any other field is refused with `400`.

```bash
curl -X PATCH https://crm.empiraa.app/api/v1/property/call-offs/3a5c7e9b-1d2f-4a4c-8e6b-5d7f9b1d3f84 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"invoice_number": "INV-20931", "invoiced_at": "2026-11-12"}'
```

```js
// Later, when the invoice is paid:
await fetch("https://crm.empiraa.app/api/v1/property/call-offs/3a5c7e9b-1d2f-4a4c-8e6b-5d7f9b1d3f84", {
  method: "PATCH",
  headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ paid_at: "2026-12-02T15:00:00Z" }),
});
```

**Response `200`:** the updated [call-off object](#the-call-off-object) in `data`.

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | Nothing to record was sent; an unknown field was sent; `invoice_number` is blank or too long; `invoiced_at` was sent without `invoice_number`; `paid` isn't `true`; or a date isn't valid. |
| 404 | `NOT_FOUND` | The call-off isn't in this workspace. |
| 409 | `CONFLICT` | The call-off's state doesn't allow it (for example it already has a different invoice number, or isn't invoiced yet when recording a payment), or it changed during the request. `title` explains. |

**Webhook events:** none.
