Create a booking
Write a booking your system took into Doorloom. Like every create on this API it requires an idempotency key, and it is the only endpoint that can refuse for reasons of inventory.
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 the property creates, so never reuse a key across endpoints.
Refusals are stored too. If a create fails with
409 UNAVAILABLE, retrying the same
key replays that refusal rather than racing for the room again — so a naive retry loop cannot turn
a conflict into a double booking.Request body
Body
| Field | Type | Required | Description |
|---|---|---|---|
external_booking_id
|
string | Required | Your id for this booking. Max 128 characters. Unique within your integration — sending one twice returns the existing booking rather than creating another. |
property
|
object | Required | Which property is being booked. |
property.doorloom_id
|
integer | Optional | Doorloom's property id. |
property.external_id
|
string | Optional | Your alias for the property. Max 128 characters. |
units
|
array | Required | One entry per unit being booked. A two-unit stay is two entries. 1 to 20 entries. |
units[].inventory_id
|
integer | Optional | Pin a specific Doorloom unit. |
units[].external_unit_id
|
string | Optional | Pin a specific unit by your alias. Max 128 characters. |
units[].adults
|
integer | Required | 1 to 50. |
units[].children
|
integer | Optional | 0 to 50. Defaults to 0. |
units[].meal_plan
|
string | Optional | EP, CP, MP or AP. |
units[].stay_price
|
number | Optional | Room money for this unit across the whole stay. See pricing below. |
check_in
|
string | Required | YYYY-MM-DD. May not be before yesterday. |
check_out
|
string | Required | YYYY-MM-DD. Must be after check_in. Exclusive — the guest leaves this morning. |
guest
|
object | Required | The lead guest. |
guest.name
|
string | Required | Max 255 characters. |
guest.phone_code
|
string | Optional | Dialling code, max 8 characters. Defaults to +91. |
guest.mobile
|
string | Required | Max 20 characters. |
guest.email
|
string | Optional | Valid email, max 255 characters. |
guest.country
|
string | Optional | Max 64 characters. |
pricing
|
object | Required | What the guest is being charged. |
pricing.total
|
number | Required | The booking total. Doorloom stores it as given and does not re-price it. |
pricing.currency
|
string | Optional | Must be INR if sent. |
pricing.includes_gst
|
boolean | Optional | Whether total already includes GST. |
pricing.breakdown
|
object | Optional | Free-form. Stored verbatim and handed back to you unchanged; Doorloom never interprets it. |
payment
|
object | Optional | What you have collected so far. |
payment.status
|
string | Optional | paid, partial or unpaid. |
payment.amount_collected
|
number | Optional | Becomes the advance recorded on the booking. |
payment.collected_by
|
string | Optional | partner or host. |
payment.reference
|
string | Optional | Your payment reference. Max 128 characters. |
source
|
object | Optional | Where the booking came from. |
source.channel
|
string | Optional | e.g. airbnb. Max 64 characters. Set once at create — it cannot be patched later. |
source.reference
|
string | Optional | Your reference on that channel. Max 128 characters. |
status
|
string | Optional | confirmed or pending. Defaults to confirmed. Both hold inventory. |
notes
|
string | Optional | Max 2000 characters. |
Give the property as either doorloom_id or
external_id. Sending neither is a 422. Stay length is capped; a stay
longer than the platform maximum is refused on check_out.
Pricing: you set it
Doorloom stores pricing.total as the booking total and does not re-derive it. Your
system is the source of truth for the price of a booking it took.
Per-unit stay_price values are used when every unit has one.
If any is missing, the total is split evenly across units instead. Send all of them or none.
payment.amount_collected becomes the advance, and the balance follows from the
total.
Units: pinning versus allocating
Each entry in units[] is one unit of the property for the whole stay.
- Give
inventory_idorexternal_unit_idto pin a specific unit. - Give neither to have Doorloom allocate any free unit.
A pinned unit that is taken is refused, never swapped
409 UNAVAILABLE and nothing is written. Omit the unit reference if you are
happy with any room.Example
{
"external_booking_id": "PMS-100234",
"property": {
"external_id": "VILLA-9"
},
"units": [
{
"external_unit_id": "ROOM-A",
"adults": 2,
"children": 1,
"meal_plan": "CP",
"stay_price": 8000
},
{
"adults": 2,
"meal_plan": "EP",
"stay_price": 8000
}
],
"check_in": "2026-10-12",
"check_out": "2026-10-14",
"guest": {
"name": "Asha Rao",
"phone_code": "+91",
"mobile": "9876543210",
"email": "[email protected]",
"country": "IN"
},
"pricing": {
"total": 18600,
"currency": "INR",
"includes_gst": false,
"breakdown": {
"room": 16000,
"meals": 2600
}
},
"payment": {
"status": "partial",
"amount_collected": 4000,
"collected_by": "partner",
"reference": "pay_123"
},
"source": {
"channel": "airbnb",
"reference": "HM-77"
},
"status": "confirmed",
"notes": "Late arrival, around 11pm"
}
The first unit is pinned to ROOM-A; the second is left for Doorloom to allocate.
Responses
| Field | Type | Description |
|---|---|---|
201
|
created | A new booking. data is the booking object. |
200
|
duplicate | A booking with this external_booking_id already exists. It is returned unchanged, with data.duplicate: true added. Treat this as success. |
{
"success": true,
"message": "Booking created",
"data": {
"id": 55231,
"external_booking_id": "PMS-100234",
"revision": 1,
"origin": "partner",
"status": "confirmed",
"property": {
"doorloom_id": 902,
"external_id": "VILLA-9"
},
"check_in": "2026-10-12",
"check_out": "2026-10-14",
"guests": {
"adults": 4,
"children": 1
},
"units": [
"…"
],
"pricing": {
"currency": "INR",
"total": 18600,
"advance": 4000,
"balance": 14600
},
"…": "…"
}
}
When the stay is not available
A 409 UNAVAILABLE means no free unit for the whole stay, or a pinned unit that is
taken. Nothing is written. The response carries the per-night picture so you can
correct your calendar without a second call.
{
"success": false,
"code": "UNAVAILABLE",
"message": "Amber Villa, Alibaug is not available for these dates.",
"errors": {
"availability": {
"property": {
"doorloom_id": 902
},
"window": {
"from": "2026-10-12",
"to": "2026-10-13"
},
"total_units": 2,
"dates": [
{
"date": "2026-10-12",
"available_units": 1,
"total_units": 2,
"is_blocked": false,
"block_scope": null,
"units_cap": null
},
{
"date": "2026-10-13",
"available_units": 0,
"total_units": 2,
"is_blocked": false,
"block_scope": null,
"units_cap": null
}
]
}
}
}
The conflict window ends on the last night
errors.availability.window.to is check_out minus one day
— the last night of the stay, not the departure date. It lines up with
dates[], which lists nights.The dates[] array is exactly an
availability.changed
payload minus units[], so you can feed it through the same code path.
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. |
| 422 | PROPERTY_NOT_MAPPED |
The property reference is missing, unknown, or outside your integration's coverage. Coverage is what you created through POST /properties plus what Doorloom staff mapped, and only ever includes properties the brand owner owns outright, never cohosted ones. |
| 422 | PROPERTY_ARCHIVED |
The property exists and is covered, but the host has archived it. Archived properties are off-market: they cannot take bookings, and they cannot be edited through the API. |
| 422 | UNIT_NOT_MAPPED |
A units[] line names a unit alias Doorloom does not know, or one that belongs to a different property. |
| 422 | UNIT_INACTIVE |
The unit is mapped but no longer sellable — deactivated or deleted by the host. |
| 409 | UNAVAILABLE |
There is no free unit for the whole stay, or a unit you pinned is taken. Nothing was written. Doorloom never silently swaps a pinned unit for another. |
| 429 | RATE_LIMITED |
You exceeded one of the rate limits. The standard Retry-After and X-RateLimit-* headers are set on the response. |
What happens afterwards
A successful create emits two things: an
availability.changed
event for the property, because inventory moved, and a
booking.changed
event with origin: "partner" — your own write echoed back. Neither requires any
action from you.