Unit Management
Manage property units in the Sakneen system using the Units API.
Overview
The Units API allows you to create, update, and retrieve property units in the Sakneen system. This is essential for property developers, real estate companies, and property management systems that need to maintain up-to-date unit inventory and display unit data on external platforms.
List Units
Retrieve a paginated list of units belonging to your organization.
Endpoint
GET /external/apis/v1.0/units
Authentication
All requests require a valid API key in the header:
api-key: YOUR_API_KEY
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number (min: 1) |
limit | number | 10 | Number of results per page (min: 1, max: 100) |
sortBy | string | defaultSort | Field to sort by (defaults to createdAt) |
order | string | desc | Sort order: asc, desc, ascending, or descending |
statuses | string[] | — | Filter by unit statuses. Values are case-sensitive (e.g., Available, Sold). Repeat the parameter for multiple values (statuses=Available&statuses=Reserved); a comma-separated value is treated as one status |
brokerId | string | — | Broker organization ID. Returns only units that are visible to brokers and assigned to this broker. An invalid ID returns 400 |
The maximum number of records per page is 100. Even if a higher limit value is provided, the API will cap it at 100.
Response Fields
Each unit object in the response includes:
| Field | Type | Description |
|---|---|---|
_id | string | MongoDB document ID |
recordSystemId | string | Unique system identifier for the record |
unitId | string | Unique identifier for the unit |
title | string | Unit title |
translations | object | Localized data including name and slug per language (en-us, ar-eg) |
status | string | Current status of the unit |
saleType | string | Type of sale (primary or resale) |
propertyType | object | Populated property type details |
price | number | Base price of the unit |
priceCurrency | string | Currency code for the price |
bua | number | Built-up area in square meters |
totalBua | number | Total built-up area in square meters |
landArea | number | Land area in square meters |
gardenArea | number | Garden area in square meters |
beds | number | Number of bedrooms |
bathrooms | number | Number of bathrooms |
floor | string | Floor level |
finishingType | object | Populated finishing type details |
amenities | array | Populated list of amenities |
features | array | Populated list of features |
mainImageUrl | string | URL of the unit's main image |
media | array | Array of media objects (images, videos, etc.) |
deliveryDetails | object | Delivery date and timeline information |
createdAt | string | ISO 8601 timestamp of creation |
updatedAt | string | ISO 8601 timestamp of last update |
Success Response (200 OK)
{
"data": [
{
"_id": "64f1234567890abcdef123456",
"recordSystemId": "00163EAB688B1EECA9C3E1F963E029CC",
"unitId": "VH-12",
"title": "Luxury Apartment - VH-12",
"translations": {
"en-us": {
"name": "Luxury Apartment VH-12",
"slug": "luxury-apartment-vh-12"
},
"ar-eg": {
"name": "شقة فاخرة VH-12",
"slug": "شقة-فاخرة-vh-12"
}
},
"status": "Available",
"saleType": "primary",
"propertyType": {
"_id": "64a1b2c3d4e5f6789012345",
"name": "Apartment"
},
"price": 1200000,
"priceCurrency": "EGP",
"bua": 120,
"totalBua": 150,
"landArea": 150,
"gardenArea": 30,
"beds": 2,
"bathrooms": 2,
"floor": "2nd",
"finishingType": {
"_id": "64a1b2c3d4e5f6789012346",
"name": "Fully Finished"
},
"amenities": [
{ "_id": "64a1b2c3d4e5f6789012347", "name": "Swimming Pool" },
{ "_id": "64a1b2c3d4e5f6789012348", "name": "Gym" }
],
"features": [
{ "_id": "64a1b2c3d4e5f6789012349", "name": "Balcony" },
{ "_id": "64a1b2c3d4e5f6789012350", "name": "City View" }
],
"mainImageUrl": "https://example.com/images/vh-12-main.jpg",
"media": [
{ "url": "https://example.com/images/vh-12-1.jpg", "type": "image" },
{ "url": "https://example.com/images/vh-12-2.jpg", "type": "image" }
],
"deliveryDetails": {
"type": "months",
"date": "2026-12-01",
"months": 18
},
"createdAt": "2024-08-07T10:30:00.000Z",
"updatedAt": "2024-08-10T14:15:00.000Z"
}
],
"pagination": {
"count": 1,
"limit": 10,
"pages": 1,
"total": 1
}
}
Error Responses
401 Unauthorized - Missing or Invalid API Key
{
"statusCode": 401,
"message": "Access Denied, API key not provided",
"error": "Unauthorized"
}
Code Examples
cURL
# Basic request
curl -X GET "https://your-domain/external/apis/v1.0/units?page=1&limit=20" \
-H "api-key: YOUR_API_KEY"
# With sorting and status filter
curl -X GET "https://your-domain/external/apis/v1.0/units?page=1&limit=10&sortBy=price&order=asc&statuses=Available&statuses=Reserved" \
-H "api-key: YOUR_API_KEY"
JavaScript/Node.js
const apiKey = process.env.SAKNEEN_API_KEY;
const baseDomain = process.env.SAKNEEN_DOMAIN;
async function getUnits({ page = 1, limit = 10, sortBy, order, statuses } = {}) {
const params = new URLSearchParams({ page, limit });
if (sortBy) params.append('sortBy', sortBy);
if (order) params.append('order', order);
if (statuses) {
for (const status of statuses) {
params.append('statuses', status);
}
}
const response = await fetch(
`https://${baseDomain}/external/apis/v1.0/units?${params}`,
{
method: 'GET',
headers: { 'api-key': apiKey },
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP ${response.status}: ${errorData.message || response.statusText}`);
}
return await response.json();
}
// Usage
getUnits({ page: 1, limit: 20, statuses: ['Available'] })
.then(result => {
console.log(`Found ${result.pagination.total} units`);
console.log('Units:', result.data);
})
.catch(error => console.error('Error:', error));
Python
import os
import requests
api_key = os.getenv('SAKNEEN_API_KEY')
base_domain = os.getenv('SAKNEEN_DOMAIN')
def get_units(page=1, limit=10, sort_by=None, order=None, statuses=None):
headers = { 'api-key': api_key }
params = { 'page': page, 'limit': limit }
if sort_by:
params['sortBy'] = sort_by
if order:
params['order'] = order
if statuses:
params['statuses'] = statuses # requests handles list params
response = requests.get(
f'https://{base_domain}/external/apis/v1.0/units',
headers=headers,
params=params
)
response.raise_for_status()
return response.json()
# Usage
try:
result = get_units(page=1, limit=20, statuses=['Available', 'Reserved'])
print(f"Found {result['pagination']['total']} units")
for unit in result['data']:
print(f" - {unit['unitId']}: {unit['title']} ({unit['status']})")
except requests.exceptions.RequestException as e:
print(f'Error fetching units: {e}')
PHP
<?php
function getUnits($page = 1, $limit = 10, $sortBy = null, $order = null, $statuses = []) {
$domain = $_ENV['SAKNEEN_DOMAIN'];
$apiKey = $_ENV['SAKNEEN_API_KEY'];
$params = ['page' => $page, 'limit' => $limit];
if ($sortBy) $params['sortBy'] = $sortBy;
if ($order) $params['order'] = $order;
$query = http_build_query($params);
foreach ($statuses as $status) {
$query .= '&' . http_build_query(['statuses' => $status]);
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{$domain}/external/apis/v1.0/units?{$query}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['api-key: ' . $apiKey],
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($httpCode !== 200) {
throw new Exception("HTTP Error: $httpCode");
}
return json_decode($response, true);
}
// Usage
try {
$result = getUnits(1, 20, null, null, ['Available']);
echo "Found {$result['pagination']['total']} units\n";
foreach ($result['data'] as $unit) {
echo " - {$unit['unitId']}: {$unit['title']} ({$unit['status']})\n";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
Export Units
Retrieve all matching units in one response, without pagination. Use it to sync your full inventory, or to load the units assigned to one broker.
Endpoint
GET /external/apis/v1.0/units/export/all
Authentication
All requests require a valid API key in the header:
api-key: YOUR_API_KEY
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
statuses | string[] | — | Filter by unit statuses. Values are case-sensitive (e.g., Available). Repeat the parameter for multiple values (statuses=Available&statuses=Reserved) |
brokerId | string | — | Broker organization ID. Returns only units that are visible to brokers and assigned to this broker |
dateFilterBy | string | — | Date field to filter on: createdAt or updatedAt |
startDate | string | — | YYYY-MM-DD. Returns units from the start of this day |
endDate | string | — | YYYY-MM-DD. Returns units up to the end of this day |
sortBy | string | defaultSort | Field to sort by (defaults to createdAt) |
order | string | desc | Sort order: asc, desc, ascending, or descending |
- Without
statuses, the export returns the same unit statuses as List Units. - The date filter applies only when
dateFilterByis sent withstartDate,endDate, or both. Day boundaries use Egypt time (Africa/Cairo), and both days are included.
An export can return up to 10,000 units. If more units match, the API returns 400 and no units. Narrow the export with statuses, brokerId, or a date range.
Language
Translated fields such as name, slug, and compoundName are returned in English (en-us) by default. Send the Accept-Language: ar-eg header to get them in Arabic. A translated field is omitted when the record has no translation in the requested language.
This endpoint does not use the language header.
Response Fields
The response is an object with a data array. There is no pagination object. Stored unit fields that have no value are omitted.
| Field | Type | Description |
|---|---|---|
_id | string | Unit ID |
unitId | string | Unit code |
recordSystemId | string | Unit ID in your system |
name | string | Unit name in the response language |
slug | string | Unit slug in the response language |
title | string | Unit title |
status | string | Unit status (e.g., Available) |
saleType | string | primary or resale |
price | number | Unit price |
priceCurrency | string | Currency code for the price |
priceByYear | object | Prices by payment plan length. Keys are numbers of years |
priceOne | number | See Payment Plan Prices |
priceTwo | number | null | See Payment Plan Prices |
buaPricePerSquareMeter | number | Price per square meter of built-up area |
lanAreaPricePerSquareMeter | number | Price per square meter of land area |
bua | number | Built-up area in square meters. Uses the unit design's bua when the unit has none |
coveredTerrace | number | Covered terrace area in square meters |
totalBua | number | bua + coveredTerrace when coveredTerrace is set (including 0). Otherwise the unit's stored totalBua, then bua, then 0. Not rounded |
landArea | number | Land area in square meters |
gardenArea | number | Garden area in square meters |
beds | number | Number of bedrooms. Uses the unit design's bedrooms when the unit has none |
bathrooms | number | Number of bathrooms |
floor | object | Floor, e.g. { "name": "Third" } |
orientation | string | Unit orientation |
deliveryDetails | object | Delivery details: type (date or months) with date or numberOfMonths |
mainImageUrl | string | Main image URL. Uses the unit design's image when the unit has none |
media | array | The unit's media combined with its unit design's media. Items include image, imageType, and type |
floorPlans | string[] | Image URLs of the media items whose imageType is floorplan |
unitLocationImage | string | Unit location image URL |
buildingLocationImageUrl | string | Building location image URL |
locationId | string | ID of the compound or phase the unit belongs to |
compound | object | null | _id, type (Compound), and translated fields such as name, slug, and description. null when the unit belongs to a phase |
phase | object | null | _id, parentId (ID of the phase's compound), type (Phase), compound (the phase's compound, with the same fields as compound), and translated fields such as name, slug, and description. null when the unit belongs directly to a compound |
compoundName | string | Compound name in the response language. For a unit in a phase, this is the name of the phase's compound |
phaseName | string | Phase name in the response language. Returned only for units that belong to a phase |
propertyTypeId | string | Property type ID |
type | object | Property type: _id and translated fields such as name and slug. {} when the unit has no property type |
unitDesignId | string | Unit design ID |
unitDesign | object | null | Unit design: _id, bua, bedrooms, mainImageUrl, media, and translated fields such as name and slug |
amenitiesIds | string[] | Amenity IDs |
amenities | array | Amenities: _id and translated fields such as name and slug |
featuresIds | string[] | Feature IDs |
finishingTypeId | string | Finishing type ID |
paymentPlansIds | string[] | Payment plan IDs |
paymentPlans | array | Payment plans: _id, numberOfYears, and translated name |
brokersIds | string[] | IDs of the broker organizations the unit is assigned to |
brokers | array | Broker organizations: _id, name, and slug |
createdAt | string | ISO 8601 timestamp of creation |
updatedAt | string | ISO 8601 timestamp of last update |
Payment Plan Prices
priceOne and priceTwo are taken from priceByYear. Only prices above 0 are used, and only for years that match the numberOfYears of the unit's payment plans.
- Two or more prices:
priceTwois the highest price andpriceOneis the second highest. - One price:
priceOneis that price andpriceTwoisnull. - No prices:
priceOneis omitted andpriceTwoisnull.
Success Response (200 OK)
{
"data": [
{
"_id": "6aaa653020fd7406492f056f",
"amenitiesIds": ["6aaa653020fd7406492f055f"],
"bathrooms": 3,
"beds": 3,
"brokersIds": ["6aaa653020fd7406492f0554"],
"bua": 185.5,
"buaPricePerSquareMeter": 38544,
"buildingLocationImageUrl": "https://img.example.com/units/b2-305-building.jpg",
"coveredTerrace": 14.5,
"createdAt": "2026-05-10T08:30:00.000Z",
"deliveryDetails": {
"date": "2028-06-30T00:00:00.000Z",
"type": "date"
},
"featuresIds": [],
"finishingTypeId": "6899bff45e04ee7b2f7364b6",
"floor": {
"name": "Third"
},
"gardenArea": 0,
"landArea": 0,
"lanAreaPricePerSquareMeter": 0,
"locationId": "6aaa653020fd7406492f0561",
"mainImageUrl": "https://img.example.com/units/b2-305-main.jpg",
"media": [
{
"image": "https://img.example.com/units/b2-305-gallery-1.jpg",
"imageType": "gallery",
"type": "image"
},
{
"image": "https://img.example.com/designs/type-b-floorplan.jpg",
"imageType": "floorplan",
"type": "image"
},
{
"image": "https://img.example.com/units/b2-305-floorplan.jpg",
"imageType": "floorplan",
"type": "image"
}
],
"orientation": "North East",
"paymentPlansIds": ["6aaa653020fd7406492f0569", "6aaa653020fd7406492f056b"],
"price": 7150000,
"priceByYear": {
"5": 7150000,
"8": 8230000
},
"priceCurrency": "EGP",
"propertyTypeId": "6aaa653020fd7406492f055d",
"recordSystemId": "CRM-B2-305",
"saleType": "primary",
"status": "Available",
"title": "B2-305",
"totalBua": 200,
"unitDesignId": "6aaa653020fd7406492f0567",
"unitId": "B2-305",
"unitLocationImage": "https://img.example.com/units/b2-305-location.jpg",
"updatedAt": "2026-09-14T12:00:00.000Z",
"paymentPlans": [
{ "_id": "6aaa653020fd7406492f0569", "numberOfYears": 5, "name": "5 Years" },
{ "_id": "6aaa653020fd7406492f056b", "numberOfYears": 8, "name": "8 Years" }
],
"amenities": [
{ "_id": "6aaa653020fd7406492f055f", "name": "Swimming Pool", "slug": "swimming-pool" }
],
"brokers": [
{ "_id": "6aaa653020fd7406492f0554", "name": "example brokerage", "slug": "example-brokerage" }
],
"phase": null,
"type": {
"_id": "6aaa653020fd7406492f055d",
"name": "Apartment",
"slug": "apartment"
},
"compound": {
"_id": "6aaa653020fd7406492f0561",
"type": "Compound",
"description": "Compound description",
"name": "Sample Compound",
"slug": "sample-compound"
},
"unitDesign": {
"_id": "6aaa653020fd7406492f0567",
"mainImageUrl": "https://img.example.com/designs/type-b-main.jpg",
"bua": 200,
"media": [
{
"image": "https://img.example.com/designs/type-b-floorplan.jpg",
"imageType": "floorplan",
"type": "image"
}
],
"bedrooms": 3,
"name": "Type B",
"slug": "type-b"
},
"compoundName": "Sample Compound",
"floorPlans": [
"https://img.example.com/designs/type-b-floorplan.jpg",
"https://img.example.com/units/b2-305-floorplan.jpg"
],
"priceOne": 7150000,
"priceTwo": 8230000,
"name": "Apartment B2-305",
"slug": "apartment-b2-305"
}
]
}
Error Responses
400 Bad Request - Too Many Units
{
"message": "Export matched 12450 units, which exceeds the maximum of 10000. Narrow the results using status, broker, or date filters.",
"error": "Bad Request",
"statusCode": 400
}
400 Bad Request - Invalid Query Parameter
{
"message": ["brokerId must be a mongodb id"],
"error": "Bad Request",
"statusCode": 400
}
401 Unauthorized - Missing or Invalid API Key
{
"message": "Access Denied, API key not provided",
"error": "Unauthorized",
"statusCode": 401
}
An invalid key returns the same response with the message Access Denied, API key not valid.
Broker-Scoped 3D Views
A compound's 3D view link in Sakneen can include these placeholders:
| Placeholder | Replaced with |
|---|---|
{brokerId} | The broker organization ID of the user viewing the compound |
{compoundId} | The compound ID |
Example link: https://3d.example.com/view?brokerId={brokerId}&compoundId={compoundId}
When a user from a broker organization opens the compound's 3D view, Sakneen replaces the first {brokerId} and the first {compoundId} in the link, then loads it. Pass the received brokerId to this endpoint to get only that broker's units.
When a user from the developer's own organization opens the 3D view, the link loads unchanged and the placeholders stay in the URL as text. Only send brokerId when it is a valid ID; otherwise the API returns 400.
Call this endpoint from your server. Your API key must never be exposed in a browser or in the 3D view URL.
Code Examples
cURL
# Default statuses, newest first
curl -X GET "https://your-domain/external/apis/v1.0/units/export/all" \
-H "api-key: YOUR_API_KEY"
# Available units assigned to one broker, in Arabic
curl -X GET "https://your-domain/external/apis/v1.0/units/export/all?statuses=Available&brokerId=6aaa653020fd7406492f0554" \
-H "api-key: YOUR_API_KEY" \
-H "Accept-Language: ar-eg"
# Units updated during September 2026
curl -X GET "https://your-domain/external/apis/v1.0/units/export/all?dateFilterBy=updatedAt&startDate=2026-09-01&endDate=2026-09-30" \
-H "api-key: YOUR_API_KEY"
JavaScript/Node.js
const apiKey = process.env.SAKNEEN_API_KEY;
const baseDomain = process.env.SAKNEEN_DOMAIN;
async function exportUnits({ statuses = [], brokerId, dateFilterBy, startDate, endDate } = {}) {
const params = new URLSearchParams();
for (const status of statuses) {
params.append('statuses', status);
}
if (brokerId) params.append('brokerId', brokerId);
if (dateFilterBy) params.append('dateFilterBy', dateFilterBy);
if (startDate) params.append('startDate', startDate);
if (endDate) params.append('endDate', endDate);
const response = await fetch(
`https://${baseDomain}/external/apis/v1.0/units/export/all?${params}`,
{
method: 'GET',
headers: { 'api-key': apiKey },
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP ${response.status}: ${errorData.message || response.statusText}`);
}
return await response.json();
}
// Usage
exportUnits({ statuses: ['Available'], brokerId: '6aaa653020fd7406492f0554' })
.then(result => console.log(`Exported ${result.data.length} units`))
.catch(error => console.error('Error:', error));
Python
import os
import requests
api_key = os.getenv('SAKNEEN_API_KEY')
base_domain = os.getenv('SAKNEEN_DOMAIN')
def export_units(statuses=None, broker_id=None, date_filter_by=None, start_date=None, end_date=None):
params = {}
if statuses:
params['statuses'] = statuses # sent as repeated statuses params
if broker_id:
params['brokerId'] = broker_id
if date_filter_by:
params['dateFilterBy'] = date_filter_by
if start_date:
params['startDate'] = start_date
if end_date:
params['endDate'] = end_date
response = requests.get(
f'https://{base_domain}/external/apis/v1.0/units/export/all',
headers={'api-key': api_key},
params=params
)
response.raise_for_status()
return response.json()
# Usage
try:
result = export_units(statuses=['Available'], date_filter_by='updatedAt', start_date='2026-09-01')
print(f"Exported {len(result['data'])} units")
except requests.exceptions.RequestException as e:
print(f'Error exporting units: {e}')
PHP
<?php
function exportUnits($statuses = [], $brokerId = null) {
$domain = $_ENV['SAKNEEN_DOMAIN'];
$apiKey = $_ENV['SAKNEEN_API_KEY'];
$queryParts = [];
foreach ($statuses as $status) {
$queryParts[] = http_build_query(['statuses' => $status]);
}
if ($brokerId) {
$queryParts[] = http_build_query(['brokerId' => $brokerId]);
}
$query = implode('&', $queryParts);
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{$domain}/external/apis/v1.0/units/export/all?{$query}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['api-key: ' . $apiKey],
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($httpCode !== 200) {
throw new Exception("HTTP Error: $httpCode");
}
return json_decode($response, true);
}
// Usage
try {
$result = exportUnits(['Available'], '6aaa653020fd7406492f0554');
echo "Exported " . count($result['data']) . " units\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
Get Unit by Slug
Retrieve a single unit by its slug. The slug is matched against all available languages (en-us and ar-eg).
Endpoint
GET /external/apis/v1.0/units/:slug
Authentication
All requests require a valid API key in the header:
api-key: YOUR_API_KEY
Path Parameters
| Parameter | Type | Description |
|---|---|---|
slug | string | The unit slug (from any language in translations) |
Response
The response contains a single unit object with the exact same fields as each item in the List Units response.
Success Response (200 OK)
{
"data": {
"_id": "64f1234567890abcdef123456",
"recordSystemId": "00163EAB688B1EECA9C3E1F963E029CC",
"unitId": "VH-12",
"title": "Luxury Apartment - VH-12",
"translations": {
"en-us": {
"name": "Luxury Apartment VH-12",
"slug": "luxury-apartment-vh-12"
},
"ar-eg": {
"name": "شقة فاخرة VH-12",
"slug": "شقة-فاخرة-vh-12"
}
},
"status": "available",
"saleType": "primary",
"propertyType": {
"_id": "64a1b2c3d4e5f6789012345",
"name": "Apartment"
},
"price": 1200000,
"priceCurrency": "EGP",
"bua": 120,
"totalBua": 150,
"landArea": 150,
"gardenArea": 30,
"beds": 2,
"bathrooms": 2,
"floor": "2nd",
"finishingType": {
"_id": "64a1b2c3d4e5f6789012346",
"name": "Fully Finished"
},
"amenities": [
{ "_id": "64a1b2c3d4e5f6789012347", "name": "Swimming Pool" },
{ "_id": "64a1b2c3d4e5f6789012348", "name": "Gym" }
],
"features": [
{ "_id": "64a1b2c3d4e5f6789012349", "name": "Balcony" },
{ "_id": "64a1b2c3d4e5f6789012350", "name": "City View" }
],
"mainImageUrl": "https://example.com/images/vh-12-main.jpg",
"media": [
{ "url": "https://example.com/images/vh-12-1.jpg", "type": "image" },
{ "url": "https://example.com/images/vh-12-2.jpg", "type": "image" }
],
"deliveryDetails": {
"type": "months",
"date": "2026-12-01",
"months": 18
},
"createdAt": "2024-08-07T10:30:00.000Z",
"updatedAt": "2024-08-10T14:15:00.000Z"
}
}
Error Responses
404 Not Found - Unit does not exist
{
"statusCode": 404,
"message": "There is no unit with slug: non-existent-slug",
"error": "Not Found"
}
401 Unauthorized - Missing or Invalid API Key
{
"statusCode": 401,
"message": "Access Denied, API key not provided",
"error": "Unauthorized"
}
Code Examples
cURL
curl -X GET "https://your-domain/external/apis/v1.0/units/luxury-apartment-vh-12" \
-H "api-key: YOUR_API_KEY"
JavaScript/Node.js
const apiKey = process.env.SAKNEEN_API_KEY;
const baseDomain = process.env.SAKNEEN_DOMAIN;
async function getUnitBySlug(slug) {
const response = await fetch(
`https://${baseDomain}/external/apis/v1.0/units/${encodeURIComponent(slug)}`,
{
method: 'GET',
headers: { 'api-key': apiKey },
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP ${response.status}: ${errorData.message || response.statusText}`);
}
return await response.json();
}
// Usage
getUnitBySlug('luxury-apartment-vh-12')
.then(result => console.log('Unit:', result.data))
.catch(error => console.error('Error:', error));
Python
import os
import requests
from urllib.parse import quote
api_key = os.getenv('SAKNEEN_API_KEY')
base_domain = os.getenv('SAKNEEN_DOMAIN')
def get_unit_by_slug(slug):
headers = { 'api-key': api_key }
response = requests.get(
f'https://{base_domain}/external/apis/v1.0/units/{quote(slug)}',
headers=headers
)
response.raise_for_status()
return response.json()
# Usage
try:
result = get_unit_by_slug('luxury-apartment-vh-12')
unit = result['data']
print(f"Unit: {unit['unitId']} - {unit['title']}")
print(f"Price: {unit['price']} {unit['priceCurrency']}")
print(f"Status: {unit['status']}")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
print('Unit not found')
else:
print(f'Error: {e}')
PHP
<?php
function getUnitBySlug($slug) {
$domain = $_ENV['SAKNEEN_DOMAIN'];
$apiKey = $_ENV['SAKNEEN_API_KEY'];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{$domain}/external/apis/v1.0/units/" . urlencode($slug),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['api-key: ' . $apiKey],
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($httpCode === 404) {
throw new Exception("Unit not found");
}
if ($httpCode !== 200) {
throw new Exception("HTTP Error: $httpCode");
}
return json_decode($response, true);
}
// Usage
try {
$result = getUnitBySlug('luxury-apartment-vh-12');
$unit = $result['data'];
echo "Unit: {$unit['unitId']} - {$unit['title']}\n";
echo "Price: {$unit['price']} {$unit['priceCurrency']}\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
Create or Update Unit
Create a new unit or update an existing one in the Sakneen system.
Endpoint
POST /external/apis/v1.0/units
Authentication
All requests require a valid API key in the header:
api-key: YOUR_API_KEY
Request Body Schema
Required Fields
| Field | Type | Description |
|---|---|---|
recordSystemId | string | Unique system identifier for the record |
unitId | string | Unique identifier for the unit |
Optional Fields
| Field | Type | Description | Validation |
|---|---|---|---|
price | number | Base price of the unit | >= 0 |
bua | number | Built-up area in square meters | >= 0 |
propertyType | string | Type of property | Any string |
status | string | Current status of the unit | Any string |
bedrooms | number | Number of bedrooms | >= 0 |
bathrooms | number | Number of bathrooms | >= 0 |
landArea | number | Land area in square meters | >= 0 |
gardenArea | number | Garden area in square meters | >= 0 |
finishingType | string | Type of finishing | Any string |
floor | string | Floor level | Any string |
amenities | string[] | Array of amenity names | Array of strings |
features | string[] | Array of feature names | Array of strings |
saleType | string | Type of sale | primary or resale (default: primary) |
priceByYear | object | Price variations by year | Key-value pairs (year: price) |
priceByPaymentPlan | array | Array of payment plan pricing | See Payment Plan Schema below |
Payment Plan Schema
Each item in priceByPaymentPlan array should contain:
| Field | Type | Required | Description | Validation |
|---|---|---|---|---|
price | number | Yes | Price for this payment plan | >= 0 |
paymentPlanId | string | Yes | MongoDB ObjectId of payment plan | Valid MongoDB ObjectId |
finishingTypeId | string | Yes | MongoDB ObjectId of finishing type | Valid MongoDB ObjectId |
numberOfYears | number | Optional | Payment plan duration in years | >= 0 |
Headers
| Header | Required | Description |
|---|---|---|
api-key | Yes | Your organization's API key |
Content-Type | Yes | Must be application/json |
language | Optional | Language preference (e.g., en-us, ar-eg) |
Complete Example Request
{
"recordSystemId": "00163EAB688B1EECA9C3E1F963E029CC",
"unitId": "VH-12",
"price": 1200000,
"bua": 120,
"propertyType": "apartment",
"status": "available",
"bedrooms": 2,
"bathrooms": 2,
"landArea": 150,
"gardenArea": 30,
"finishingType": "Fully finished",
"floor": "2nd",
"amenities": ["wifi", "parking", "gym", "pool"],
"features": ["balcony", "parking", "city_view"],
"saleType": "primary",
"priceByYear": {
"2024": 1200000,
"2025": 1250000,
"2026": 1300000
},
"priceByPaymentPlan": [
{
"price": 1200000,
"paymentPlanId": "64a1b2c3d4e5f6789012345",
"finishingTypeId": "64a1b2c3d4e5f6789012346",
"numberOfYears": 5
},
{
"price": 1150000,
"paymentPlanId": "64a1b2c3d4e5f6789012347",
"finishingTypeId": "64a1b2c3d4e5f6789012348",
"numberOfYears": 3
}
]
}
Success Response (201 Created)
{
"success": true,
"message": "Unit created/updated successfully",
"data": {
"unitId": "VH-12",
"recordSystemId": "00163EAB688B1EECA9C3E1F963E029CC"
}
}
Error Responses
400 Bad Request - Validation Error
{
"statusCode": 400,
"message": [
"recordSystemId should not be empty",
"unitId should not be empty",
"price must be a number conforming to the specified constraints"
],
"error": "Bad Request"
}
401 Unauthorized - Invalid API Key
{
"statusCode": 401,
"message": "Access Denied, API key not provided",
"error": "Unauthorized"
}
Code Examples
cURL
curl -X POST "https://your-domain/external/apis/v1.0/units" \
-H "api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "language: en-us" \
-d '{
"recordSystemId": "00163EAB688B1EECA9C3E1F963E029CC",
"unitId": "VH-12",
"price": 1200000,
"bua": 120,
"propertyType": "apartment",
"status": "available",
"bedrooms": 2,
"bathrooms": 2,
"landArea": 150,
"finishingType": "Fully finished",
"floor": "2nd",
"amenities": ["wifi", "parking", "gym"],
"features": ["balcony", "parking"],
"saleType": "primary"
}'
JavaScript/Node.js
const apiKey = process.env.SAKNEEN_API_KEY;
const baseDomain = process.env.SAKNEEN_DOMAIN;
async function createUnit(unitData) {
try {
const response = await fetch(`https://${baseDomain}/external/apis/v1.0/units`, {
method: 'POST',
headers: {
'api-key': apiKey,
'Content-Type': 'application/json',
'language': 'en-us'
},
body: JSON.stringify(unitData)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP ${response.status}: ${errorData.message || response.statusText}`);
}
const result = await response.json();
console.log('Unit created:', result);
return result;
} catch (error) {
console.error('Error creating unit:', error);
throw error;
}
}
// Usage
const unitData = {
recordSystemId: "00163EAB688B1EECA9C3E1F963E029CC",
unitId: "VH-12",
price: 1200000,
bua: 120,
propertyType: "apartment",
status: "available",
bedrooms: 2,
bathrooms: 2,
landArea: 150,
gardenArea: 30,
finishingType: "Fully finished",
floor: "2nd",
amenities: ["wifi", "parking", "gym", "pool"],
features: ["balcony", "parking", "city_view"],
saleType: "primary"
};
createUnit(unitData)
.then(result => console.log('Success:', result))
.catch(error => console.error('Error:', error));
Python
import requests
import os
def create_unit(unit_data):
api_key = os.getenv('SAKNEEN_API_KEY')
base_domain = os.getenv('SAKNEEN_DOMAIN')
url = f'https://{base_domain}/external/apis/v1.0/units'
headers = {
'api-key': api_key,
'Content-Type': 'application/json',
'language': 'en-us'
}
try:
response = requests.post(url, headers=headers, json=unit_data)
response.raise_for_status()
result = response.json()
print(f'Unit created: {result}')
return result
except requests.exceptions.RequestException as e:
print(f'Error creating unit: {e}')
raise
# Usage
unit_data = {
"recordSystemId": "00163EAB688B1EECA9C3E1F963E029CC",
"unitId": "VH-12",
"price": 1200000,
"bua": 120,
"propertyType": "apartment",
"status": "available",
"bedrooms": 2,
"bathrooms": 2,
"landArea": 150,
"gardenArea": 30,
"finishingType": "Fully finished",
"floor": "2nd",
"amenities": ["wifi", "parking", "gym", "pool"],
"features": ["balcony", "parking", "city_view"],
"saleType": "primary"
}
try:
result = create_unit(unit_data)
print(f'Success: {result}')
except Exception as e:
print(f'Error: {e}')
PHP
<?php
function createUnit($unitData) {
$domain = $_ENV['SAKNEEN_DOMAIN'];
$apiKey = $_ENV['SAKNEEN_API_KEY'];
$headers = [
'api-key: ' . $apiKey,
'Content-Type: application/json'
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{$domain}/external/apis/v1.0/units",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($unitData),
CURLOPT_HTTPHEADER => $headers,
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($httpCode !== 201) {
throw new Exception("HTTP Error: $httpCode");
}
return json_decode($response, true);
}
// Usage
$unitData = [
'recordSystemId' => '00163EAB688B1EECA9C3E1F963E029CC',
'unitId' => 'VH-12',
'price' => 1200000,
'bua' => 120,
'propertyType' => 'apartment',
'status' => 'available',
'bedrooms' => 2,
'bathrooms' => 2,
'landArea' => 150,
'finishingType' => 'Fully finished',
'floor' => '2nd',
'amenities' => ['wifi', 'parking'],
'features' => ['parking'],
'saleType' => 'primary'
];
try {
$result = createUnit($unitData);
echo "Unit created: " . json_encode($result) . "\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
Validation Rules
Required Field Validation
- All required fields must be present and not empty
pricemust be a positive numberpropertyTypemust be one of the allowed valuesstatusmust be one of the allowed values
Data Type Validation
bedroomsandbathroomsmust be integers if providedbuaandlandAreamust be numbers if providedamenitiesandfeaturesmust be arrays if provideddeliveryDatemust follow DD-MM-YYYY format if provided
Business Logic Validation
- If
deliveryDateTypeis "months",deliveryDateMonthsshould be provided - If
deliveryDateTypeis "date",deliveryDateshould be provided unitIdshould be unique within the system
Error Handling
Common Error Scenarios
-
Missing API Key (401)
- Ensure API key is included in headers
- Verify API key is valid
-
Invalid Data (400)
- Check all required fields are present
- Verify data types match the specification
- Ensure enum values are from allowed lists
-
Unit Not Found (404) (GET by slug only)
- Verify the slug exists and belongs to your organization
- Check both
en-usandar-egslugs
-
Network Issues
- Implement retry logic for transient failures
- Set appropriate timeouts
Best Practices
- Pagination: Use reasonable page sizes (10-50) to balance performance and usability. Avoid requesting the maximum limit of 100 unless necessary.
- Filtering: Use the
statusesfilter to retrieve only the units you need, reducing response size and processing time. - Slug Lookup: Cache unit slugs client-side to avoid unnecessary list requests when you need a specific unit.
- Data Validation: Always validate data before sending to the POST API.
- Error Handling: Implement comprehensive error handling for all endpoints.
- Retry Logic: Add retry mechanisms for network failures.
- Rate Limiting: Respect API rate limits.
Use Cases
Property Listing Websites
- Use List Units to display paginated property listings with status filtering
- Use Get Unit by Slug for individual property detail pages with SEO-friendly URLs
- Use Create or Update Unit to sync inventory from your property management system
Real Estate Portals
- Import and browse property inventory using the list endpoint
- Link to individual unit pages using slugs
- Keep unit data synchronized with bulk create/update operations
CRM Integration
- Retrieve available units for sales teams
- Display unit details in customer-facing views
- Track unit availability changes via status filtering