Skip to content

WordPress

This guide builds a single-file plugin, aziz-catalogue.php. WordPress calls the API from the server, caches responses in transients, renders projects with shortcodes, and forwards enquiries through its own REST route — the key never reaches the browser.

  • WordPress 6.0 or later on PHP 7.4 or later, with outgoing HTTPS allowed.
  • An agency API key — see Getting started.
  • An agency with an approved licence and at least one approved developer contract. See Authentication.
  1. On estate.azizdubai.com, open API keys and issue a key named after the site, for example WordPress (server). Do not add a domain list.

  2. Add the key to wp-config.php, above the /* That's all, stop editing! */ line:

    wp-config.php
    define( 'AZIZ_API_KEY', 'azk_…' );
  3. Verify the key from the server:

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

Save the plugin as wp-content/plugins/aziz-catalogue/aziz-catalogue.php and activate it under Plugins. aziz_api_get() adds the key, serializes parameters (arrays become CSV, empty values are dropped), unwraps the response envelope and caches successful responses for 10 minutes. Errors are returned as WP_Error with the API message and the HTTP status, and are not cached.

The key is read from the AZIZ_API_KEY constant. To manage it from the admin instead, store it with update_option( 'aziz_api_key', 'azk_…', false ) and remove the constant — aziz_api_key() falls back to the option.

