Skip to main content

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

ParameterTypeDefaultDescription
pagenumber1Page number (min: 1)
limitnumber10Number of results per page (min: 1, max: 100)
sortBystringdefaultSortField to sort by (defaults to createdAt)
orderstringdescSort order: asc, desc, ascending, or descending
statusesstring[]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
brokerIdstringBroker organization ID. Returns only units that are visible to brokers and assigned to this broker. An invalid ID returns 400
Hard Limit

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:

FieldTypeDescription
_idstringMongoDB document ID
recordSystemIdstringUnique system identifier for the record
unitIdstringUnique identifier for the unit
titlestringUnit title
translationsobjectLocalized data including name and slug per language (en-us, ar-eg)
statusstringCurrent status of the unit
saleTypestringType of sale (primary or resale)
propertyTypeobjectPopulated property type details
pricenumberBase price of the unit
priceCurrencystringCurrency code for the price
buanumberBuilt-up area in square meters
totalBuanumberTotal built-up area in square meters
landAreanumberLand area in square meters
gardenAreanumberGarden area in square meters
bedsnumberNumber of bedrooms
bathroomsnumberNumber of bathrooms
floorstringFloor level
finishingTypeobjectPopulated finishing type details
amenitiesarrayPopulated list of amenities
featuresarrayPopulated list of features
mainImageUrlstringURL of the unit's main image
mediaarrayArray of media objects (images, videos, etc.)
deliveryDetailsobjectDelivery date and timeline information
createdAtstringISO 8601 timestamp of creation
updatedAtstringISO 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

ParameterTypeDefaultDescription
statusesstring[]Filter by unit statuses. Values are case-sensitive (e.g., Available). Repeat the parameter for multiple values (statuses=Available&statuses=Reserved)
brokerIdstringBroker organization ID. Returns only units that are visible to brokers and assigned to this broker
dateFilterBystringDate field to filter on: createdAt or updatedAt
startDatestringYYYY-MM-DD. Returns units from the start of this day
endDatestringYYYY-MM-DD. Returns units up to the end of this day
sortBystringdefaultSortField to sort by (defaults to createdAt)
orderstringdescSort order: asc, desc, ascending, or descending
  • Without statuses, the export returns the same unit statuses as List Units.
  • The date filter applies only when dateFilterBy is sent with startDate, endDate, or both. Day boundaries use Egypt time (Africa/Cairo), and both days are included.
Hard Limit

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.

FieldTypeDescription
_idstringUnit ID
unitIdstringUnit code
recordSystemIdstringUnit ID in your system
namestringUnit name in the response language
slugstringUnit slug in the response language
titlestringUnit title
statusstringUnit status (e.g., Available)
saleTypestringprimary or resale
pricenumberUnit price
priceCurrencystringCurrency code for the price
priceByYearobjectPrices by payment plan length. Keys are numbers of years
priceOnenumberSee Payment Plan Prices
priceTwonumber | nullSee Payment Plan Prices
buaPricePerSquareMeternumberPrice per square meter of built-up area
lanAreaPricePerSquareMeternumberPrice per square meter of land area
buanumberBuilt-up area in square meters. Uses the unit design's bua when the unit has none
coveredTerracenumberCovered terrace area in square meters
totalBuanumberbua + coveredTerrace when coveredTerrace is set (including 0). Otherwise the unit's stored totalBua, then bua, then 0. Not rounded
landAreanumberLand area in square meters
gardenAreanumberGarden area in square meters
bedsnumberNumber of bedrooms. Uses the unit design's bedrooms when the unit has none
bathroomsnumberNumber of bathrooms
floorobjectFloor, e.g. { "name": "Third" }
orientationstringUnit orientation
deliveryDetailsobjectDelivery details: type (date or months) with date or numberOfMonths
mainImageUrlstringMain image URL. Uses the unit design's image when the unit has none
mediaarrayThe unit's media combined with its unit design's media. Items include image, imageType, and type
floorPlansstring[]Image URLs of the media items whose imageType is floorplan
unitLocationImagestringUnit location image URL
buildingLocationImageUrlstringBuilding location image URL
locationIdstringID of the compound or phase the unit belongs to
compoundobject | null_id, type (Compound), and translated fields such as name, slug, and description. null when the unit belongs to a phase
phaseobject | 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
compoundNamestringCompound name in the response language. For a unit in a phase, this is the name of the phase's compound
phaseNamestringPhase name in the response language. Returned only for units that belong to a phase
propertyTypeIdstringProperty type ID
typeobjectProperty type: _id and translated fields such as name and slug. {} when the unit has no property type
unitDesignIdstringUnit design ID
unitDesignobject | nullUnit design: _id, bua, bedrooms, mainImageUrl, media, and translated fields such as name and slug
amenitiesIdsstring[]Amenity IDs
amenitiesarrayAmenities: _id and translated fields such as name and slug
featuresIdsstring[]Feature IDs
finishingTypeIdstringFinishing type ID
paymentPlansIdsstring[]Payment plan IDs
paymentPlansarrayPayment plans: _id, numberOfYears, and translated name
brokersIdsstring[]IDs of the broker organizations the unit is assigned to
brokersarrayBroker organizations: _id, name, and slug
createdAtstringISO 8601 timestamp of creation
updatedAtstringISO 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: priceTwo is the highest price and priceOne is the second highest.
  • One price: priceOne is that price and priceTwo is null.
  • No prices: priceOne is omitted and priceTwo is null.

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:

