Create a booking
POST /api/v1/bookings
The heart of the product. No business rule is rewritten here: availability, buffer, opening hours, the rate grid, the currency, the timezone and the plan ceiling are all enforced by the same service the app calls.
The date and the start time are local to the organization, not instants: send
date: "2026-09-12" and startMinute: 540 for 09:00 local. Read settings.timezone from
/api/v1/me if you need to know which local that is.
Authorization
| Role in the organization | This endpoint |
|---|---|
owner | Allowed |
admin | Allowed |
member | Allowed |
The product gives members the right to create bookings — they are often the ones with the customer on the phone. Refusing them here would make the API stricter than the screen.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
propertyId | uuid | yes | An active villa of the organization |
customerId | uuid | yes | A customer of the organization |
date | string | yes | Local day YYYY-MM-DD |
startMinute | integer | yes | Minutes from local midnight, e.g. 540 for 09:00 |
durationMinutes | integer | yes | Must be priced for this villa |
status | enum | yes | HOLD or CONFIRMED |
priceAmount | integer | no | Negotiated price; defaults to the grid amount |
depositPercent | integer | no | 1 to 100; null means the full price is due |
Example
curl -X POST -H "x-api-key: $VILLASLOT_API_KEY" -H "content-type: application/json" \
-d '{"propertyId":"1f0a…","customerId":"8d3e…","date":"2026-09-12","startMinute":540,"durationMinutes":120,"status":"HOLD"}' \
"https://villaslot.app/api/v1/bookings"Response
201 Created— the booking. AHOLDcarriesholdExpiresAt; aCONFIRMEDdoes not.
What it refuses
-
409 Conflict— the booking was not created, anddetail.reasonsays why:reasonMeaning OVERLAPThe slot, or its buffer, is taken CLOSEDThe villa is closed that day OUTSIDE_OPENING_HOURSThe slot falls outside the opening range INVALID_INTERVALEnd before start DURATION_NOT_PRICEDThat duration has no rate for this villa CURRENCY_NOT_CONFIGUREDThe organization has not chosen a currency PROPERTY_NOT_BOOKABLEUnknown, archived, or another organization's villa CUSTOMER_NOT_FOUNDUnknown, or another organization's customer -
409 Conflict— the plan limit for bookings is reached. -
422 Unprocessable Entity— a malformed field;detailslists them. -
401 Unauthorized— missing or invalid key, or a key whose bearer left the organization. -
404 Not Found— unknown id, or an id belonging to another organization. The two are deliberately indistinguishable. -
429 Too Many Requests— over 120 requests in a minute for this key.
Agent recipe
Ask availability first, then create — a refused POST costs a round trip and tells you no more
than the free slots already did:
const {data} = await get(
`/api/v1/properties/${propertyId}/availability?date=2026-09-12&durationMinutes=120`
)
if (data.slots.length === 0) return 'no slot that day'
await post('/api/v1/bookings', {
propertyId,
customerId,
date: '2026-09-12',
startMinute: 540,
durationMinutes: 120,
status: 'HOLD',
})Notes
- Overriding a conflict is not available over the API. Forcing a booking onto a taken slot is a deliberate human decision, made on screen after seeing what it collides with.
- Overnight stays are not covered by this endpoint yet.
Airbnb availability
Successful writes may return generic warnings with code AIRBNB_CONFLICT or AIRBNB_SYNC_UNAVAILABLE. A blocking policy rejects the operation with the corresponding conflict reason; forceConflict cannot bypass it. When the last successful synchronization is between 65 minutes and 3 hours old, conflicts are still checked against the last snapshot and the AIRBNB_SYNC_UNAVAILABLE warning is returned; beyond 3 hours, blocking data causes a temporary refusal. No feed URL, UID or traveler details are returned.
Check-out / check-in window
When the organization protects the check-out / check-in window (turnoverGuardEnabled), a shoot that starts before
the villa’s check-in time, ends after its check-out time and lasts less than turnoverGuardMinimumMinutes is still
created by the API, with the warning {"code": "TURNOVER_WINDOW", "minimumMinutes": 240}. Moving a booking
(POST /api/v1/bookings/{id}/reschedule) returns it in the booking’s warnings too. Only the public pages refuse
that slot.
If an Airbnb reservation or an active overnight stay of the same villa departs or arrives on the shoot’s local day,
a shoot that overlaps the window is concerned whatever its duration: it is created with the warning
{"code": "TURNOVER_ADJACENT_STAY"} instead of TURNOVER_WINDOW.
A real payment is retained when automatic confirmation is prevented. The booking keeps its unconfirmed status and airbnbResolutionRequiredAt; the client link shows that team intervention is needed. Payment replay never clears this marker. Explicit confirmation after a new availability check, or explicit cancellation, resolves it. No automatic refund is issued.