Skip to content

JavaScript

This guide wires the Catalogue API into a static website with modern JavaScript and the native fetch API. Every snippet is an ES module; load it with <script type="module">. The same client is reused by the React and Vue guides.

  • An agency API key — see Getting started.
  • An agency with an approved licence and at least one approved developer contract; otherwise the API answers 403 or returns an empty catalogue. See Authentication.
  • A page served over HTTP(S) from a domain you control. Browsers send an Origin header that the key’s domain restriction checks.
  1. On estate.azizdubai.com, open API keys and issue a key named after the site, for example Website (browser).

  2. Restrict the key to the domains that serve the site. List every host separately — example.com does not cover www.example.com.

  3. Verify the key from the terminal:

    Terminal window
    curl https://agent-dev.azizdubai.com/whoIAm -H "X-API-Key: azk_<your key>"

    A restricted key needs an Origin header here too: add -H "Origin: https://example.com".

azizFetch(path, params, options) adds the key, serializes filter parameters (arrays become CSV, empty values are dropped), and unwraps the response envelope: it returns data on success and throws an AzizApiError carrying the HTTP status and the API message otherwise.

aziz.js
const AZIZ_API_BASE = 'https://agent-dev.azizdubai.com';
const AZIZ_API_KEY = 'azk_…'; // browser key, restricted to your domains
export class AzizApiError extends Error {
constructor(status, message) {
super(message);
this.name = 'AzizApiError';
this.status = status;
}
}
function toQuery(params) {
const query = new URLSearchParams();
for (const [name, value] of Object.entries(params)) {
if (value === undefined || value === null || value === '') continue;
if (Array.isArray(value)) {
if (value.length > 0) query.set(name, value.join(','));
} else {
query.set(name, String(value));
}
}
return query.toString();
}
export async function azizFetch(path, params = {}, { method = 'GET', body, signal } = {}) {
const url = new URL(path, AZIZ_API_BASE);
url.search = toQuery(params);
const headers = { 'X-API-Key': AZIZ_API_KEY, Accept: 'application/json' };
if (body !== undefined) headers['Content-Type'] = 'application/json';
const response = await fetch(url, {
method,
headers,
signal,
body: body === undefined ? undefined : JSON.stringify(body),
});
let payload;
try {
payload = await response.json();
} catch {
throw new AzizApiError(response.status, `unexpected ${response.status} response`);
}
if (payload.status !== 'success') {
throw new AzizApiError(response.status, payload.message ?? 'unknown error');
}
return payload.data;
}
export function formatPrice(amount, currency = 'AED') {
return amount == null ? 'Price on request' : `${currency} ${Math.round(amount).toLocaleString('en')}`;
}
export function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
}

Prefer Authorization: Bearer <key> over X-API-Key? Replace the header — both are accepted.

GET /projects returns { total, items }. Pass limit (up to 100) and offset, and use total to decide whether there is a next page. price_min and price_max are in the key’s prices; bedrooms is a CSV where 0 is a studio and 5 means 5+. See Filtering projects and Pagination.

<form id="filters">
<input name="price_min" type="number" min="0" step="1000" placeholder="Min price" />
<input name="price_max" type="number" min="0" step="1000" placeholder="Max price" />
<fieldset>
<legend>Bedrooms</legend>
<label><input type="checkbox" name="bedrooms" value="0" /> Studio</label>
<label><input type="checkbox" name="bedrooms" value="1" /> 1</label>
<label><input type="checkbox" name="bedrooms" value="2" /> 2</label>
<label><input type="checkbox" name="bedrooms" value="3" /> 3</label>
<label><input type="checkbox" name="bedrooms" value="4" /> 4</label>
<label><input type="checkbox" name="bedrooms" value="5" /> 5+</label>
</fieldset>
<button type="submit">Search</button>
</form>
<p id="summary"></p>
<div id="projects"></div>
<button id="prev" type="button">Previous</button>
<button id="next" type="button">Next</button>
<script type="module" src="./projects.js"></script>

GET /projects/{id} returns the whole project — location, cost, payment plan, media and units — in one response. A draft, deleted or unknown project answers 404.

<article id="project">Loading…</article>
<script type="module" src="./project.js"></script>

POST /units/{id}/applications records the enquiry in your agency’s CRM. The body requires client.first_name and at least one of client.email or client.mobile_number; preferences.based_on_unit: true derives the client’s interests from the selected unit. The form below sends the fields a typical enquiry needs — see Submitting applications for the full body.

<form id="application">
<input name="first_name" required maxlength="255" placeholder="First name" />
<input name="last_name" maxlength="255" placeholder="Last name" />
<input name="email" type="email" maxlength="255" placeholder="Email" />
<input name="mobile_number" type="tel" maxlength="50" placeholder="+971 50 000 00 00" />
<select name="currency">
<option value="">Project currency</option>
<option value="AED">AED</option>
<option value="USD">USD</option>
</select>
<textarea name="message" maxlength="5000" placeholder="Message"></textarea>
<button type="submit">Send enquiry</button>
<p id="result" role="status"></p>
</form>
<script type="module" src="./apply.js"></script>
  • Handle 401 and 403 as configuration errors. missing api key, invalid api key and origin is not allowed for this api key will not fix themselves on retry — log them and show a generic message. See Troubleshooting.
  • Never commit the key. Inject it at build time from your hosting provider’s environment settings instead of hard-coding it in aziz.js.
  • Cache GET responses. Catalogue data changes rarely; keep list and project responses in memory or sessionStorage for a few minutes instead of re-requesting on every navigation. Never cache POST requests.
  • Use separate keys for browser and server. The browser key carries a domain list; a key used from a server, cron job or CRM must have no domain list, because server requests carry no Origin or Referer and are rejected by a restricted key.
  • Use a separate key for local development. A key restricted to your production domains rejects requests from localhost.