Reservations Management
Create or update unit reservations in Sakneen from an external system using external record identifiers.
Overview
The Reservations API accepts reservation data from partner systems and queues it for asynchronous processing. A successful request means Sakneen accepted the payload for processing; the actual create/update work runs after the request through the Google Cloud Tasks-backed queue ledger.
Use this endpoint when your external system owns the reservation lifecycle and needs to send reservation records into Sakneen.
Use POST /external/apis/v1.0/reservations to create or update reservation records from your external system.
Use POST /external/apis/v1.0/reservations/webhook for callback flows that update payment, cheque, or status information for existing Sakneen reservations.
Endpoint
POST /external/apis/v1.0/reservations
Authentication
All requests require a valid API key. The API key must be configured with allowExternal: true permission to access this endpoint.
Include your API key as the api-key request header:
api-key: YOUR_API_KEY
Headers
| Header | Required | Description |
|---|---|---|
api-key | Yes | Your organization's API key |
Content-Type | Yes | Must be application/json |
language | Optional | Language preference, for example en-us or ar-eg |
Request Body Schema
Required Fields
| Field | Type | Description | Validation |
|---|---|---|---|
recordSystemId | string | Unique reservation identifier from your external system. Sakneen uses this value to create or update the reservation. | Required, non-empty string |
unitRecordSystemId | string | External unit identifier. Sakneen resolves this against the unit's recordSystemId. | Required, non-empty string |
status | enum | Reservation status to store in Sakneen. | Required, one of the status values below |
Optional Fields
| Field | Type | Description | Validation |
|---|---|---|---|
fullName | string | Customer full name | Optional, non-empty string |
phoneNumber | string | Customer phone number | Optional, non-empty string |
email | string | Customer email address | Optional, valid email |
nationalID | string | Customer national ID | Optional, non-empty string |
crmID | string | External CRM identifier | Optional, non-empty string |
reservationAmount | number | Reservation amount | Optional, must be 0 or greater |
expiryDate | string | Reservation expiry date | Optional, ISO 8601 date string |
crmOpportunityId | string | External CRM opportunity identifier | Optional, non-empty string |
salesPersonEmail | string | Email address of the Sakneen sales person to attach to the reservation | Optional, valid email |
reservationData | object | Additional reservation fields to map into the organization's reservation form when configured | Optional object |
Enum: status
| Value | Meaning |
|---|---|
pending | Reservation is pending |
approved | Reservation is approved |
rejected | Reservation is rejected |
canceled | Reservation is canceled |
expired | Reservation is expired |
Matching Behavior
recordSystemIdis the external reservation ID and is used as the upsert key.- If a non-deleted reservation already exists with the same
recordSystemIdfor your organization, Sakneen updates it. - If no matching reservation exists, Sakneen creates a new reservation and assigns a Sakneen serial number.
unitRecordSystemIdis matched against the unit's externalrecordSystemId.- If the unit cannot be resolved during processing, Sakneen still creates or updates the reservation and stores the original
unitRecordSystemIdin the raw reservation payload. The reservation will not be linked to a local unit until the unit exists and the reservation is sent again. - This endpoint does not update unit data or unit status. Send unit changes through
POST /external/apis/v1.0/units.
Async Processing
A successful request returns 201 Created with { "ok": true }. This means the request was accepted and queued.
Reservation processing happens asynchronously. If the queued job cannot resolve unitRecordSystemId to a local unit, Sakneen still creates or updates the reservation without a local unit link and records an operational warning. Other processing failures are tracked internally by Sakneen's Google Cloud Tasks-backed queue ledger and operational notifications; those failures are not returned to the original HTTP request.
Request Example
{
"recordSystemId": "RES-001",
"unitRecordSystemId": "UNIT-001",
"status": "approved",
"fullName": "Palm Hills Client",
"phoneNumber": "+201001234567",
"email": "[email protected]",
"nationalID": "29801011234567",
"crmID": "CRM-123",
"reservationAmount": 150000,
"expiryDate": "2026-07-01T00:00:00.000Z",
"crmOpportunityId": "OPP-456",
"salesPersonEmail": "[email protected]",
"reservationData": {
"paymentPlan": "5 years",
"source": "external-crm"
}
}
Success Response
201 Created
{
"ok": true
}
Error Responses
400 Bad Request
{
"message": [
"recordSystemId should not be empty",
"unitRecordSystemId should not be empty",
"status must be one of the following values: pending, approved, rejected, canceled, expired"
],
"error": "Bad Request",
"statusCode": 400
}
401 Unauthorized
{
"statusCode": 401,
"message": "Access Denied, API key not valid"
}
Code Examples
cURL
curl -X POST "https://your-domain/external/apis/v1.0/reservations" \
-H "api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "language: en-us" \
-d '{
"recordSystemId": "RES-001",
"unitRecordSystemId": "UNIT-001",
"status": "approved",
"fullName": "Palm Hills Client",
"phoneNumber": "+201001234567",
"email": "[email protected]",
"reservationAmount": 150000
}'
JavaScript/Node.js
const apiKey = process.env.SAKNEEN_API_KEY;
const baseDomain = process.env.SAKNEEN_DOMAIN;
async function upsertReservation(reservationData) {
const response = await fetch(
`https://${baseDomain}/external/apis/v1.0/reservations`,
{
method: 'POST',
headers: {
'api-key': apiKey,
'Content-Type': 'application/json',
language: 'en-us',
},
body: JSON.stringify(reservationData),
},
);
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
throw new Error(
`Reservation request failed (${response.status}): ${JSON.stringify(errorBody)}`,
);
}
return response.json();
}
await upsertReservation({
recordSystemId: 'RES-001',
unitRecordSystemId: 'UNIT-001',
status: 'approved',
fullName: 'Palm Hills Client',
phoneNumber: '+201001234567',
email: '[email protected]',
reservationAmount: 150000,
});
Python
import os
import requests
api_key = os.getenv('SAKNEEN_API_KEY')
base_domain = os.getenv('SAKNEEN_DOMAIN')
def upsert_reservation(reservation_data):
response = requests.post(
f'https://{base_domain}/external/apis/v1.0/reservations',
json=reservation_data,
headers={
'api-key': api_key,
'Content-Type': 'application/json',
'language': 'en-us',
},
)
response.raise_for_status()
return response.json()
upsert_reservation({
'recordSystemId': 'RES-001',
'unitRecordSystemId': 'UNIT-001',
'status': 'approved',
'fullName': 'Palm Hills Client',
'phoneNumber': '+201001234567',
'email': '[email protected]',
'reservationAmount': 150000,
})
Best Practices
- Send units first through
POST /external/apis/v1.0/units, then send reservations that reference those units byunitRecordSystemId. - If a reservation is accepted before its unit exists in Sakneen, send the unit through
POST /external/apis/v1.0/unitsand then resend the reservation to link it. - Keep
recordSystemIdstable for the lifetime of the reservation in your external system. - Retry only the initial HTTP request if Sakneen does not return a
201response. - Treat
{ "ok": true }as acceptance into the queue, not confirmation that the reservation has already been written.