Create a property
Create a listing in Doorloom from your own system. The property belongs to the brand behind
your API key, is covered by your integration the moment it exists, and comes back with the same
representation GET /properties returns — so you can store the id mapping from the
201 and never ask Doorloom staff to onboard a property again.
When to use it
Use this when a listing exists in your PMS or channel manager and not yet in Doorloom: during
onboarding, and whenever the host adds a property on your side first. If the property already
exists in Doorloom — created by the host in the app — it is either already in your coverage
(GET /properties lists it)
or Doorloom staff map it for you. Do not create a second copy of it.
To load a portfolio, use
POST /properties/bulk,
which takes up to 25 of these bodies in one call.
Headers
| Field | Type | Required | Description |
|---|---|---|---|
Authorization
|
string | Required | Bearer <your API key>. |
Content-Type
|
string | Required | application/json. |
Idempotency-Key
|
string | Required | 8 to 128 characters, unique per create attempt. A UUID is ideal. |
The idempotency contract
IDEMPOTENCY_KEY_REUSED. Keys are remembered for 24 hours and are shared
with POST /bookings, so never reuse a key across endpoints.
Refusals are stored too. If a create fails with
409 PROPERTY_EXTERNAL_ID_EXISTS,
retrying the same key replays that refusal. To create after correcting the body, use a new key.Request body
Body
| Field | Type | Required | Description |
|---|---|---|---|
external_id
|
string | Optional | Your id for this property. Max 128 characters of A-Z a-z 0-9 . _ : -. Unique within your integration. It becomes the external_id you may use anywhere the API takes a property reference. |
name
|
string | Required | The listing name guests see. Max 255 characters. Returned as name. |
property_type
|
string | Optional | One of villa, apartment, farm_stay, home_stay, hostel, bed_and_breakfast, resort, cottage, guest_house, room, suite, dorm, hotel. |
property_category
|
string | Optional | vacation_rental or resort_hotel. |
description
|
string | Required | The guest-facing description. Max 2000 characters. |
address
|
object | Required | Where the property is. |
address.address_line_1
|
string | Required | Max 255 characters. |
address.address_line_2
|
string | Optional | Max 255 characters. |
address.landmark
|
string | Optional | Max 255 characters. |
address.city
|
string | Required | Max 100 characters. |
address.state
|
string | Required | Max 100 characters. |
address.country
|
string | Optional | Max 100 characters. Defaults to India. |
address.pin_code
|
string | Required | Postal code, max 20 characters. |
address.latitude
|
number | Optional | -90 to 90. |
address.longitude
|
number | Optional | -180 to 180. |
address.formatted_address
|
string | Optional | Max 500 characters. Derived from the other address fields when omitted. |
space
|
object | Required | Room counts. |
space.bedrooms
|
integer | Required | 0 to 500. |
space.washrooms
|
integer | Required | 0 to 500. |
occupancy
|
object | Required | Guest limits for the property as a whole. |
occupancy.min_guests
|
integer | Optional | 1 or more. Defaults to 1. |
occupancy.max_guests
|
integer | Required | 1 to 500, not below min_guests. |
pricing
|
object | Required | The opening base rates. Doorloom owns pricing from here on: the host edits it in the app and you receive rates.changed. You may correct these base rates later with PUT. |
pricing.weekday
|
object | Required | Monday to Thursday nights. |
pricing.weekday.base_price
|
number | Required | INR per night, 0 or more. |
pricing.weekday.per_night_occupancy
|
integer | Optional | Guests included in the base price. Defaults to occupancy.max_guests. |
pricing.weekday.extra_adult_price
|
number | Optional | INR per extra adult per night. Defaults to 0. |
pricing.weekday.extra_child_price
|
number | Optional | INR per extra child per night. Defaults to 0. |
pricing.weekend
|
object | Required | Friday and Saturday nights. Same fields as weekday. |
units
|
array | Optional | The sellable units (rooms, cottages, tents). 1 to 50 entries. Omit it for a single-unit listing and Doorloom seeds one default unit. |
units[].name
|
string | Required | Max 255 characters. Unique within the request. |
units[].external_id
|
string | Optional | Your id for the unit — the external_unit_id you may use on bookings. Same character rules as external_id; unique within your integration. |
units[].code
|
string | Optional | A short code, max 64 characters. |
units[].max_occupancy
|
integer | Optional | Guests the unit sleeps. Not enforced by Doorloom. |
features
|
object | Optional | House rules and facts. |
features.is_private
|
boolean | Optional | The whole place, not shared. Defaults to false. |
features.is_pet_friendly
|
boolean | Optional | Defaults to false. |
features.max_pets
|
integer | Optional | 0 to 50. |
features.pet_charges
|
number | Optional | INR per pet. |
features.is_veg
|
boolean | Optional | Vegetarian-only kitchen. Defaults to false. |
features.is_smoking_in_rooms
|
boolean | Optional | Defaults to false. |
Nothing else is accepted. The owner, the brand, the status, the coverage and the cluster position are decided by Doorloom from your API key, and any such field in the body is ignored. Photos, amenities, meal plans, cancellation policies and booking rules are set by the host in the Doorloom app after creation.
Minimal request
curl -X POST https://doorloom.com/api/integrations/v1/properties \
-H "Authorization: Bearer $DOORLOOM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2c5d0b7e-1f7a-4c1b-9b3e-3e7d9a4f6c21" \
-d '{
"external_id": "VILLA-001",
"name": "Amber Villa, Alibaug",
"property_type": "villa",
"description": "Sea-facing villa with a private pool and staff.",
"address": { "address_line_1": "12 Kihim Beach Road", "city": "Alibaug", "state": "Maharashtra", "pin_code": "402201" },
"space": { "bedrooms": 3, "washrooms": 3 },
"occupancy": { "max_guests": 8 },
"pricing": { "weekday": { "base_price": 12000 }, "weekend": { "base_price": 15000 } }
}'
That creates a single-unit villa with one default unit, priced from the day it exists.
Complete request
{
"external_id": "RESORT-77",
"name": "Kihim Sands Resort",
"property_type": "resort",
"property_category": "resort_hotel",
"description": "Twelve garden cottages and a pool on the beach road.",
"address": {
"address_line_1": "4 Beach Road",
"address_line_2": "Near Kihim jetty",
"landmark": "Opposite the lighthouse",
"city": "Alibaug",
"state": "Maharashtra",
"country": "India",
"pin_code": "402201",
"latitude": 18.7302,
"longitude": 72.8711
},
"space": {
"bedrooms": 12,
"washrooms": 12
},
"occupancy": {
"min_guests": 1,
"max_guests": 30
},
"pricing": {
"weekday": {
"base_price": 6500,
"per_night_occupancy": 2,
"extra_adult_price": 1200,
"extra_child_price": 600
},
"weekend": {
"base_price": 8500,
"per_night_occupancy": 2,
"extra_adult_price": 1500,
"extra_child_price": 800
}
},
"units": [
{
"name": "Garden Cottage 1",
"external_id": "GC-1",
"code": "GC1",
"max_occupancy": 3
},
{
"name": "Garden Cottage 2",
"external_id": "GC-2",
"code": "GC2",
"max_occupancy": 3
},
{
"name": "Pool Suite",
"external_id": "PS-1",
"code": "PS1",
"max_occupancy": 4
}
],
"features": {
"is_private": false,
"is_pet_friendly": true,
"max_pets": 1,
"pet_charges": 500,
"is_veg": false,
"is_smoking_in_rooms": false
}
}
Response
| Field | Type | Description |
|---|---|---|
201
|
created | data is the property block, exactly as GET /properties returns it. |
{
"success": true,
"message": "Property created",
"data": {
"doorloom_id": 902,
"external_id": "RESORT-77",
"coverage": "active",
"name": "Kihim Sands Resort",
"property_type": "resort",
"status": "active",
"is_master": true,
"master_property_id": 902,
"units": [
{
"inventory_id": 3101,
"external_unit_id": "GC-1",
"name": "Garden Cottage 1",
"code": "GC1",
"is_active": true,
"is_default": true,
"max_occupancy": 3,
"deleted": false
},
{
"inventory_id": 3102,
"external_unit_id": "GC-2",
"name": "Garden Cottage 2",
"code": "GC2",
"is_active": true,
"is_default": false,
"max_occupancy": 3,
"deleted": false
},
{
"inventory_id": 3103,
"external_unit_id": "PS-1",
"name": "Pool Suite",
"code": "PS1",
"is_active": true,
"is_default": false,
"max_occupancy": 4,
"deleted": false
}
]
}
}
Response fields
| Field | Type | Description |
|---|---|---|
doorloom_id
|
integer | Doorloom's permanent id for the property. Store it. |
external_id
|
string|null | The id you sent, now mapped. Null if you sent none. |
coverage
|
string | Always active on a create: the property is in your scope at once. |
name
|
string | The listing name you sent. |
property_type
|
string|null | As sent. |
status
|
string | active on a create. See the note on plans below. |
is_master
|
boolean | Always true on a create: a new property is standalone. |
master_property_id
|
integer | Equals doorloom_id on a create. |
units[]
|
array | The units, in the order you sent them, each with its permanent inventory_id and the external_unit_id you mapped. The first is the default unit. |
Doorloom ids and your ids
Every property has a permanent doorloom_id. Your external_id is a
mapping on top of it, scoped to your integration: another integration may use the same string for
a different property, and a property may have no external id at all. The mapping is created with
the property, in the same transaction, so there is never a moment where the property exists and
the id does not.
Sending an external_id that is already mapped to one of your properties is a
409 PROPERTY_EXTERNAL_ID_EXISTS. Nothing is created, and errors.doorloom_id
tells you which property already carries it — if that is the same listing, store the mapping and
carry on. To change a property's external id later, use
PUT /properties/{id}.
The same rules apply to units[].external_id, which becomes the unit's
external_unit_id. Unit aliases are unique within your integration; one already
mapped to another unit is refused with 422 VALIDATION_FAILED on
units.N.external_id.
Units
Every property in Doorloom has at least one unit. Omit units[] and Doorloom seeds
one default unit; send units[] and it creates exactly those, the first one being the
default. Creation is transactional: the property, its units, the address and the prices either all
exist or none do. Add, rename or deactivate units afterwards through
PUT /properties/{id};
there is no separate unit endpoint.
Coverage
The property is added to your integration's coverage in the same transaction, with
coverage: "active", whether or not your integration auto-enrols new listings. It is
listed by GET /properties and accepted by POST /bookings from the moment
the 201 arrives. Other integrations of the same brand that auto-enrol new listings also gain it, as
they would for a property the host created in the app.
What happens afterwards
Within seconds you receive the property's full initial state through the normal webhook path,
the same set POST /sync produces for
one property:
property.changed,
inventory.changed,
and availability.changed,
rates.changed and
restrictions.changed
in month-sized windows across your horizon. That is around forty deliveries per property, all
subject to your event subscription and the sequence rule. The 201 already carries the property
block, so none of them tells you anything you do not know; they exist so your calendar starts from
Doorloom's state rather than your assumptions.
Properties need a plan
status: "inactive" on
GET /properties — without a property.changed event, since that check
runs as a bulk job. The brand keeps its plan current in the Doorloom app.Validation
- Required:
name,description,address(line 1, city, state, pin code),space(bedrooms, washrooms),occupancy.max_guests, and bothpricinggroups with abase_price. external_idandunits[].external_id: letters, digits and. _ : -only, up to 128 characters.property_typeandproperty_categorymust be one of the listed values; anything else is a 422, never coerced.units[]: 1 to 50 entries, unique names, unique external ids.- Every failure is a
422 VALIDATION_FAILEDwhoseerrorskeys are the dotted field paths, e.g.address.pin_codeorunits.2.external_id.
Errors
Errors
| Status | Code | Meaning |
|---|---|---|
| 422 | VALIDATION_FAILED |
The request body failed validation. errors is Laravel's field bag: an object keyed by field path, each holding an array of messages. |
| 422 | IDEMPOTENCY_KEY_REQUIRED |
A create — POST /bookings, POST /properties or POST /properties/bulk — was sent without a usable Idempotency-Key header. |
| 422 | IDEMPOTENCY_KEY_REUSED |
The key has already been used, with a different request body. Doorloom fingerprints the body (SHA-256 over key-sorted JSON), so key order does not matter but any value change does. Keys are shared across every create endpoint, so a booking key resent on a property create is a reuse too. |
| 409 | IN_PROGRESS |
An identical request is still being processed. Two requests raced; the first one is still inside its transaction. |
| 409 | PROPERTY_EXTERNAL_ID_EXISTS |
The external_id you sent is already mapped to a property of your integration. On a create, that property was not created; on an update, the mapping was not changed. errors.doorloom_id tells you which property holds the id, so you can store the mapping or pick a different id. |
| 429 | RATE_LIMITED |
You exceeded one of the rate limits. The standard Retry-After and X-RateLimit-* headers are set on the response. |
| 401 | UNAUTHENTICATED |
No API key, or a key that is not valid. Rotating your API key invalidates the previous one immediately. |
| 403 | NOT_INTEGRATION_TOKEN |
The bearer token is valid but is not an integration API key — it belongs to another Doorloom token surface. Integration keys are the only credential this API accepts. |
| 403 | INTEGRATION_DISABLED |
Doorloom staff disabled this integration. No events are recorded and no API call succeeds. This is distinct from paused: a paused integration keeps full API access and only stops receiving webhooks. |
Retrying
| Field | Type | Description |
|---|---|---|
Timeout or 5xx
|
retry, same key | Resend the identical body with the same Idempotency-Key. If the first attempt actually succeeded you get its 201 back; if it did not, nothing was left behind and the create runs afresh. |
409 IN_PROGRESS
|
retry, same key | Your own earlier attempt is still inside its transaction. Retry in a few seconds. |
429
|
retry after Retry-After |
The write tier is 30 creates a minute; use the bulk endpoint for a portfolio. |
409 PROPERTY_EXTERNAL_ID_EXISTS
|
do not retry | Store errors.doorloom_id if it is the same listing, or correct the id and send with a new key. |
422
|
do not retry unchanged | Fix the body and send with a new key. |