Skip to the article
Aniq-UI

HollinaAPI reference

API reference

Every route the API serves, who may call it, and how sign-in, permissions, errors, lists, bookings and limits work.

For the Full Stack package

Base URL and response format

Every route is served under the /api prefix. Locally the base URL is http://localhost:8000/api. On a server it is your API's address followed by /api, the same value both frontends read from NEXT_PUBLIC_API_BASE_URL (Environment variables). Photographs are served outside the prefix, at /media/….

The website's routes are public; the dashboard's need an admin's bearer token, and most also a permission. Request bodies are JSON, except file uploads. Every answer comes in the same envelope:

Envelope
{
  "success": true,
  "data": { },
  "message": ""
}

data holds the result. message is a short sentence some writes fill in, in the request's language, and empty otherwise. Prices are whole US dollars, and dates are company calendar days written YYYY-MM-DD.

The health check needs no token:

Terminal
curl http://localhost:8000/api/health
Response
{"success":true,"data":{"status":"ok","database":"up"},"message":""}

When the database cannot be read it still answers 200, with "status":"degraded" and "database":"down". The Docker image's health check reads that field.

Errors

A failure keeps the envelope, with success: false and the HTTP status. message is a sentence in the request's language, code the untranslated key behind it, and a refusal that names something adds the values behind its sentence in details:

Error
{
  "success": false,
  "data": null,
  "message": "Booking B0021 already holds some of these nights.",
  "code": "dates_taken",
  "details": { "held_by": "booking", "reference": "B0021" }
}

A body the validation pipe refuses carries code: "validation_failed" and errors, keyed by each field's full name:

Validation error
{
  "success": false,
  "data": null,
  "message": "guest_email must be an email",
  "errors": { "guest_email": ["guest_email must be an email"] },
  "code": "validation_failed"
}
  • Branch on code, never on the sentence, which changes with the language. Booking and availability refusals carry codes such as dates_taken, min_nights, capacity, cancellation_window_passed, awaiting_payment and payment_method_unavailable, and a limit rate_limited.
  • errors appears on a validation failure, with the problems under the field's name as the request spelled it (check_in, guest_email). An unknown field in a body is refused.
  • A guarded route with no token, or an expired one, answers 401; a token without the permission answers 403.
  • Nights already held answer 409, a booking step from the wrong status 422, and a request over a limit 429.

Lists and paging

A list
{ "data": [ ], "page": 1, "page_size": 6, "total": 12, "total_pages": 2 }
  • Stays, bookings, inquiries and reviews page with page and page_size: 6 stays a page on the website (at most 24), 20 in the dashboard (at most 50), 10 bookings and 20 inquiries (at most 50 each), 8 reviews. The dashboard's lists also return counts per status.
  • Admins, roles and settings page with page and page_count (settings also take page_size), 10 by default and at most 100, and answer { data, page, limit, total, totalPages }.
  • The website's stay filters are validated: an unknown value is refused with 400 rather than ignored, while a key the API does not read is ignored, so a shared link with extra parameters still works.

Language

Send Accept-Language: ar for Arabic messages; anything else answers in English. Stays, destinations and the home page questions carry both languages as { "en": "…", "ar": "…" } whatever the header says, and the client picks one.

Signing in

  1. Sign in as an admin

    Terminal
    curl -X POST http://localhost:8000/api/auth/login \
      -H "Content-Type: application/json" \
      -d '{"email":"admin@hollina.example","password":"Admin@123"}'

    data.access_token is the token, with token_type Bearer and the admin with their roles and permissions. The token lasts JWT_EXPIRATION, 7d by default, and expires_in reports that value as written. GET /api/auth/me returns the signed-in admin with their roles and permissions.

  2. Call a protected route

    Terminal
    curl http://localhost:8000/api/bookings/manage -H "Authorization: Bearer YOUR_TOKEN"
  • An unknown email and a wrong password both answer 401 with the same message.
  • Ten failed sign-ins from one address within 15 minutes answer 429 until the oldest failure is 15 minutes old (RATE_LIMIT_LOGIN). Successful sign-ins are not counted.
  • There is no sign-up and no sign-out route: accounts come from the seed or from Admin management, and a client signs out by forgetting its token.
  • Guests have no accounts. A guest's booking routes take the booking's access_token as ?token=, or the email it was made with as ?email=; a wrong pair answers 404.
