Create a booking
Write a booking your system took into Doorloom. This is the only endpoint that requires an idempotency key, and the only one 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.
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 |
POST /bookings 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. |
| 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 set by Doorloom staff 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 and cannot take bookings. |
| 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.