Dynamic Web Lab
Integration GuideLaravel 11+

Laravel Integration Guide

Build a property data backend with the Dubai Real Estate Data API using Laravel, Guzzle HTTP client, service classes, caching, queues, and Blade templates.

Overview

The Dubai Real Estate Data API provides comprehensive access to Dubai's property market data, sourced directly from the Dubai Land Department (DLD). This guide walks you through integrating the API into a Laravel application using clean service patterns, route proxies, caching, and background jobs.

1M+

Transactions

AED 4.37T

Total Value

356

Areas

3,517

Projects

API Base URL

https://api.buyorsell24.com

Getting an API Key

All API requests require a Bearer token for authentication. Follow these steps to obtain your key:

  1. 1

    Visit the Contact Page

    Go to dynamicweblab.com/contact and fill out the form requesting API access.

  2. 2

    Receive Your Token

    You will receive a Bearer token via email within 24 hours.

  3. 3

    Store It Securely

    Add your token to your .env file. Never commit API keys to version control.

Laravel Project Setup

Create a New Laravel Project

composer create-project laravel/laravel dubai-property-api
cd dubai-property-api

Required Packages

# Guzzle is included with Laravel by default
# If you need to install or update it:
composer require guzzlehttp/guzzle

Environment Variables

Add the following to your .env file:

# .env
REAL_ESTATE_API_BASE_URL=https://api.buyorsell24.com
REAL_ESTATE_API_KEY=your_bearer_token_here

HTTP Client Configuration

Laravel includes the HTTP client wrapper around Guzzle. Configure a reusable client with your base URL and auth middleware for clean API interactions.

Register a Guzzle Client in a Service Provider

File: app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use GuzzleHttp\Client;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton('real-estate-api', function () {
            return new Client([
                'base_uri' => config('services.real_estate_api.base_url'),
                'timeout' => 30,
                'headers' => [
                    'Authorization' => 'Bearer ' . config('services.real_estate_api.key'),
                    'Content-Type'  => 'application/json',
                    'Accept'        => 'application/json',
                ],
            ]);
        });
    }
}

Add Config Values

File: config/services.php

return [
    // ...existing config

    'real_estate_api' => [
        'base_url' => env('REAL_ESTATE_API_BASE_URL', 'https://api.buyorsell24.com'),
        'key'      => env('REAL_ESTATE_API_KEY'),
    ],
];

API Service Class

Wrap all API calls in a dedicated service class. This keeps your controllers clean and makes it easy to swap or mock the API layer.

RealEstateApiService

File: app/Services/RealEstateApiService.php

<?php

namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;

class RealEstateApiService
{
    private Client $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function getTransactions(array $params = []): array
    {
        return $this->get('/api/v1/transactions', $params);
    }

    public function getAreas(array $params = []): array
    {
        return $this->get('/api/v1/areas', $params);
    }

    public function getProjects(array $params = []): array
    {
        return $this->get('/api/v1/projects', $params);
    }

    public function getDevelopers(array $params = []): array
    {
        return $this->get('/api/v1/developers', $params);
    }

    public function getArea(string $areaId): array
    {
        return $this->get("/api/v1/areas/{$areaId}");
    }

    public function getProject(string $projectId): array
    {
        return $this->get("/api/v1/projects/{$projectId}");
    }

    public function getDeveloper(string $developerId): array
    {
        return $this->get("/api/v1/developers/{$developerId}");
    }

    /**
     * @throws GuzzleException
     */
    private function get(string $endpoint, array $params = []): array
    {
        $response = $this->client->get($endpoint, [
            'query' => array_filter($params, fn ($v) => $v !== null && $v !== ''),
        ]);

        return json_decode($response->getBody()->getContents(), true);
    }
}

Laravel Route Definitions

Define API routes to proxy requests through your Laravel backend. This keeps the API key server-side and lets you add middleware, rate limiting, and caching.

routes/api.php

<?php

use App\Http\Controllers\RealEstateController;
use Illuminate\Support\Facades\Route;

