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.
| 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.
| 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 |
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"201 Created — the booking. A HOLD carries holdExpiresAt; a CONFIRMED does not.409 Conflict — the booking was not created, and detail.reason says why:
reason | Meaning |
|---|---|
OVERLAP | The slot, or its buffer, is taken |
CLOSED | The villa is closed that day |
OUTSIDE_OPENING_HOURS | The slot falls outside the opening range |
INVALID_INTERVAL | End before start |
DURATION_NOT_PRICED | That duration has no rate for this villa |
CURRENCY_NOT_CONFIGURED | The organization has not chosen a currency |
PROPERTY_NOT_BOOKABLE | Unknown, archived, or another organization's villa |
CUSTOMER_NOT_FOUND | Unknown, or another organization's customer |
409 Conflict — the plan limit for bookings is reached.
422 Unprocessable Entity — a malformed field; details lists 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.
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',
})