API reference
Every route the API serves, who may call it, and how sign-in, permissions, errors, lists 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). Photos 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:
{
"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 decimal numbers in the property's currency.
The health check needs no token:
curl http://localhost:8000/api/health{"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:
{
"success": false,
"data": null,
"message": "…",
"errors": { "email": ["…"] }
}messageis a sentence in the request's language.errorsappears on a validation failure, with the problems per field, keyed by the path the client sent (such astitle.en). 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 answers403. - A request over a limit answers
429.
Lists and paging
{ "data": [ ], "page": 1, "limit": 12, "total": 15, "totalPages": 2 }- Properties, inquiries and viewings page with
pageandpage_size: 12 a page by default on the website (at most 24), 10 in the dashboard (at most 50). The dashboard's lists also return their counts per status. - Admins, roles and settings page with
pageandpage_count. - Unless the route has its own order, rows come newest-updated first, with the id as a tiebreaker, so paging is stable.
- The website's property filters are validated: an unknown value is refused with
400rather 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. Properties and neighborhoods carry both languages as { "en": "…", "ar": "…" } whatever the header says, and the client picks one.
Signing in
Sign in as an admin
Terminalcurl -X POST http://localhost:8000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"admin@velra.example","password":"Admin@123"}'data.access_tokenis the token, withtoken_typeBearerand the admin with their roles and permissions. The token lastsJWT_EXPIRATION,7dby default, andexpires_inreports that value as written.GET /api/auth/mereturns the signed-in admin with their roles and permissions.Call a protected route
Terminalcurl http://localhost:8000/api/leads -H "Authorization: Bearer YOUR_TOKEN"
- An unknown email and a wrong password both answer
401with the same message. - Ten failed sign-ins from one address within 15 minutes answer
429until 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.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/auth/login | Anyone | Admin sign-in: email, password |
GET | /api/auth/me | Any signed-in admin | The 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 31 permissions and the two seeded roles are listed in Admins, roles and permissions.
Properties on the website
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/properties | Anyone | Published homes. purpose (sale, rent), type (apartment, villa, townhouse), neighborhood (slug), min_price, max_price, bedrooms (0 for studios, otherwise a minimum), amenities (comma separated slugs, all required), q (title or reference), sort (newest, price_asc, price_desc), page, page_size |
GET | /api/properties/featured | Anyone | The home page's featured homes: the references in the home_featured_references setting, in order, or published homes marked featured when none of those is published |
GET | /api/properties/facets | Anyone | Published homes counted by purpose, by type and by neighborhood, for the filters and the home page |
GET | /api/properties/lookup | Anyone | Published homes by id, for saved homes and compare: ?ids=1,2,3, at most 12 |
GET | /api/properties/:slug/similar | Anyone | Up to three similar published homes |
GET | /api/properties/:slug | Anyone | One published home in full, with its gallery, its neighborhood and its public staged room |
Neighborhoods and the agency's settings
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/neighborhoods | Anyone | The neighborhoods marked visible, in their sort order |
GET | /api/settings/public | Anyone | The settings the website renders: agency_name, the taglines and descriptions in both languages, contact_email, agency_city, currency, timezone, home_featured_references and home_staging_property_slug |
The contact form and viewing requests
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/leads | Anyone | The contact form: name, email, intent (buy, rent, other), client_token (a UUID v4), and optionally phone, preferred_areas (slugs), budget, message (1000 characters), property_id. Answers 201 with { id, received_at } |
POST | /api/viewings | Anyone | A viewing request: property_id (a published home), name, email, preferred_date (YYYY-MM-DD), preferred_time (HH:MM, agency time), client_token, and optionally phone, message. Answers 201 with { reference, status, preferred_at, timezone } |
- A repeated
client_tokenreturns the first submission's receipt instead of storing a second one. - A filled
companyfield is treated as a bot: the answer looks the same, and nothing is stored. - A viewing must start at least an hour ahead, in
Asia/Dubaitime; sooner answers400withpreferred_time_too_soononpreferred_at. - Each address may send five of each in ten minutes; the next answers
429.
Managing properties
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/properties/manage | Admins with properties.view | Every property, drafts included. purpose, type, status, q, page, page_size; returns counts of all, published and drafts |
GET | /api/properties/manage/:id | Admins with properties.view | One property in full, with its gallery and staging pairs |
POST | /api/properties/manage | Admins with properties.create | Create a draft. Required: title (en, ar), purpose, type, price, bedrooms, bathrooms, area_sqm |
PUT | /api/properties/manage/:id | Admins with properties.edit | Change any field, the gallery included (media, up to 40 items) |
PATCH | /api/properties/manage/:id/status | Admins with properties.publish | Set status: draft, published, sold, rented or archived |
PUT | /api/properties/manage/:id/media | Admins with properties.edit | Replace the gallery, in order, with one item marked is_cover |
DELETE | /api/properties/manage/:id | Admins with properties.delete | Delete the property |
Inquiries and viewings
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/leads | Admins with leads.view | Inquiries. status, q, received_within_days, page, page_size; with counts per status |
GET | /api/leads/:id | Admins with leads.view | One inquiry with its property, notes and viewing |
PATCH | /api/leads/:id | Admins with leads.edit | Set status (new, contacted, qualified, closed) and notes |
GET | /api/viewings | Admins with viewings.view | Viewings. status, q, scheduled_from, scheduled_to, page, page_size; with counts per status |
GET | /api/viewings/:id | Admins with viewings.view | One viewing with its inquiry and property |
PATCH | /api/viewings/:id | Admins with viewings.edit | action (confirm, complete, cancel), confirmed_at (ISO 8601) and admin_notes. An action from the wrong status answers 400 |
GET | /api/overview | Admins with overview.view | The overview: published counts, new inquiries, viewings awaiting confirmation and coming up, recent rows, featured homes and the map's points |
Neighborhoods and settings
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/neighborhoods/manage | Admins with neighborhoods.view | Every neighborhood, hidden ones included |
POST | /api/neighborhoods/manage | Admins with neighborhoods.create | Add one: name (en, ar), and optionally slug, short_description, city, is_featured, sort_order, is_active, cover_url, cover_alt |
PUT | /api/neighborhoods/manage/:id | Admins with neighborhoods.edit | Change one |
DELETE | /api/neighborhoods/manage/:id | Admins with neighborhoods.delete | Delete one; its homes keep existing without a neighborhood |
GET | /api/settings | Admins with settings.view | Every setting. search, category, page, page_count |
GET | /api/settings/:key | Admins with settings.view | One setting by key |
PATCH | /api/settings/:key | Admins with settings.edit | Change one setting's value |
DELETE | /api/settings/:key | Admins with settings.edit | Delete one setting |
Admins and roles
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/admins | Admins with admins.view | Admins. email, name, phone, page, page_count |
GET | /api/admins/statistics | Admins with admins.view | Counts for the admin list |
GET | /api/admins/:id | Admins with admins.view | One admin, by id or username |
POST | /api/admins | Admins with admins.create | Add an admin: first_name, last_name, email, password and password_confirmation (6 characters or more), optionally phone |
PATCH | /api/admins/:id | Admins with admins.edit | Change an admin |
PATCH | /api/admins/:id/roles | Admins with admins.assign_roles | Set an admin's roles: role_ids |
DELETE | /api/admins/:id | Admins with admins.delete | Delete an admin |
PATCH | /api/admins/profile | Any signed-in admin | Change the signed-in admin's own first_name, last_name, email, phone, profile_picture or country_id. A password or role_ids in the body answers 400: the password has its own route |
PATCH | /api/admins/profile/password | Any signed-in admin | Change the signed-in admin's own password: current_password, password, password_confirmation |
GET | /api/roles | Admins with roles.view | Roles. name, guard_name, created_from, created_to, page, page_count |
GET | /api/roles/statistics | Admins with roles.view | Counts for the roles list |
GET | /api/roles/select | Admins with roles.view | Every role's id and name, for a picker |
GET | /api/roles/permissions | Admins with roles.view | Every permission, grouped by area |
GET | /api/roles/:id | Admins with roles.view | One role with its permissions |
POST | /api/roles | Admins with roles.create | Add a role |
PUT | /api/roles/:id | Admins with roles.edit | Rename or describe a role |
POST | /api/roles/:id/permissions | Admins with roles.assign_permissions | Set a role's permissions |
DELETE | /api/roles/:id | Admins with roles.delete | Delete a role |
Notifications and uploads
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/notifications | Any signed-in admin | The signed-in admin's newest 30 notifications |
PATCH | /api/notifications/read-all | Any signed-in admin | Mark them all read |
PATCH | /api/notifications/:id/read | Any signed-in admin | Mark one read |
DELETE | /api/notifications/:id | Any signed-in admin | Remove one |
POST | /api/helpers/upload | Any signed-in admin | Store a file (multipart file, with path for the folder, for for the size preset such as property, and type). Photos are re-encoded, at most 1920 pixels wide; videos are accepted as MP4, WebM, MOV or M4V; 150 MB at most. Answers the stored file's address |
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
| Route | Limit | Variable |
|---|---|---|
POST /api/auth/login | 10 failed sign-ins per 15 minutes | RATE_LIMIT_LOGIN |
POST /api/leads | 5 per 10 minutes | None |
POST /api/viewings | 5 per 10 minutes | None |
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.
Staging studio
Included with your purchase. Sign in to read, or open it in your download.
The studio's routes: generations, saving a staged pair and approving it.
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 route
Included with your purchase. Sign in to read, or open it in your download.
The route that exists only on a public demo build.