MethodPathWho can call itWhat it does
POST/api/auth/loginAnyoneAdmin sign-in: email, password
GET/api/auth/meAny signed-in adminThe signed-in admin, their roles and permissions

Permissions

Dashboard routes check a permission named <area>.<action>. An admin holds every permission of every role they have, and a route that names several accepts any one of them. The 32 permissions and the two seeded roles are listed in Admins, roles and permissions.

Stays on the website

MethodPathWho can call itWhat it does
GET/api/staysAnyonePublished stays. destination (slug), checkIn and checkOut (YYYY-MM-DD, both or neither: only stays free for every night), guests (the fewest it must sleep), type (villa, apartment, cabin, chalet, loft, cottage), sort (recommended, price_asc, price_desc, rating), page, page_size
GET/api/stays/featuredAnyoneThe home page's featured stays: the references in the home_featured_references setting, in order, published ones only
GET/api/stays/heroAnyoneThe stay named by home_hero_stay_reference, or null
GET/api/stays/facetsAnyonePublished stays counted by type and by destination, for the filters and the home page
GET/api/stays/lookupAnyonePublished stays by id, for saved stays: ?ids=1,2,3, at most 12
GET/api/stays/:slugAnyoneOne published stay in full, with its photographs, amenities, house rules and destination
GET/api/stays/:slug/similarAnyoneUp to three similar published stays
GET/api/stays/:slug/availabilityAnyoneThe nights already held between from and to, 92 days at most
GET/api/stays/:slug/quoteAnyonecheckIn, checkOut, guests (1 to 50): nights, nightly rate, subtotal, cleaning fee, total, available and the refusal reason (past, min_nights, capacity, taken)
GET/api/stays/:slug/reviewsAnyoneThe stay's published reviews, newest first, with its average

Destinations, questions and the company's settings

MethodPathWho can call itWhat it does
GET/api/destinationsAnyoneThe destinations marked visible, in their sort order
GET/api/destinations/:slugAnyoneOne destination
GET/api/faqAnyoneThe home page's questions and answers, in order
GET/api/settings/publicAnyoneThe settings the website renders: company_name, the tagline and description in both languages, contact_email, contact_phone, contact_address_hint, currency, timezone, the default check-in and check-out times, allow_pay_on_arrival, cancellation_policy, home_featured_references, home_hero_stay_reference and service_email_signature
GET/api/payments/methodsAnyoneWhat the checkout may offer, in priority order: { methods: [{ method, hosted }], allowPayOnArrival, simulated }. Only configured processors' methods are listed, on_arrival while its setting is on, and simulated is true while the simulator takes card and PayPal because no processor is configured

Bookings

MethodPathWho can call itWhat it does
POST/api/bookingsAnyoneThe checkout: stay_slug, check_in, check_out, guests, guest_first_name, guest_last_name, guest_email, payment_method (card, paypal, on_arrival), client_token (8 to 64 characters), and optionally guest_phone, special_requests, locale (en, ar) and, for the simulator only, card_number, card_name, card_expiry (MM/YY), card_cvc. Answers 201 with { reference, access_token, status, payment_status, payment_method, total, currency, redirect_url, failure_reason }
GET/api/bookings/:referenceAnyone with the booking's token or emailThe guest's booking, with can_cancel and can_review
POST/api/bookings/:reference/settleAnyone with the booking's token or emailCalled by the return page: captures an approved PayPal order, and answers the booking's current state for Stripe. Safe to call repeatedly
POST/api/bookings/:reference/cancelAnyone with the booking's token or emailThe guest cancels, while pending or confirmed and at least seven days before local check-in; later answers 422 with cancellation_window_passed
POST/api/bookings/:reference/reviewAnyone with the booking's token or emailReview a completed booking, once: rating (1 to 5, halves allowed), comment (10 to 2000 characters)
POST/api/webhooks/payments/:providerThe provider, by its signaturestripe or paypal. Every verified event answers 200; a bad signature answers 401, an unknown provider 404
  • A repeated client_token returns the first booking's receipt instead of making a second one, and a filled company field is treated as a bot: the answer looks the same, and nothing is stored.
  • A payment_method that GET /api/payments/methods does not list (PayPal with only Stripe configured, card with only PayPal, pay on arrival with its setting off) answers 422 with payment_method_unavailable, before anything is written.
  • The nights are checked when the booking is written: held nights answer 409 with dates_taken, and a stay's minimum, its capacity or a past check-in 422 with the reason.
  • A hosted method answers pending with a redirect_url to Stripe or PayPal; the booking is confirmed only by the processor's webhook or the PayPal capture.