Route::prefix('realestate')->group(function () {
    Route::get('/transactions', [RealEstateController::class, 'transactions']);
    Route::get('/areas', [RealEstateController::class, 'areas']);
    Route::get('/areas/{id}', [RealEstateController::class, 'area']);
    Route::get('/projects', [RealEstateController::class, 'projects']);
    Route::get('/projects/{id}', [RealEstateController::class, 'project']);
    Route::get('/developers', [RealEstateController::class, 'developers']);
    Route::get('/developers/{id}', [RealEstateController::class, 'developer']);
});

Controller Pattern

Controllers act as the thin layer between your routes and the service class. Handle validation, error responses, and caching here.

RealEstateController

File: app/Http/Controllers/RealEstateController.php

<?php

namespace App\Http\Controllers;

use App\Services\RealEstateApiService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class RealEstateController extends Controller
{
    public function __construct(
        private RealEstateApiService $api
    ) {}

    public function transactions(Request $request): JsonResponse
    {
        try {
            $data = $this->api->getTransactions($request->only([
                'type', 'area', 'min_price', 'max_price',
                'from_date', 'to_date', 'limit', 'cursor',
            ]));

            return response()->json($data);
        } catch (\Exception $e) {
            return response()->json([
                'error' => 'Failed to fetch transactions',
            ], 502);
        }
    }

    public function areas(Request $request): JsonResponse
    {
        try {
            $data = $this->api->getAreas($request->only(['limit', 'cursor']));
            return response()->json($data);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Failed to fetch areas'], 502);
        }
    }

    public function area(string $id): JsonResponse
    {
        try {
            $data = $this->api->getArea($id);
            return response()->json($data);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Area not found'], 404);
        }
    }

    public function projects(Request $request): JsonResponse
    {
        try {
            $data = $this->api->getProjects($request->only([
                'developer', 'area', 'limit', 'cursor',
            ]));

            return response()->json($data);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Failed to fetch projects'], 502);
        }
    }

    public function project(string $id): JsonResponse
    {
        try {
            $data = $this->api->getProject($id);
            return response()->json($data);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Project not found'], 404);
        }
    }

    public function developers(Request $request): JsonResponse
    {
        try {
            $data = $this->api->getDevelopers($request->only(['limit', 'cursor']));
            return response()->json($data);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Failed to fetch developers'], 502);
        }
    }

    public function developer(string $id): JsonResponse
    {
        try {
            $data = $this->api->getDeveloper($id);
            return response()->json($data);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Developer not found'], 404);
        }
    }
}

Authentication

All requests to the API must include a Bearer token in the Authorization header. Your API key is stored server-side and never exposed to the frontend.

Do

  • Store API keys in .env
  • Use Laravel config for key access
  • Proxy API calls through your backend
  • Add .env to .gitignore

Don't

  • Hardcode API keys in source files
  • Return raw API responses to the client
  • Commit .env to Git
  • Log API keys or tokens

Common Endpoints

GET/api/v1/transactions

Property transactions from the Dubai Land Department. Filter by type, area, price range, and date.

$response = $this->api->getTransactions([
    'type'      => 'sale',
    'area'      => 'Palm Jumeirah',
    'min_price' => 1000000,
    'limit'     => 20,
]);

$transactions = $response['data'];
$nextCursor   = $response['next_cursor'] ?? null;
GET/api/v1/areas

All 356 Dubai areas with building counts, stats, and project data.

$areas = $this->api->getAreas();
// Returns: { "areas": [...], "total": 356 }
GET/api/v1/projects

3,517 projects with stats, buildings, transactions, and price trends.

$projects = $this->api->getProjects([
    'developer' => 'Emaar',
    'limit'     => 10,
]);
GET/api/v1/developers

Developer directory with AI-generated profiles, projects, and stats.

$developers = $this->api->getDevelopers();
// Returns: { "developers": [...] }

Error Handling

Handle Guzzle exceptions and map API error codes to meaningful responses for your frontend consumers.

Service-Level Error Handling

<?php

namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use Illuminate\Support\Facades\Log;