aziz-catalogue.php
<?php
/**
* Plugin Name: Aziz Catalogue
* Description: Projects from the Aziz Catalogue API and a server-side enquiry endpoint.
* Version: 1.0.0
*/
defined( 'ABSPATH' ) || exit;
if ( ! defined( 'AZIZ_API_BASE' ) ) {
define( 'AZIZ_API_BASE', 'https://agent-dev.azizdubai.com' );
}
function aziz_api_key() {
return defined( 'AZIZ_API_KEY' ) ? AZIZ_API_KEY : (string) get_option( 'aziz_api_key', '' );
}
/**
* GET an API path. Returns `data` from the envelope or a WP_Error.
*/
function aziz_api_get( $path, $params = array() ) {
$params = array_filter(
$params,
static function ( $value ) {
return null !== $value && '' !== $value && array() !== $value;
}
);
$params = array_map(
static function ( $value ) {
return is_array( $value ) ? implode( ',', $value ) : $value;
},
$params
);
$url = AZIZ_API_BASE . $path . ( $params ? '?' . http_build_query( $params ) : '' );
$cache_key = 'aziz_' . md5( $url );
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
$response = wp_remote_get(
$url,
array(
'timeout' => 10,
'headers' => array(
'X-API-Key' => aziz_api_key(),
'Accept' => 'application/json',
),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
$payload = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $payload ) || 'success' !== ( $payload['status'] ?? '' ) ) {
$message = $payload['message'] ?? "unexpected $code response";
return new WP_Error( 'aziz_api_error', $message, array( 'status' => $code ) );
}
set_transient( $cache_key, $payload['data'], 10 * MINUTE_IN_SECONDS );
return $payload['data'];
}
function aziz_format_price( $amount, $currency = 'AED' ) {
return null === $amount ? 'Price on request' : $currency . ' ' . number_format_i18n( $amount );
}
function aziz_error_html( WP_Error $error ) {
// The API message is for logs; visitors get a generic text.
error_log( 'Aziz API: ' . $error->get_error_message() );
$status = $error->get_error_data()['status'] ?? 0;
$text = 404 === $status ? 'This project is no longer available.' : 'The catalogue is temporarily unavailable.';
return '<p class="aziz-error">' . esc_html( $text ) . '</p>';
}
/**
* Number from the query string, or '' when absent.
*/
function aziz_query_number( $name ) {
$value = isset( $_GET[ $name ] ) ? sanitize_text_field( wp_unslash( $_GET[ $name ] ) ) : '';
return is_numeric( $value ) && $value >= 0 ? $value : '';
}
/**
* [aziz_projects limit="12" project_page="/project/"]
*/
add_shortcode( 'aziz_projects', 'aziz_projects_shortcode' );
function aziz_projects_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'limit' => 12,
'project_page' => '/project/',
),
$atts,
'aziz_projects'
);
$limit = min( 100, max( 1, (int) $atts['limit'] ) );
$page = max( 1, isset( $_GET['aziz_page'] ) ? absint( $_GET['aziz_page'] ) : 1 );
$bedrooms = isset( $_GET['bedrooms'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['bedrooms'] ) ) : array();
$filters = array(
'price_min' => aziz_query_number( 'price_min' ),
'price_max' => aziz_query_number( 'price_max' ),
'bedrooms' => array_values( array_unique( array_filter( $bedrooms, static fn( $n ) => $n <= 5 ) ) ),
);
$data = aziz_api_get( '/projects', $filters + array( 'limit' => $limit, 'offset' => ( $page - 1 ) * $limit ) );
if ( is_wp_error( $data ) ) {
return aziz_error_html( $data );
}
ob_start();
?>
<form class="aziz-filters" method="get">
<input name="price_min" type="number" min="0" step="1000" placeholder="Min price" value="<?php echo esc_attr( $filters['price_min'] ); ?>">
<input name="price_max" type="number" min="0" step="1000" placeholder="Max price" value="<?php echo esc_attr( $filters['price_max'] ); ?>">
<?php foreach ( array( 0 => 'Studio', 1 => '1', 2 => '2', 3 => '3', 4 => '4', 5 => '5+' ) as $value => $label ) : ?>
<label>
<input type="checkbox" name="bedrooms[]" value="<?php echo esc_attr( $value ); ?>" <?php checked( in_array( $value, $filters['bedrooms'], true ) ); ?>>
<?php echo esc_html( $label ); ?>
</label>
<?php endforeach; ?>
<button type="submit">Search</button>
</form>
<?php if ( 0 === (int) $data['total'] ) : ?>
<p>No projects match these filters.</p>
<?php endif; ?>
<div class="aziz-grid">
<?php foreach ( $data['items'] as $project ) : ?>
<a class="aziz-card" href="<?php echo esc_url( add_query_arg( 'project_id', $project['id'], home_url( $atts['project_page'] ) ) ); ?>">
<?php if ( ! empty( $project['logo'] ) ) : ?>
<img src="<?php echo esc_url( $project['logo'] ); ?>" alt="" loading="lazy">
<?php endif; ?>
<h3><?php echo esc_html( $project['name'] ); ?></h3>
<p><?php echo esc_html( $project['company']['name'] ?? '' ); ?></p>
<p>From <?php echo esc_html( aziz_format_price( $project['price_from'], $project['currency'] ) ); ?></p>
</a>
<?php endforeach; ?>
</div>
<nav class="aziz-pagination">
<?php if ( $page > 1 ) : ?>
<a href="<?php echo esc_url( add_query_arg( 'aziz_page', $page - 1 ) ); ?>">Previous</a>
<?php endif; ?>
<?php if ( $page * $limit < $data['total'] ) : ?>
<a href="<?php echo esc_url( add_query_arg( 'aziz_page', $page + 1 ) ); ?>">Next</a>
<?php endif; ?>
</nav>
<?php
return ob_get_clean();
}
/**
* [aziz_project id="42" apply_page="/apply/"] — without id, reads ?project_id=.
*/
add_shortcode( 'aziz_project', 'aziz_project_shortcode' );
function aziz_project_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'id' => isset( $_GET['project_id'] ) ? absint( $_GET['project_id'] ) : 0,
'apply_page' => '/apply/',
),
$atts,
'aziz_project'
);
$id = absint( $atts['id'] );
if ( ! $id ) {
return '<p class="aziz-error">Project not found.</p>';
}
$project = aziz_api_get( "/projects/$id" );
if ( is_wp_error( $project ) ) {
return aziz_error_html( $project );
}
$currency = $project['cost']['currency'] ?? 'AED';
$images = array_filter( $project['media']['images'] ?? array(), static fn( $image ) => ! empty( $image['file'] ) );
ob_start();
?>
<article class="aziz-project">
<h2><?php echo esc_html( $project['name'] ); ?></h2>
<p><?php echo esc_html( implode( ', ', array_filter( array( $project['area_name'], $project['area_city'] ) ) ) ); ?></p>
<div class="aziz-gallery">
<?php foreach ( $images as $image ) : ?>
<img src="<?php echo esc_url( $image['file']['url'] ); ?>" alt="" loading="lazy">
<?php endforeach; ?>
</div>
<?php echo wpautop( esc_html( $project['description'] ) ); ?>
<table>
<thead><tr><th>Unit</th><th>Bedrooms</th><th>Area, m²</th><th>Price</th><th></th></tr></thead>
<tbody>
<?php foreach ( $project['units'] as $unit ) : ?>
<tr>
<td><?php echo esc_html( $unit['unit_number'] ); ?></td>
<td><?php echo 0 === $unit['bedrooms'] ? 'Studio' : esc_html( $unit['bedrooms'] ); ?></td>
<td><?php echo esc_html( $unit['unit_area'] ); ?></td>
<td><?php echo esc_html( aziz_format_price( $unit['price'], $currency ) ); ?></td>
<td><a href="<?php echo esc_url( add_query_arg( 'unit_id', $unit['id'], home_url( $atts['apply_page'] ) ) ); ?>">Enquire</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</article>
<?php
return ob_get_clean();
}
/**
* POST /wp-json/aziz/v1/apply — forwards an enquiry to POST /units/{id}/applications.
*/
add_action(
'rest_api_init',
static function () {
register_rest_route(
'aziz/v1',
'/apply',
array(
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => 'aziz_rest_apply',
'args' => array(
'unit_id' => array( 'required' => true, 'type' => 'integer', 'minimum' => 1 ),
'first_name' => array( 'required' => true, 'type' => 'string', 'maxLength' => 255 ),
'last_name' => array( 'type' => 'string', 'maxLength' => 255 ),
'email' => array( 'type' => 'string', 'maxLength' => 255 ),
'mobile_number' => array( 'type' => 'string', 'maxLength' => 50 ),
'currency' => array( 'type' => 'string', 'enum' => array( '', 'AED', 'USD' ) ),
'message' => array( 'type' => 'string', 'maxLength' => 5000 ),
),
)
);
}
);
function aziz_rest_apply( WP_REST_Request $request ) {
$body = array_filter(
array(
'client' => array_filter(
array(
'first_name' => sanitize_text_field( $request['first_name'] ),
'last_name' => sanitize_text_field( $request['last_name'] ?? '' ),
'email' => sanitize_email( $request['email'] ?? '' ),
'mobile_number' => sanitize_text_field( $request['mobile_number'] ?? '' ),
)
),
'preferences' => array( 'based_on_unit' => true ),
'message' => sanitize_textarea_field( $request['message'] ?? '' ),
'currency' => $request['currency'] ?? '',
)
);
$response = wp_remote_post(
AZIZ_API_BASE . '/units/' . $request['unit_id'] . '/applications',
array(
'timeout' => 15,
'headers' => array(
'X-API-Key' => aziz_api_key(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $body ),
)
);
if ( is_wp_error( $response ) ) {
error_log( 'Aziz API: ' . $response->get_error_message() );
return new WP_REST_Response( array( 'status' => 'error', 'message' => 'service unavailable' ), 502 );
}
$code = wp_remote_retrieve_response_code( $response );
$payload = json_decode( wp_remote_retrieve_body( $response ), true );
if ( 201 === $code ) {
// The result holds CRM identifiers; the visitor only needs to know it worked.
return new WP_REST_Response( array( 'status' => 'success' ), 201 );
}
if ( 400 === $code || 404 === $code ) {
// Validation and "unit not found" messages are safe to show.
return new WP_REST_Response( array( 'status' => 'error', 'message' => $payload['message'] ?? 'invalid request' ), $code );
}
// 401, 403 and 500 are configuration problems: log them, do not expose them.
error_log( "Aziz API: $code " . ( $payload['message'] ?? '' ) );
return new WP_REST_Response( array( 'status' => 'error', 'message' => 'service unavailable' ), 502 );
}

Add the shortcode to any page:

[aziz_projects limit="12" project_page="/project/"]

It renders a filter form with price_min, price_max and bedrooms, a grid of projects, and Previous / Next links. Filters travel in the page’s query string, and the page number in aziz_page, so every filtered page has its own URL and its own cache entry. Prices are in the key’s prices; bedrooms value 5 means 5+. See Filtering projects and Pagination.

Create a page at /project/ with:

[aziz_project apply_page="/apply/"]

Cards from [aziz_projects] link to it with ?project_id=…. To pin a page to one project, pass it explicitly: [aziz_project id="42"]. A draft, deleted or unknown project answers 404, which the shortcode renders as “no longer available”.

Each unit row links to /apply/?unit_id=…. On that page, add a Custom HTML block with the form below. It posts to the plugin’s REST route, which forwards the enquiry to POST /units/{id}/applications with the key added on the server. client.first_name is required, plus at least one of client.email or client.mobile_number; 400 validation messages are passed through for display. See Submitting applications for the full body.

<form id="aziz-apply">
<input type="hidden" name="unit_id" />
<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="aziz-apply-result" role="status"></p>
</form>

If WordPress is installed in a subdirectory or uses plain permalinks, replace /wp-json/aziz/v1/apply with the URL of rest_url( 'aziz/v1/apply' ).

  • Handle 401 and 403 as configuration errors. The plugin logs them with error_log() and shows visitors a generic message. Check the log after deploying; see Troubleshooting for each message.
  • Never commit the key. Keep wp-config.php out of version control, or read the key from an environment variable there: define( 'AZIZ_API_KEY', getenv( 'AZIZ_API_KEY' ) );.
  • Cache GET responses. The plugin caches each URL for 10 minutes in transients; with a persistent object cache (Redis, Memcached) they stay out of the database. Enquiries are never cached.
  • Use separate keys for browser and server. This plugin needs only a server key without a domain list. If the same site also calls the API from JavaScript, issue a second, domain-restricted key for that.
  • Protect the enquiry endpoint. aziz/v1/apply is public and each call creates a CRM record. Add a CAPTCHA or a honeypot field and rate-limit the route, for example with your security plugin.