Property
Manage developments, plots, reservations and call-offs in Signal's optional Property module.
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: 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. |
{
"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. |
curl "https://crm.empiraa.app/api/v1/property/developments?status=active" \
-H "Authorization: Bearer YOUR_API_KEY"
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 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. |
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"}'
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 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.
Get a development#
GET /property/developments/{id} · Scope: property:read
curl https://crm.empiraa.app/api/v1/property/developments/1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37 \
-H "Authorization: Bearer YOUR_API_KEY"
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 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.
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"}'
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 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.
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.
curl -X DELETE https://crm.empiraa.app/api/v1/property/developments/1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37 \
-H "Authorization: Bearer YOUR_API_KEY"
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. |
{
"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. |
curl "https://crm.empiraa.app/api/v1/property/plots?development_id=1f3b5d7e-9a2c-4e4b-8d6f-0b2d4f6a8c37&status=available" \
-H "Authorization: Bearer YOUR_API_KEY"
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 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. |
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}'
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 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.
Get a plot#
GET /property/plots/{id} · Scope: property:read
curl https://crm.empiraa.app/api/v1/property/plots/5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51 \
-H "Authorization: Bearer YOUR_API_KEY"
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 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, except development_id, which can't be changed.
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"}'
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 in data. 404 NOT_FOUND if it isn't in this workspace.
Webhook events: plot.updated, plus plot.status_changed 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).
curl -X DELETE https://crm.empiraa.app/api/v1/property/plots/5d7f9b1c-3e4a-4c6d-9f8b-2a4c6e8a0c51 \
-H "Authorization: Bearer YOUR_API_KEY"
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. |
{
"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. |
curl "https://crm.empiraa.app/api/v1/property/reservations?status=active" \
-H "Authorization: Bearer YOUR_API_KEY"
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 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 |
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}'
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 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
curl https://crm.empiraa.app/api/v1/property/reservations/7f9b1d3e-5a6c-4e8f-8b0d-4c6e8a0c2e73 \
-H "Authorization: Bearer YOUR_API_KEY"
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 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, which also frees the plot.
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}'
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 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.
curl -X DELETE https://crm.empiraa.app/api/v1/property/reservations/7f9b1d3e-5a6c-4e8f-8b0d-4c6e8a0c2e73 \
-H "Authorization: Bearer YOUR_API_KEY"
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. |
{
"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. |
curl "https://crm.empiraa.app/api/v1/property/call-offs?statuses=dispatched,invoiced" \
-H "Authorization: Bearer YOUR_API_KEY"
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 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.
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"
}'
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 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
curl https://crm.empiraa.app/api/v1/property/call-offs/3a5c7e9b-1d2f-4a4c-8e6b-5d7f9b1d3f84 \
-H "Authorization: Bearer YOUR_API_KEY"
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 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.
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"}'
// 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 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.