Questions to the host

MethodPathWho can call itWhat it does
POST/api/inquiriesAnyoneThe contact form and the stay page's question dialog: name, email, message (10 to 4000 characters), client_token, and optionally phone, stay_slug (a published stay), check_in, check_out, guests
  • A repeated client_token returns the first submission instead of storing a second one, and a filled company field is treated as a bot.
  • Each address may send five bookings, five questions and five reviews in ten minutes; the next answers 429.

Managing stays

MethodPathWho can call itWhat it does
GET/api/stays/manageAdmins with stays.viewEvery stay, drafts included. status, type, destination, guests, bedrooms, nights (the longest minimum), price (the highest nightly rate), sort (updated, rate_asc, rate_desc), q, page, page_size
GET/api/stays/manage/:idAdmins with stays.viewOne stay in full
POST/api/stays/manageAdmins with stays.createCreate a draft. Required: title, short_description, description and location_hint (en, ar), type, destination_id, bedrooms, beds, bathrooms, max_guests, nightly_rate, cleaning_fee, min_nights
PUT/api/stays/manage/:idAdmins with stays.editChange any field, the gallery included (media, up to 30 items)
PATCH/api/stays/manage/:id/statusAdmins with stays.publishSet status: draft or published. Publishing needs three photographs and both languages
PUT/api/stays/manage/:id/mediaAdmins with stays.editReplace the gallery, in order; position one is the cover
DELETE/api/stays/manage/:idAdmins with stays.deleteDelete the stay; refused with 409 while a booking that is not cancelled exists

The calendar and blocked dates

MethodPathWho can call itWhat it does
GET/api/calendarAdmins with calendar.viewThe month view: month (YYYY-MM) and optionally stay
GET/api/availability/blocksAdmins with calendar.viewEvery block, soonest first
POST/api/availability/blocksAdmins with calendar.blockBlock nights: stay_id, from, to (exclusive, like a check-out), and optionally reason (maintenance, owner_use, other) and note. A conflict answers 409 with code dates_taken and details: { held_by, reference }, naming the booking or block in the way
PUT/api/availability/blocks/:idAdmins with calendar.blockChange a block
DELETE/api/availability/blocks/:idAdmins with calendar.blockRemove a block

Bookings, inquiries and reviews

MethodPathWho can call itWhat it does
GET/api/bookings/manageAdmins with bookings.viewBookings. status, from and to (check-in range), q, stay, payment, method, guests, sort (newest, check_in, total), page, page_size; with counts per status
GET/api/bookings/manage/:referenceAdmins with bookings.viewOne booking with its timeline and the actions allowed now, each with its reason when it is not
PATCH/api/bookings/manage/:referenceAdmins with bookings.editaction (confirm, check_in, complete, cancel) and admin_notes. A step from the wrong status answers 422 with its reason
GET/api/inquiriesAdmins with inquiries.viewInquiries. status, q, from, to, stay, page, page_size; with counts per status
GET/api/inquiries/:idAdmins with inquiries.viewOne inquiry with its stay
PATCH/api/inquiries/:idAdmins with inquiries.editSet status (new, replied, closed) and admin_notes
GET/api/reviews/manageAdmins with reviews.viewReviews. status, stay (id or slug), rating (a minimum), q, since (7, 30, 90, 365 days), page; with counts
PATCH/api/reviews/manage/:idAdmins with reviews.moderateSet status: published or hidden
GET/api/overviewAdmins with overview.viewThe overview: this month's cards, today's arrivals and departures, pending and latest bookings, top stays, guest countries, bookings by status, latest reviews and open inquiries
GET/api/overview/seriesAdmins with overview.viewBookings, revenue and occupancy for each of the last twelve months

Destinations, questions and settings

