Everything the app does to a villa, a booking or a customer is reachable over HTTP. A script — or an agent — works on the same data as the interface, under the same rules, without a browser session.
This page is the map: authentication, the shape of every answer, and every endpoint with the page that details it. Each endpoint has its own page: one call, its parameters, its answers and what it refuses.
Every /api/v1 request carries an API key in the x-api-key header. A key is created from
Account → API Keys and belongs to one organization: that organization is the entire scope
of the key.
curl -H "x-api-key: $VILLASLOT_API_KEY" \
"https://villaslot.app/api/v1/me"Not in the body, not in the URL, not in a header. It comes from the key and from nothing else. A
resource belonging to another organization answers 404, exactly like an id that does not exist.
A key is not a second identity with its own powers. It carries the rights of the member who created it, in the organization it was created for. Nothing is possible through the API that the same person could not do in the interface — and nothing is refused that they could.
A key stops working when its bearer leaves the organization. No orphan access survives a
departure: the key answers 401 from that moment, exactly as if it had been revoked. Revoking the
key of someone who left is therefore housekeeping, not a security fix.
Two questions decide every call, and both have to pass:
| Question | Decided by | Answer when it fails |
|---|---|---|
| Is the bearer still a member, and with which role? | The key's bearer and their role | 401 / 403 |
| Does the resource belong to the key's organization? | The key's organization | 404 |
| Operation | Minimum role in the organization |
|---|---|
| Reading anything | member |
| Creating and correcting customers, creating and managing bookings | member |
| Creating, renaming, archiving a villa, its photo, its hours, its rates | admin |
| Changing the booking settings of the organization | admin |
That split is not an API decision: it is the product's own permission matrix. A member is often the one with the customer on the phone, and the product reserves villa configuration to the owner and the organization admins.
| Case | Body |
|---|---|
| Success | {"success": true, "data": …} |
| Paginated list | {"success": true, "data": [...], "pagination": {"total", "page", "limit", "totalPages"}} |
| Error | {"error": "…"} |
| Validation error | {"error": "…", "details": [ … field-level issues … ]} |
| Business refusal | {"error": "…", "detail": { … reason, names, amounts … }} |
| Status | When |
|---|---|
200 | Read or change done |
201 | Resource created |
401 | Missing or invalid key, or a key whose bearer left the organization |
403 | The bearer is a legitimate member, but their role is too low |
404 | Unknown resource, or a resource of another organization |
409 | State conflict — a slot taken, a status that moved, a plan limit reached |
413 | A batch over its bound; the message names the limit |
422 | Invalid parameters or body — details lists the offending fields |
429 | Rate limit of the key exceeded (120 requests per minute) |
500 | Unexpected server error |
A resource of another organization answers 404, never 403. A 403 would confirm that the
id exists. The same response for an unknown id and for an id you are not allowed to read — you
cannot tell them apart, and that is the point. The 403 is reserved for the other question: you
are in the right organization, your role is too low.
1500000 in IDR is
15 000 rupiah; 4500 in EUR is 45,00 €. Never divide by 100 without knowing the currency.startAt, endAt, createdAt).date (YYYY-MM-DD)
and startMinute (minutes from local midnight). Read settings.timezone from GET /api/v1/me.dayOfWeek following 0 Sunday … 6 Saturday.| Method | Path | What it does | Role |
|---|---|---|---|
GET | /api/v1/me | My context | member |
GET | /api/v1/properties | List villas | member |
POST | /api/v1/properties | Create a villa | admin |
GET | /api/v1/properties/{propertyId} | Get a villa | member |
PATCH | /api/v1/properties/{propertyId} | Rename a villa | admin |
POST | /api/v1/properties/{propertyId}/archive | Archive a villa | admin |
POST | /api/v1/properties/{propertyId}/photo | Upload a photo | admin |
GET | /api/v1/properties/{propertyId}/opening-hours | Get opening hours | member |
PUT | /api/v1/properties/{propertyId}/opening-hours | Replace opening hours | admin |
GET | /api/v1/properties/{propertyId}/rates | Get the rate grid | member |
PUT | /api/v1/properties/{propertyId}/rates | Replace the rate grid | admin |
GET | /api/v1/properties/{propertyId}/availability | Get availability | member |
GET | /api/v1/bookings | List bookings | member |
POST | /api/v1/bookings | Create a booking | member |
GET | /api/v1/bookings/{bookingId} | Get a booking | member |
POST | /api/v1/bookings/{bookingId}/confirm | Confirm a booking | member |
POST | /api/v1/bookings/{bookingId}/complete | Complete a booking | member |
POST | /api/v1/bookings/{bookingId}/cancel | Cancel a booking | member |
POST | /api/v1/bookings/{bookingId}/reschedule | Reschedule a booking | member |
GET | /api/v1/customers | List customers | member |
POST | /api/v1/customers | Create a customer | member |
POST | /api/v1/customers/bulk | Import customers | member |
GET | /api/v1/customers/{customerId} | Get a customer | member |
PATCH | /api/v1/customers/{customerId} | Update a customer | member |
GET | /api/v1/settings/booking | Get booking settings | member |
PATCH | /api/v1/settings/booking | Update booking settings | admin |
The limit is carried by the key, not by the IP: 120 requests per minute. A key over its limit
answers 429 on every endpoint. Back off and retry — nothing was done.
Call GET /api/v1/me first: it turns a bare key into a
context — the organization it opens, the role it carries, the timezone and currency every other
call depends on, and how much room the plan leaves.
Then the usual path: find or create the customer, ask the availability of a villa, and create the booking. The whole sequence, with its refusals, is walked in Create a booking.