class RealEstateApiService
{
    private Client $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    private function get(string $endpoint, array $params = []): array
    {
        try {
            $response = $this->client->get($endpoint, [
                'query' => array_filter($params, fn ($v) => $v !== null && $v !== ''),
            ]);

            return json_decode($response->getBody()->getContents(), true);
        } catch (ClientException $e) {
            $status = $e->getResponse()->getStatusCode();
            $body   = json_decode($e->getResponse()->getBody()->getContents(), true);

            Log::warning('API Client Error', [
                'endpoint' => $endpoint,
                'status'   => $status,
                'message'  => $body['message'] ?? 'Unknown error',
            ]);

            throw new \RuntimeException(
                $body['message'] ?? 'API request failed',
                $status
            );
        } catch (ServerException $e) {
            Log::error('API Server Error', [
                'endpoint' => $endpoint,
                'status'   => 500,
            ]);

            throw new \RuntimeException('API service unavailable', 503);
        }
    }
}

Global Exception Handler

File: app/Exceptions/Handler.php

use RuntimeException;

public function register(): void
{
    $this->renderable(function (RuntimeException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'error' => $e->getMessage(),
            ], $e->getCode() >= 400 && $e->getCode() < 600 ? $e->getCode() : 500);
        }
    });
}

Rate Limits

The API enforces rate limits per API key. The base plan allows 100 requests per minute. Implement caching and retry logic to stay within limits.

Rate Limit Tiers

Startup Lite
100
requests / minute
Startup Growth
300
requests / minute
Enterprise
Custom
unlimited

Retry with Exponential Backoff

private function getWithRetry(string $endpoint, array $params = [], int $maxRetries = 3): array
{
    $lastException = null;

    for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
        try {
            $response = $this->client->get($endpoint, [
                'query' => array_filter($params, fn ($v) => $v !== null && $v !== ''),
            ]);

            return json_decode($response->getBody()->getContents(), true);
        } catch (ClientException $e) {
            if ($e->getResponse()->getStatusCode() === 429 && $attempt < $maxRetries) {
                $retryAfter = $e->getResponse()->getHeaderLine('Retry-After');
                $delay      = $retryAfter ? (int) $retryAfter * 1000 : (2 ** $attempt) * 1000;

                usleep($delay * 1000);
                continue;
            }

            throw $e;
        }
    }

    throw $lastException ?? new \RuntimeException('Max retries exceeded');
}

Pagination

Use the next_cursor field to paginate through large result sets. Pass the cursor as a query parameter to fetch the next page.

Cursor-Based Pagination

// In a controller method
public function allTransactions(Request $request): JsonResponse
{
    $cursor = $request->query('cursor');
    $limit  = $request->query('limit', 20);

    $params = ['limit' => $limit];
    if ($cursor) {
        $params['cursor'] = $cursor;
    }

    $data = $this->api->getTransactions($params);

    return response()->json([
        'data'        => $data['data'],
        'next_cursor' => $data['next_cursor'] ?? null,
    ]);
}

Caching with Laravel Cache

Use the Laravel Cache facade to reduce API calls and improve response times. Cache responses for areas, projects, and other slow-changing data.

Cache in the Controller

<?php

namespace App\Http\Controllers;

use App\Services\RealEstateApiService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cache;

class RealEstateController extends Controller
{
    public function __construct(
        private RealEstateApiService $api
    ) {}

    public function areas(): JsonResponse
    {
        $data = Cache::remember('api:areas', now()->addHours(24), function () {
            return $this->api->getAreas();
        });

        return response()->json($data);
    }

    public function projects(Request $request): JsonResponse
    {
        $developer = $request->query('developer');
        $cacheKey  = 'api:projects:' . md5($request->getQueryString() ?? 'all');

        $data = Cache::remember($cacheKey, now()->addHours(6), function () use ($request) {
            return $this->api->getProjects($request->only([
                'developer', 'area', 'limit', 'cursor',
            ]));
        });

        return response()->json($data);
    }