MethodPathWho can call itWhat it does
GET/api/destinations/manageAdmins with destinations.viewEvery destination, hidden ones included
POST/api/destinations/manageAdmins with destinations.createAdd one: name, tagline and description (en, ar), and optionally slug, cover_url, cover_alt, sort_order, is_active
PUT/api/destinations/manage/:idAdmins with destinations.editChange one
DELETE/api/destinations/manage/:idAdmins with destinations.deleteDelete one; refused while stays belong to it
GET/api/faq/manageAdmins with settings.viewEvery home page question, hidden ones included
POST/api/faq/manageAdmins with settings.editAdd one: question and answer (en, ar), optionally sort_order and is_active
PUT/api/faq/manage/:idAdmins with settings.editChange one
DELETE/api/faq/manage/:idAdmins with settings.editDelete one
GET/api/settingsAdmins with settings.viewEvery setting. search, category, page, page_size or page_count
GET/api/settings/payments-statusAdmins with settings.view{ stripe: { configured, testMode }, paypal: { configured, sandbox }, simulator }: whether each processor is configured and test or live, and whether the simulator is in use, which it is only while no processor is configured. Never a key
GET/api/settings/:keyAdmins with settings.viewOne setting by key
PATCH/api/settings/:keyAdmins with settings.editChange one setting's value
DELETE/api/settings/:keyAdmins with settings.editDelete one setting

Admins and roles

MethodPathWho can call itWhat it does
GET/api/adminsAdmins with admins.viewAdmins. email, name, phone, page, page_count
GET/api/admins/statisticsAdmins with admins.viewCounts for the admin list
GET/api/admins/:idAdmins with admins.viewOne admin, by id or username
POST/api/adminsAdmins with admins.createAdd an admin: first_name, last_name, email, password and password_confirmation (6 characters or more), optionally phone
PATCH/api/admins/:idAdmins with admins.editChange an admin
PATCH/api/admins/:id/rolesAdmins with admins.assign_rolesSet an admin's roles: role_ids
DELETE/api/admins/:idAdmins with admins.deleteDelete an admin
PATCH/api/admins/profileAny signed-in adminChange the signed-in admin's own first_name, last_name, email, phone or profile_picture. The password has its own route
PATCH/api/admins/profile/passwordAny signed-in adminChange the signed-in admin's own password: current_password, password, password_confirmation
GET/api/rolesAdmins with roles.viewRoles. name, guard_name, created_from, created_to, page, page_count
GET/api/roles/statisticsAdmins with roles.viewCounts for the roles list
GET/api/roles/selectAdmins with roles.viewEvery role's id and name, for a picker
GET/api/roles/permissionsAdmins with roles.viewEvery permission, grouped by area
GET/api/roles/:idAdmins with roles.viewOne role with its permissions
POST/api/rolesAdmins with roles.createAdd a role
PUT/api/roles/:idAdmins with roles.editRename or describe a role
POST/api/roles/:id/permissionsAdmins with roles.assign_permissionsSet a role's permissions
DELETE/api/roles/:idAdmins with roles.deleteDelete a role

Notifications and uploads

MethodPathWho can call itWhat it does
GET/api/notificationsAny signed-in adminThe signed-in admin's newest 30 notifications
PATCH/api/notifications/read-allAny signed-in adminMark them all read
PATCH/api/notifications/:id/readAny signed-in adminMark one read
DELETE/api/notifications/:idAny signed-in adminRemove one
POST/api/helpers/uploadAny signed-in adminStore a file (multipart file, with path for the folder, for for the size preset such as stay, cover or profile, and type). Photographs are re-encoded as JPEG, at most 1920 pixels wide; videos are accepted as MP4, WebM, MOV or M4V; 150 MB at most. Answers the stored file's addresses

An upload has no permission of its own: what the file becomes is checked by the route that saves it. Notifications are kept for 30 days.

Limits per address

RouteLimitVariable
POST /api/auth/login10 failed sign-ins per 15 minutesRATE_LIMIT_LOGIN
POST /api/bookings5 per 10 minutesNone
POST /api/inquiries5 per 10 minutesNone
POST /api/bookings/:reference/review5 per 10 minutesNone

A request over a limit answers 429. The counts live in the API's memory, so a restart clears them. Behind a reverse proxy, set TRUST_PROXY so each visitor is counted on their own address.

AI assistant and its conversations

Included with your purchase. Sign in to read, or open it in your download.

The assistant's streaming route, its model list and the saved conversations.

MCP server

Included with your purchase. Sign in to read, or open it in your download.

The MCP endpoint for coding agents and how it is authorised.

Live updates over WebSocket

Included with your purchase. Sign in to read, or open it in your download.

The Socket.IO namespaces the dashboard listens on, their events and how a connection is authorised.

Demo mode routes

Included with your purchase. Sign in to read, or open it in your download.

The routes that exist only for a public demo build.

Stuck on a step?

Find a fix before you start over.

Troubleshooting

Cookie Preferences

We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising.