Hotel API Documentation
A simple, RESTful JSON API for looking up hotel/property information by ID. All endpoints accept and return application/json over HTTPS.
Overview
The Hotel API exposes a curated read‑only catalogue of properties (hotels, apartments, etc.) including their location, accommodation type, chain affiliation, facilities, and image gallery.
- RESTful — predictable resource URLs, standard HTTP verbs and status codes.
- JSON only — requests must send
Content-Type: application/jsonandAccept: application/json. - UTF‑8 — all responses are UTF‑8 encoded.
- ISO 8601 — timestamps use ISO 8601 with timezone (e.g.
2026-05-10T08:30:00+00:00).
Base URL & format
All endpoints below are relative to:
https://parto.pro.plus/api
Headers required on every request
| Header | Value |
|---|---|
Accept | application/json |
Content-Type | application/json (for requests with a body) |
Authentication
Hotel-search endpoints are protected by a short-lived SessionId issued by POST /api/Authenticate/CreateSession. The flow mirrors the Parto CRS contract so existing client libraries can be reused with minimal change:
- Compute the
SHA-512hex digest of the plaintext password you received from the administrator. - Call
POST /api/Authenticate/CreateSessionwith{ OfficeId, UserName, Password }to receive aSessionId. - Include the
SessionIdin the JSON body (or asX-Session-Idheader) of every protected request. - The session is valid for 15 minutes and is automatically extended by another 15 minutes on every successful authenticated call.
The public /properties/lookup and /cities/search endpoints do not require authentication.
SessionId as secrets.
Errors
The API uses conventional HTTP status codes to signal success or failure of a request.
| Status | Meaning |
|---|---|
| 200 | OK — the request succeeded. |
| 422 | Unprocessable Entity — validation failed. Inspect errors. |
| 405 | Method Not Allowed — you used a wrong HTTP verb. |
| 500 | Internal Server Error — something went wrong on our side. |
Validation error shape
// HTTP/1.1 422 Unprocessable Entity
{
"message": "The ids field is required.",
"errors": {
"ids": ["The ids field is required."]
}
}
Rate limits
By default the API allows up to 60 requests per minute per IP. When the limit is exceeded, the API returns 429 Too Many Requests. The current limit is reflected in the response headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per window. |
X-RateLimit-Remaining | Requests left in the current window. |
Retry-After | Seconds to wait before retrying (only on 429). |
Lookup properties
Returns a list of properties matching the given IDs, including their full details (location, facilities, images, etc). Up to 200 IDs may be requested in a single call.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
ids | integer[] | required | Array of property IDs. Min 1, max 200. |
Example request
curl -X POST "https://parto.pro.plus/api/properties/lookup" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"ids":[101, 202, 303]}'const res = await fetch("https://parto.pro.plus/api/properties/lookup", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ ids: [101, 202, 303] }),
});
const json = await res.json();
console.log(json.data);$response = Http::acceptJson()->post('https://parto.pro.plus/api/properties/lookup', [
'ids' => [101, 202, 303],
]);
$properties = $response->json('data');import requests
r = requests.post(
"https://parto.pro.plus/api/properties/lookup",
json={"ids": [101, 202, 303]},
headers={"Accept": "application/json"},
timeout=10,
)
data = r.json()["data"]Successful response 200
{
"data": [
{
"id": 101,
"name": "Grand Plaza Hotel",
"rating": 4.5,
"address": "12 Avenue de la Liberté",
"postal_code": "75008",
"phone": "+33 1 23 45 67 89",
"url": "https://example.com/grand-plaza",
"last_update": "2026-05-10T08:30:00+00:00",
"location": {
"latitude": 48.8738,
"longitude": 2.295,
"city": { "id": 7, "name": "Paris" },
"destination": { "id": 21, "name": "Île-de-France" },
"country_id": 33
},
"accommodation": { "id": 1, "name": "Hotel" },
"chain": { "id": 42, "name": "Grand Collection" },
"facilities": [
{ "id": 5, "name": "Free Wi-Fi", "group": { "id": 2, "name": "Internet" } },
{ "id": 14, "name": "Swimming Pool", "group": { "id": 9, "name": "Wellness" } }
],
"images": [
{ "url": "https://cdn.example.com/p101/1.jpg", "thumbnail_url": "https://cdn.example.com/p101/1-thumb.jpg" }
]
}
],
"not_found": [303]
}
not_found array. The endpoint always returns 200 OK as long as the request itself was valid.
Validation errors 422
{
"message": "The ids field is required.",
"errors": {
"ids": [
"The ids field is required."
]
}
}
idsmust be an array.idsmust contain at least 1 element and at most 200.- Every element of
idsmust be an integer.
Search cities
Find cities by partial or full name. Each result includes the city's destination (region) and country. Useful for autocomplete and city-pickers.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
q | string | required | Full or partial city name. 1–100 chars. Case-insensitive. |
limit | integer | optional | Max results to return. 1–200. Default 50. |
q are returned first, then the rest, both sorted alphabetically.
q=p32414 bypasses filtering and limit, and returns every city in the catalogue, alphabetically. The response can be several megabytes.
Example request
curl -G "https://parto.pro.plus/api/cities/search" \
--data-urlencode "q=par" \
--data-urlencode "limit=10" \
-H "Accept: application/json"const params = new URLSearchParams({ q: 'par', limit: 10 });
const res = await fetch("https://parto.pro.plus/api/cities/search?" + params, {
headers: { "Accept": "application/json" },
});
const json = await res.json();
console.log(json.data);$response = Http::acceptJson()->get('https://parto.pro.plus/api/cities/search', [
'q' => 'par',
'limit' => 10,
]);
$cities = $response->json('data');import requests
r = requests.get(
"https://parto.pro.plus/api/cities/search",
params={"q": "par", "limit": 10},
headers={"Accept": "application/json"},
timeout=10,
)
cities = r.json()["data"]Successful response 200
{
"data": [
{
"id": 7,
"external_id": 12345,
"name": "Paris",
"destination": { "id": 21, "name": "Île-de-France" },
"country": { "id": 33, "code": "FR", "name": "France" }
},
{
"id": 82,
"external_id": 67890,
"name": "Parma",
"destination": { "id": 114, "name": "Emilia-Romagna" },
"country": { "id": 39, "code": "IT", "name": "Italy" }
}
],
"meta": {
"query": "par",
"count": 2,
"limit": 10
}
}
When the dump-all token p32414 is used, meta.limit is null.
Validation errors 422
qis required and must be 1–100 characters.limit, if provided, must be an integer between 1 and 200.
Create a session
Exchange your credentials for a short-lived SessionId that authorises calls to the protected endpoints. The contract mirrors the upstream Parto CRS exactly.
Request body
| Field | Type | Description |
|---|---|---|
OfficeId required | string | Your numeric OfficeId, shown on your user dashboard. |
UserName required | string | Your alphanumeric API username (e.g. API582716). |
Password required | string (hex) | The SHA-512 hex digest of your plaintext password — not the plaintext. |
Example request
curl -X POST "https://parto.pro.plus/api/Authenticate/CreateSession" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"OfficeId": "1273298",
"UserName": "API582716",
"Password": "<sha512-hex-of-your-password>"
}'
Successful response 200
{
"Error": null,
"Success": true,
"SessionId": "01492146-ae4c-f111-b885-00505682c602"
}
Invalid credentials 200
For security reasons, invalid credentials return HTTP 200 with Success:false (rather than 401), matching the upstream Parto behaviour:
{
"Error": { "Id": "Err0102001", "Message": "Invalid login credentials supplied" },
"Success": false,
"SessionId": null
}
Session lifecycle
- Each
SessionIdis valid for 15 minutes from the time of last use. - Every successful authenticated call extends the expiry by another 15 minutes (rolling window).
- An expired or unknown SessionId yields HTTP
401withErr0102003. - An inactive account yields HTTP
403withErr0102004.
Computing the SHA-512 digest
# bash
echo -n 'MyPlaintextPassword' | openssl dgst -sha512 | awk '{print $2}'
# php
echo hash('sha512', 'MyPlaintextPassword');
// javascript (Web Crypto)
const buf = await crypto.subtle.digest('SHA-512', new TextEncoder().encode(plaintext));
const hex = [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
Hotel availability search
Proxies a hotel availability search to the upstream Parto CRS and decorates every returned itinerary with the matching local property's data (joined by Parto HotelId = properties.external_id). Upstream session management is handled server‑side and is fully transparent to clients.
Authentication
This endpoint requires a valid SessionId obtained from /api/Authenticate/CreateSession. Provide it either in the JSON body as "SessionId" or as the X-Session-Id request header.
Request body
| Field | Type | Description |
|---|---|---|
SessionId required | string (UUID) | The SessionId from /api/Authenticate/CreateSession. May be sent in the body or as the X-Session-Id header. |
CheckIn required | string (YYYY‑MM‑DD) | Check‑in date. |
CheckOut required | string (YYYY‑MM‑DD) | Check‑out date. Must be after CheckIn. |
NationalityId required | string (ISO 3166‑1 alpha‑2) | Guest nationality, e.g. "TR". |
HotelId | integer | Single hotel. One of HotelId/HotelIdList/CityId is required. |
HotelIdList | integer[] | List of hotel IDs. |
CityId | integer | City ID (Parto external ID). |
CountryCode | string (ISO 3166‑1 alpha‑2) | Optional country filter. |
Occupancies required | object[] | One entry per room: {AdultCount, ChildCount, ChildAges:[]}. ChildAges length must equal ChildCount. |
Example request
curl -X POST "https://parto.pro.plus/api/Hotel/HotelAvailability" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"SessionId": "01492146-ae4c-f111-b885-00505682c602",
"CheckIn": "2026-05-24",
"CheckOut": "2026-05-26",
"NationalityId": "TR",
"CityId": 924,
"Occupancies": [
{ "AdultCount": 2, "ChildCount": 0, "ChildAges": [] }
]
}'
Successful response 200
The upstream Parto response is returned verbatim — every field (Success, Error, SearchId, PricedItineraries[] with all of HotelId, RoomRates, HotelPolicy, ExtraCharge, Surcharges, Amenities, Rooms, BoardCode, Currency, TotalFare, etc.) is preserved as-is. The proxy only adds two things on top:
- A new
Hotelkey on eachPricedItinerarycontaining the normalised local property record (ornullif the upstreamHotelIdis not present in the local catalogue). - A new top‑level
Metaobject withcount(total itineraries),matched(how many had local data), andnot_found(HotelIds with no local match).
{
// ── Everything below this line comes straight from Parto, untouched ──
"Success": true,
"Error": null,
"SearchId": "abc-123-…",
"PricedItineraries": [
{
"HotelId": 12952,
"BoardCode": "BB",
"Currency": "EUR",
"TotalFare": 123.45,
"RoomRates": [ /* full upstream room-rate objects */ ],
"Rooms": [ /* full upstream rooms */ ],
"HotelPolicy": [ /* … */ ],
"ExtraCharge": [ /* … */ ],
"Surcharges": [ /* … */ ],
"Amenities": [ /* … */ ],
// … any other fields returned by Parto are kept as-is …
// ── Added by this proxy ──
"Hotel": {
"id": 4012,
"external_id": 12952,
"name": "Example Hotel",
"rating": 4.5,
"review_score": 8.7,
"last_update": "2026-05-01T12:00:00+00:00",
"contact": { "phone": "…", "fax": "…", "email": "…", "url": "…" },
"address": {
"line": "…",
"postal_code": "34000",
"city": { "id": 1, "external_id": 924, "name": "Istanbul" },
"destination": { "id": 5, "external_id": 700, "name": "Sultanahmet" },
"country": { "id": 1, "code": "TR", "name": "Türkiye" }
},
"location": { "latitude": 41.01, "longitude": 28.97 },
"accommodation": { "id": 1, "external_id": 10, "name": "Hotel" },
"chain": null,
"facility_groups": [
{ "id": 3, "name": "Amenities", "facilities": [
{ "id": 11, "external_id": 2001, "name": "Free Wi-Fi" }
] }
],
"images_count": 12,
"images": [
{ "url": "…", "thumbnail_url": "…" }
]
}
}
],
// ── Added by this proxy ──
"Meta": {
"count": 9,
"matched": 8,
"not_found": [99999]
}
}
Validation errors 422
CheckIn/CheckOutmust beYYYY‑MM‑DDandCheckOutmust be afterCheckIn.- Exactly one of
HotelId,HotelIdList,CityIdmust be provided. - Per occupancy,
ChildAgeslength must matchChildCount.
Upstream errors 502
If the upstream Parto CRS is unreachable or returns an unexpected error, the response is { "Success": false, "Error": "…" } with HTTP 502. Upstream business errors (e.g. invalid filters) are passed through with HTTP 200 and Success:false.
Hotel check rate
Re-prices a specific itinerary returned by /Hotel/HotelAvailability against the upstream Parto CRS to confirm the current price, cancellation policy and availability before booking. The client request is forwarded verbatim to Parto (only the upstream SessionId is injected server-side) and the upstream response is returned to the client unchanged.
Authentication
Requires a valid SessionId obtained from /api/Authenticate/CreateSession. Provide it either in the JSON body as "SessionId" or via the X-Session-Id request header.
Request body
| Field | Type | Description |
|---|---|---|
SessionId required | string (UUID) | The SessionId from /api/Authenticate/CreateSession. May be sent in the body or as the X-Session-Id header. |
FareSourceCode required | string | The opaque FareSourceCode taken from a PricedItinerary in the availability response. Send it back exactly as received — do not decode or modify. |
FixStayId | string | null | Optional fixed-stay identifier. Send null if not applicable. |
Nationality required | string (ISO 3166-1 alpha-2) | Guest nationality, e.g. "IR". |
Example request
curl -X POST "https://parto.pro.plus/api/Hotel/HotelCheckRate" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"SessionId": "914363ab-981f-47f1-b6e5-6243a173df08",
"FareSourceCode": "<paste from availability>",
"FixStayId": null,
"Nationality": "IR"
}'
Successful response 200
The upstream Parto response is returned verbatim — every field (Success, Error, PricedItinerary, HotelFare, HotelPolicy, RoomRates, etc.) is preserved as-is. The proxy only adds a Hotel key on each PricedItinerary (singular or plural) carrying the normalised local property record (or null if the upstream HotelId is not present in the local catalogue) — the same shape returned by /Hotel/HotelAvailability.
Validation errors 422
FareSourceCodeis required.Nationalityis required and must be a 2-letter ISO country code.
Upstream errors 502
If the upstream Parto CRS is unreachable or returns an unexpected error, the response is { "Success": false, "Error": "…" } with HTTP 502. Upstream business errors (e.g. expired FareSourceCode) are passed through with HTTP 200 and Success:false.
Property object
The canonical shape of a property returned by the API.
| Field | Type | Description |
|---|---|---|
id | integer | Unique property identifier. |
name | string | Display name of the property. |
rating | float | null | Star rating, e.g. 4.5. |
address | string | null | Street address. |
postal_code | string | null | Postal/ZIP code. |
phone | string | null | Contact phone number. |
url | string | null | Public website URL. |
last_update | ISO 8601 | null | Last time the property was modified upstream. |
location | object | Latitude, longitude, city, destination, country. |
accommodation | object | null | Accommodation type (e.g. Hotel, Apartment). |
chain | object | null | Hotel chain affiliation. |
facilities | object[] | List of facilities, each with its group. |
images | object[] | Images with full and thumbnail URLs. |
City object
The shape of a city returned by GET /cities/search.
| Field | Type | Description |
|---|---|---|
id | integer | Internal city identifier (stable within this API). |
external_id | integer | null | Upstream city ID from the source data feed. |
name | string | City name. |
destination | object | null | The destination (region/area) the city belongs to: {id, name}. |
country | object | null | The country the city is in: {id, code, name}. code is ISO 3166-1 alpha-2. |
Changelog
2026-05-10 — v1
- Initial public release of
POST /properties/lookup. - Added
GET /cities/searchfor partial-name city search.