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.
Transactions
Total Value
Areas
Projects
API Base URL
https://api.buyorsell24.comGetting an API Key
All API requests require a Bearer token for authentication. Follow these steps to obtain your key:
- 1
Visit the Contact Page
Go to dynamicweblab.com/contact and fill out the form requesting API access.
- 2
Receive Your Token
You will receive a Bearer token via email within 24 hours.
- 3
Store It Securely
Add your token to your
.envfile. 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-apiRequired Packages
# Guzzle is included with Laravel by default
# If you need to install or update it:
composer require guzzlehttp/guzzleEnvironment 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_hereHTTP 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
.envto.gitignore
Don't
- ✗Hardcode API keys in source files
- ✗Return raw API responses to the client
- ✗Commit
.envto Git - ✗Log API keys or tokens
Common Endpoints
/api/v1/transactionsProperty 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;/api/v1/areasAll 356 Dubai areas with building counts, stats, and project data.
$areas = $this->api->getAreas();
// Returns: { "areas": [...], "total": 356 }/api/v1/projects3,517 projects with stats, buildings, transactions, and price trends.
$projects = $this->api->getProjects([
'developer' => 'Emaar',
'limit' => 10,
]);/api/v1/developersDeveloper 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
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
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
· {{ $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>
@endsectionRoute 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.