    public function developers(): JsonResponse
    {
        $data = Cache::remember('api:developers', now()->addHours(24), function () {
            return $this->api->getDevelopers();
        });

        return response()->json($data);
    }
}

Recommended Cache Durations

Transactions
1 hour
Data updates daily from DLD
Areas
24 hours
Rarely changes
Projects
6 hours
New projects added periodically
Developers
24 hours
Directory rarely changes

Queue Jobs for Bulk Processing

For bulk data imports or periodic syncs, use Laravel Jobs and Queues to process data in the background without blocking HTTP requests.

ImportTransactionsJob

File: app/Jobs/ImportTransactionsJob.php

<?php

namespace App\Jobs;

use App\Services\RealEstateApiService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class ImportTransactionsJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;

    public function handle(RealEstateApiService $api): void
    {
        $cursor = null;
        $total  = 0;

        do {
            $params = ['limit' => 100];
            if ($cursor) {
                $params['cursor'] = $cursor;
            }

            $data   = $api->getTransactions($params);
            $items  = $data['data'] ?? [];
            $total += count($items);

            foreach ($items as $transaction) {
                // Process each transaction
                // e.g., upsert into local database
                Log::info('Imported transaction', ['id' => $transaction['id']]);
            }

            $cursor = $data['next_cursor'] ?? null;

            // Respect rate limits
            usleep(650000); // 650ms between batches
        } while ($cursor);

        Cache::forget('api:transactions');
        Log::info('Import complete', ['total' => $total]);
    }
}

Dispatch the Job

// From a controller, artisan command, or scheduler
use App\Jobs\ImportTransactionsJob;

// Dispatch immediately
ImportTransactionsJob::dispatch();

// Or dispatch with a delay
ImportTransactionsJob::dispatch()->delay(now()->addMinutes(5));

Blade Template Integration

Render property data in Blade templates for server-rendered pages. Pass cached API data directly to your views.

Controller Rendering a Blade View

<?php

namespace App\Http\Controllers;

use App\Services\RealEstateApiService;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Support\Facades\Cache;

class PropertyPageController extends Controller
{
    public function index(Request $request, RealEstateApiService $api): View
    {
        $transactions = Cache::remember('page:transactions', now()->addHours(1), function () use ($api) {
            return $api->getTransactions(['limit' => 12])['data'] ?? [];
        });

        return view('properties.index', compact('transactions'));
    }

    public function show(string $id, RealEstateApiService $api): View
    {
        $area = Cache::remember("page:area:{$id}", now()->addHours(24), function () use ($api, $id) {
            return $api->getArea($id);
        });

        return view('properties.show', compact('area'));
    }
}

resources/views/properties/index.blade.php

@extends('layouts.app')

@section('title', 'Dubai Property Transactions')

@section('content')
<h1 class="text-3xl font-bold mb-6">Recent Transactions</h1>

<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
    @forelse ($transactions as $transaction)
        <div class="bg-slate-800 rounded-xl p-6 border border-slate-700">
            <h2 class="font-bold text-lg mb-2">
                {{ $transaction['property_name'] ?? 'N/A' }}
            </h2>
            <p class="text-emerald-400 text-xl font-semibold mb-1">
                AED {{ number_format($transaction['price_aed'] ?? 0) }}
            </p>
            <p class="text-slate-400 text-sm">
                {{ $transaction['area_sqft'] ?? 0 }} sqft
                &middot; {{ $transaction['location']['area'] ?? '' }}
            </p>
            <p class="text-slate-500 text-xs mt-2">
                {{ $transaction['transaction_date'] ?? '' }}
            </p>
        </div>
    @empty
        <p class="text-slate-400 col-span-3">No transactions found.</p>
    @endforelse
</div>
@endsection

Route Registration

use App\Http\Controllers\PropertyPageController;

Route::get('/', [PropertyPageController::class, 'index']);
Route::get('/area/{id}', [PropertyPageController::class, 'show']);

Ready to Build?

Get your API key and start integrating Dubai's most comprehensive real estate data into your Laravel application.

UAE PDPL
SOC 2 Type II
GDPR
FTA Ready
CCPA