API v1: Properties & units
List, create, update (single and bulk, incl. custom fields) and delete properties and units, upload photos, and check for duplicate imports.
Requires the properties:read / properties:write scopes (units:read / units:write for the unit endpoints).
List properties
GET /api/v1/properties
| Parameter | Notes |
|---|---|
limit, cursor, sort, fields | Standard pagination |
property_type | apartment, house, building, commercial, office, land, warehouse, garage |
transaction_type | buy, rent, invest, sell, lease_out, compensation_exchange |
status | available, reserved, sold, rented, inactive |
min_price, max_price | number |
city_id, region_id, neighborhood_id | uuid (location hierarchy) |
bedrooms | integer |
min_area, max_area | number |
The response returns the fields in PROPERTY_ALLOWED_FIELDS (60+ columns). Use ?fields= to request a specific subset — the table below is the commonly used subset, not the full list. Internal-only fields (internal_notes, draft_units, owner_id, created_by, updated_by, deleted_at, marketing_manager_id) are stripped from the default response and only returned when explicitly requested via ?fields=.
Property fields (commonly used)
| Field | Type | Notes |
|---|---|---|
title | string | required on create |
description_bg, description_en | string, nullable | |
property_type | enum | required on create |
transaction_type | enum | required on create |
status | enum | available, reserved, sold, rented, inactive |
street_address, city, state_region, neighborhood, postal_code, country | string, nullable | city/neighborhood are text; filter list results by the separate city_id/neighborhood_id UUID params |
latitude, longitude | number, nullable | |
price, price_per_sqm, currency, price_negotiable | number/string/boolean, nullable | |
area_net, area_total, bedrooms | number/integer, nullable |
Get / update / delete a property
GET /api/v1/properties/{id}
PATCH /api/v1/properties/{id}
PUT /api/v1/properties/{id}
DELETE /api/v1/properties/{id}
PATCH is the recommended way to update: it is sparse — only the keys in the body are written, anything you omit is left untouched. It accepts every field the property detail page can edit (the same allowlist as the update_property MCP tool), including the building-unit fields unit_number, floor_number, entrance, area_total (РЗП), area_net (ЗП), property_subtype, act_status (ACT14/ACT15/ACT16/NA), workflow_status and include_in_feed, plus custom_fields. Unknown keys are rejected with 400 instead of being silently dropped. Changing owner_id / marketing_manager_id also requires the properties.reassign_owner permission (403 otherwise). Send Idempotency-Key to make retries safe.
PATCH /api/v1/properties/3f1c2b9e-6a1d-4c55-9b0e-1f2a3b4c5d6e
{
"unit_number": "A12",
"floor_number": 3,
"area_total": 78.4,
"area_net": 64.5,
"act_status": "ACT16",
"version": 7,
"custom_fields": { "orientation": "south", "terrace_area": 12.5 }
}
PUT accepts the create fields plus unit_number, entrance, area_net, property_subtype, act_status, workflow_status, include_in_feed, neighborhood_id, title_bg, title_en, internal_notes and custom_fields.
Both return the updated property and 409 Conflict when the optional version no longer matches the stored row. DELETE moves the property to trash (204, no content), it does not hard-delete. Properties in trash return 404 on every write.
Custom fields
custom_fields is an object keyed by the custom field's field_name (or its definition id) — list them with GET /api/v1/custom-fields?entity_type=property. Each value is validated against its definition (type, select options, length); null clears the value. If any custom field is unknown or invalid, the whole request is rejected with 400 (details.errors[].path = custom_fields.<name>) before anything is written. The stored values are returned in the property's custom_fields object.
Bulk update properties
PATCH /api/v1/properties/bulk
Updates up to 100 properties per request, each with its own values — e.g. unit numbers, floors, areas and Акт status for every unit of a building. Each row is the same body as PATCH /properties/{id} plus the property id:
{
"updates": [
{ "id": "3f1c2b9e-6a1d-4c55-9b0e-1f2a3b4c5d6e", "unit_number": "A12", "area_net": 64.5, "act_status": "ACT16" },
{ "id": "7d2e4f60-1b3c-4a5d-8e9f-0a1b2c3d4e5f", "price": 145000, "custom_fields": { "orientation": "south" } }
]
}
Rows are applied independently. The response is always 200 with a per-row status (a 207-style body), so one bad row never blocks the rest:
{
"data": {
"updated": 1,
"failed": [{ "id": "7d2e4f60-…", "status": 400, "error": "custom_fields.orientation: Value must be one of: south, north" }],
"results": [
{ "id": "3f1c2b9e-…", "status": 200, "data": { "id": "3f1c2b9e-…", "unit_number": "A12", "...": "..." } },
{ "id": "7d2e4f60-…", "status": 400, "error": "…", "details": { "errors": [{ "path": "custom_fields.orientation", "message": "…" }] } }
]
}
}
Per-row status is 200, 400 (validation / custom field), 403 (permission), 404 (not found or in trash), 409 (version conflict) or 500. The whole request is rejected with 400 when it has more than 100 rows, a duplicate id or an unknown key. For larger migrations, send batches of 100. Individual PATCH /properties/{id} calls can also be combined through POST /api/v1/batch (max 20 operations).
Create a property
POST /api/v1/properties
Requires title, property_type, transaction_type. Returns 402 Payment Required if the account's plan limit for properties is exceeded.
{
"data": { "id": "...", "title": "2-bed apartment, Lozenets", "...": "..." }
}
Upload property photos
POST /api/v1/properties/{id}/images
Content-Type: multipart/form-data
Repeatable file field — JPEG, PNG, WebP or GIF, up to 10MB each, 20 files per request. Each file is verified by magic bytes, not just its declared MIME type. Add watermark=true to queue the account watermark; watermarking runs on a background queue, so the response reports how many images were queued, not how many are finished — poll the property's images to observe completion.
{
"data": [ { "id": "...", "url": "https://...", "...": "..." } ],
"meta": { "count": 3 },
"validation_errors": [],
"watermark": { "queued": 3 }
}
413 if too many files or a file exceeds the size limit; 502 if no file could be stored.
Floor plans (PDF/JPEG/PNG/WebP, per apartment, floor or building — including bulk upload from URLs) have their own endpoints: see Floor plans.
Poll for changes
GET /api/v1/properties/changes?since=2026-09-01T00:00:00Z&property_id={uuid}
Returns { property_id, change_type: "created" | "updated" | "deleted", changed_at, data } entries.
Check for a duplicate import
GET /api/v1/properties/duplicate-check?url={listing-url}
Checks the import-history table (web_property_imports) for an existing import of the given URL for this account — properties do not store their own source URL. Used by the browser extension to warn before re-importing the same listing.
{
"data": {
"is_duplicate": true,
"matches": [
{
"import_id": "...",
"property_id": "...",
"platform": "imot.bg",
"status": "completed",
"source_url": "https://...",
"title": "2-bed apartment, Lozenets",
"imported_at": "2026-08-20T10:00:00.000Z"
}
]
}
}
Units
Units are child properties (e.g. apartments inside a building listing).
GET /api/v1/units?property_id={uuid}&status=available
GET /api/v1/units/{id}
Dictionaries
Dropdown/filter values used by the property fields above:
GET /api/v1/dictionaries/{category}
category is one of property_types, transaction_types, features, heating_types, building_types. Returns { data: [{ value, label }] }.