PlaceholderReplaced 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.

warning

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

ParameterTypeDescription
slugstringThe 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

FieldTypeDescription
recordSystemIdstringUnique system identifier for the record
unitIdstringUnique identifier for the unit

Optional Fields

FieldTypeDescriptionValidation
pricenumberBase price of the unit>= 0
buanumberBuilt-up area in square meters>= 0
propertyTypestringType of propertyAny string
statusstringCurrent status of the unitAny string
bedroomsnumberNumber of bedrooms>= 0
bathroomsnumberNumber of bathrooms>= 0
landAreanumberLand area in square meters>= 0
gardenAreanumberGarden area in square meters>= 0
finishingTypestringType of finishingAny string
floorstringFloor levelAny string
amenitiesstring[]Array of amenity namesArray of strings
featuresstring[]Array of feature namesArray of strings
saleTypestringType of saleprimary or resale (default: primary)
priceByYearobjectPrice variations by yearKey-value pairs (year: price)
priceByPaymentPlanarrayArray of payment plan pricingSee Payment Plan Schema below

Payment Plan Schema

Each item in priceByPaymentPlan array should contain:

FieldTypeRequiredDescriptionValidation
pricenumberYesPrice for this payment plan>= 0
paymentPlanIdstringYesMongoDB ObjectId of payment planValid MongoDB ObjectId
finishingTypeIdstringYesMongoDB ObjectId of finishing typeValid MongoDB ObjectId
numberOfYearsnumberOptionalPayment plan duration in years>= 0

Headers

HeaderRequiredDescription
api-keyYesYour organization's API key
Content-TypeYesMust be application/json
languageOptionalLanguage 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
  • price must be a positive number
  • propertyType must be one of the allowed values
  • status must be one of the allowed values

Data Type Validation

  • bedrooms and bathrooms must be integers if provided
  • bua and landArea must be numbers if provided
  • amenities and features must be arrays if provided
  • deliveryDate must follow DD-MM-YYYY format if provided

Business Logic Validation

  • If deliveryDateType is "months", deliveryDateMonths should be provided
  • If deliveryDateType is "date", deliveryDate should be provided
  • unitId should be unique within the system

Error Handling

Common Error Scenarios

  1. Missing API Key (401)

    • Ensure API key is included in headers
    • Verify API key is valid
  2. Invalid Data (400)

    • Check all required fields are present
    • Verify data types match the specification
    • Ensure enum values are from allowed lists
  3. Unit Not Found (404) (GET by slug only)

    • Verify the slug exists and belongs to your organization
    • Check both en-us and ar-eg slugs
  4. Network Issues

    • Implement retry logic for transient failures
    • Set appropriate timeouts

Best Practices

  1. Pagination: Use reasonable page sizes (10-50) to balance performance and usability. Avoid requesting the maximum limit of 100 unless necessary.
  2. Filtering: Use the statuses filter to retrieve only the units you need, reducing response size and processing time.
  3. Slug Lookup: Cache unit slugs client-side to avoid unnecessary list requests when you need a specific unit.
  4. Data Validation: Always validate data before sending to the POST API.
  5. Error Handling: Implement comprehensive error handling for all endpoints.
  6. Retry Logic: Add retry mechanisms for network failures.
  7. 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